@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
|
@@ -0,0 +1,220 @@
|
|
|
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
|
+
import { realpathSync } from 'fs';
|
|
9
|
+
import { resolve, sep } from 'path';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
import { getAvailableAdapters } from '../adapters/index.js';
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// SENSITIVE PATH GUARD
|
|
14
|
+
// =============================================================================
|
|
15
|
+
/**
|
|
16
|
+
* Returns the directory's *canonical* absolute path if it's safe to use as a
|
|
17
|
+
* workingDir, or null if it resolves to a sensitive system location. We deny
|
|
18
|
+
* roots like `/`, `/etc`, `~`, `~/.ssh`, etc. — paths *inside* a project root
|
|
19
|
+
* are fine. The check resolves symlinks via realpath so a symlinked alias of
|
|
20
|
+
* a sensitive directory is also caught.
|
|
21
|
+
*/
|
|
22
|
+
export function checkSensitiveWorkingDir(input) {
|
|
23
|
+
let resolved;
|
|
24
|
+
try {
|
|
25
|
+
// realpath if it exists; otherwise fall back to resolve() so the standard
|
|
26
|
+
// adapter cwd-existence check produces the user-visible error.
|
|
27
|
+
resolved = realpathSync(input);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
resolved = resolve(input);
|
|
31
|
+
}
|
|
32
|
+
const home = homedir();
|
|
33
|
+
const rawDenylist = [
|
|
34
|
+
'/',
|
|
35
|
+
'/etc',
|
|
36
|
+
'/var',
|
|
37
|
+
'/usr',
|
|
38
|
+
'/bin',
|
|
39
|
+
'/sbin',
|
|
40
|
+
'/boot',
|
|
41
|
+
'/root',
|
|
42
|
+
home,
|
|
43
|
+
`${home}${sep}.ssh`,
|
|
44
|
+
`${home}${sep}.aws`,
|
|
45
|
+
`${home}${sep}.config`,
|
|
46
|
+
`${home}${sep}.gnupg`,
|
|
47
|
+
];
|
|
48
|
+
// Resolve denylist symlinks too (e.g. macOS /etc -> /private/etc) so the
|
|
49
|
+
// resolved-path comparison hits regardless of symlink direction.
|
|
50
|
+
const denylist = new Set();
|
|
51
|
+
for (const path of rawDenylist) {
|
|
52
|
+
denylist.add(path);
|
|
53
|
+
try {
|
|
54
|
+
denylist.add(realpathSync(path));
|
|
55
|
+
}
|
|
56
|
+
catch { /* ignore — path doesn't exist */ }
|
|
57
|
+
}
|
|
58
|
+
if (denylist.has(resolved)) {
|
|
59
|
+
return { ok: false, reason: `workingDir resolves to a sensitive path: ${resolved}` };
|
|
60
|
+
}
|
|
61
|
+
return { ok: true, resolved };
|
|
62
|
+
}
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// SECTION VALIDATION
|
|
65
|
+
// =============================================================================
|
|
66
|
+
const REQUIRED_SECTIONS = [
|
|
67
|
+
'Recommendation',
|
|
68
|
+
'Reasoning',
|
|
69
|
+
'Tradeoffs',
|
|
70
|
+
'Risks',
|
|
71
|
+
'Open questions for the asker',
|
|
72
|
+
];
|
|
73
|
+
/**
|
|
74
|
+
* Lightweight check for the 5 expected `## …` headers in a model's consult
|
|
75
|
+
* response. Behaviors:
|
|
76
|
+
* - Strips fenced code blocks first so a quoted format-example skeleton
|
|
77
|
+
* doesn't falsely satisfy the check.
|
|
78
|
+
* - Requires the section name as a word boundary at the start of an H2 line,
|
|
79
|
+
* but tolerates trailing decoration (colon, em-dash continuation, etc.).
|
|
80
|
+
* - Case-sensitive on the section name. Bare bold (`**Recommendation**`),
|
|
81
|
+
* wrong level (`### Recommendation`), and ALL-CAPS variants all count as
|
|
82
|
+
* missing — that's the signal we want CC to see when models drift.
|
|
83
|
+
*/
|
|
84
|
+
export function validateConsultSections(output) {
|
|
85
|
+
// Remove fenced code blocks so headers inside them don't satisfy the regex.
|
|
86
|
+
const stripped = output.replace(/```[\s\S]*?```/g, '');
|
|
87
|
+
const missing = [];
|
|
88
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
89
|
+
// Match the exact section name at the start of an H2 header line.
|
|
90
|
+
// `\b` after the name allows `:`, `—`, `-`, whitespace+more — but not
|
|
91
|
+
// suffixed letters/digits (which would change the section name itself).
|
|
92
|
+
const pattern = new RegExp(`^##\\s+${escapeRegex(section)}\\b[^\\n]*$`, 'm');
|
|
93
|
+
if (!pattern.test(stripped)) {
|
|
94
|
+
missing.push(section);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { missing };
|
|
98
|
+
}
|
|
99
|
+
function escapeRegex(s) {
|
|
100
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
101
|
+
}
|
|
102
|
+
// =============================================================================
|
|
103
|
+
// INPUT SCHEMA
|
|
104
|
+
// =============================================================================
|
|
105
|
+
export const ConsultInputSchema = z.object({
|
|
106
|
+
workingDir: z.string().describe('Working directory for the CLI to operate in'),
|
|
107
|
+
question: z.string().describe('CC-composed self-contained question for the panel'),
|
|
108
|
+
relevantFiles: z.array(z.string()).optional().describe('CC-triaged file subset for code-grounded questions'),
|
|
109
|
+
customPrompt: z.string()
|
|
110
|
+
.optional()
|
|
111
|
+
// Reject any literal `<user-steering` or `</user-steering` so a steering
|
|
112
|
+
// value cannot escape the prompt envelope and inject instructions.
|
|
113
|
+
.refine(v => !v || !/<\/?user-steering/i.test(v), {
|
|
114
|
+
message: 'customPrompt must not contain <user-steering> tags',
|
|
115
|
+
})
|
|
116
|
+
.describe('Free-form steering from $ARGUMENTS'),
|
|
117
|
+
reasoningEffort: z.enum(['high', 'xhigh']).optional().describe("Codex reasoning effort (default: 'xhigh' for consult)"),
|
|
118
|
+
serviceTier: z.enum(['default', 'fast', 'flex']).optional().describe("Codex service tier (default: 'fast')"),
|
|
119
|
+
});
|
|
120
|
+
function toConsultRequest(input) {
|
|
121
|
+
return {
|
|
122
|
+
workingDir: input.workingDir,
|
|
123
|
+
question: input.question,
|
|
124
|
+
relevantFiles: input.relevantFiles,
|
|
125
|
+
customPrompt: input.customPrompt,
|
|
126
|
+
reasoningEffort: input.reasoningEffort,
|
|
127
|
+
serviceTier: input.serviceTier,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function formatOutcome(outcome) {
|
|
131
|
+
const { adapter, result } = outcome;
|
|
132
|
+
const name = adapter.getCapabilities().name;
|
|
133
|
+
if (!result.success) {
|
|
134
|
+
const emoji = {
|
|
135
|
+
cli_not_found: '❌', timeout: '⏱️', rate_limit: '🚫', auth_error: '🔐', cli_error: '❌',
|
|
136
|
+
};
|
|
137
|
+
let msg = `## ${name}\n${emoji[result.error.type] || '❌'} **${result.error.type}**: ${result.error.message}`;
|
|
138
|
+
if (result.suggestion)
|
|
139
|
+
msg += `\n\n💡 ${result.suggestion}`;
|
|
140
|
+
return msg;
|
|
141
|
+
}
|
|
142
|
+
const drift = validateConsultSections(result.output);
|
|
143
|
+
const driftLine = drift.missing.length > 0
|
|
144
|
+
? `⚠️ Format drift: missing sections [${drift.missing.join(', ')}]\n\n`
|
|
145
|
+
: '';
|
|
146
|
+
return `## ${name}\n**Execution Time:** ${(result.executionTimeMs / 1000).toFixed(1)}s\n\n${driftLine}${result.output}`;
|
|
147
|
+
}
|
|
148
|
+
export async function handleMultiConsult(input) {
|
|
149
|
+
// Sensitive-cwd guard runs before any adapter dispatch — direct MCP callers
|
|
150
|
+
// can't bypass the slash-command body's refusal by skipping CC.
|
|
151
|
+
const guard = checkSensitiveWorkingDir(input.workingDir);
|
|
152
|
+
if (!guard.ok) {
|
|
153
|
+
return {
|
|
154
|
+
content: [{
|
|
155
|
+
type: 'text',
|
|
156
|
+
text: `❌ Refused: ${guard.reason}\n\nInvoke /multi-consult from a project root, not a sensitive system path.`,
|
|
157
|
+
}],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const request = toConsultRequest(input);
|
|
161
|
+
const adapters = await getAvailableAdapters();
|
|
162
|
+
if (adapters.length === 0) {
|
|
163
|
+
return {
|
|
164
|
+
content: [{
|
|
165
|
+
type: 'text',
|
|
166
|
+
text: '❌ No AI CLIs found.\n\nInstall at least one:\n - Codex: npm install -g @openai/codex-cli\n - Gemini: npm install -g @google/gemini-cli',
|
|
167
|
+
}],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// Promise.allSettled — a rejected adapter must NOT collapse the whole call.
|
|
171
|
+
const settled = await Promise.allSettled(adapters.map((adapter) => adapter.runConsult(request).then((result) => ({ adapter, result }))));
|
|
172
|
+
const outcomes = settled.map((s, i) => {
|
|
173
|
+
if (s.status === 'fulfilled')
|
|
174
|
+
return s.value;
|
|
175
|
+
const message = s.reason instanceof Error ? s.reason.message : String(s.reason);
|
|
176
|
+
return {
|
|
177
|
+
adapter: adapters[i],
|
|
178
|
+
result: {
|
|
179
|
+
success: false,
|
|
180
|
+
error: { type: 'cli_error', message },
|
|
181
|
+
suggestion: 'Adapter rejected — see error above',
|
|
182
|
+
executionTimeMs: 0,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
const allFailed = outcomes.every((o) => !o.result.success);
|
|
187
|
+
const someFailed = outcomes.some((o) => !o.result.success);
|
|
188
|
+
const lines = [];
|
|
189
|
+
if (allFailed)
|
|
190
|
+
lines.push('## Multi-Consult ❌ All Failed\n');
|
|
191
|
+
else if (someFailed)
|
|
192
|
+
lines.push('## Multi-Consult ⚠️ Partial Success\n');
|
|
193
|
+
else
|
|
194
|
+
lines.push('## Multi-Consult ✓\n');
|
|
195
|
+
lines.push(`**Models:** ${adapters.map((a) => a.id).join(', ')}\n`);
|
|
196
|
+
for (const outcome of outcomes) {
|
|
197
|
+
lines.push(formatOutcome(outcome));
|
|
198
|
+
lines.push('');
|
|
199
|
+
}
|
|
200
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
201
|
+
}
|
|
202
|
+
// =============================================================================
|
|
203
|
+
// TOOL DEFINITION
|
|
204
|
+
// =============================================================================
|
|
205
|
+
export const MULTI_CONSULT_TOOL_DEFINITION = {
|
|
206
|
+
name: 'multi_consult',
|
|
207
|
+
description: "Use when asking the panel for guidance, recommendation, or approach (no prior CC-produced work to review). Input shape: 'question' only — no 'ccOutput'. For reviewing existing CC-produced work (plan, findings, code), use 'multi_review' (which requires 'ccOutput'). The discriminator is the shape of the input, not the user's phrasing.",
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: {
|
|
211
|
+
workingDir: { type: 'string', description: 'Working directory for the CLI to operate in' },
|
|
212
|
+
question: { type: 'string', description: 'CC-composed self-contained question for the panel' },
|
|
213
|
+
relevantFiles: { type: 'array', items: { type: 'string' }, description: 'CC-triaged file subset for code-grounded questions' },
|
|
214
|
+
customPrompt: { type: 'string', description: 'Free-form steering from $ARGUMENTS' },
|
|
215
|
+
reasoningEffort: { type: 'string', enum: ['high', 'xhigh'], description: "Codex reasoning effort (default: 'xhigh' for consult)" },
|
|
216
|
+
serviceTier: { type: 'string', enum: ['default', 'fast', 'flex'], description: "Codex service tier (default: 'fast')" },
|
|
217
|
+
},
|
|
218
|
+
required: ['workingDir', 'question'],
|
|
219
|
+
},
|
|
220
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Implementations — Review Tools
|
|
3
|
+
*
|
|
4
|
+
* Returns raw reviewer text to CC. No JSON parsing, no reformatting.
|
|
5
|
+
* CC handles interpretation and synthesis.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
export declare const ReviewInputSchema: z.ZodObject<{
|
|
9
|
+
workingDir: z.ZodString;
|
|
10
|
+
ccOutput: z.ZodString;
|
|
11
|
+
outputType: z.ZodEnum<["plan", "findings", "analysis", "proposal"]>;
|
|
12
|
+
analyzedFiles: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
13
|
+
focusAreas: z.ZodOptional<z.ZodArray<z.ZodEnum<["security", "performance", "architecture", "correctness", "maintainability", "scalability", "testing", "documentation"]>, "many">>;
|
|
14
|
+
customPrompt: z.ZodOptional<z.ZodString>;
|
|
15
|
+
reasoningEffort: z.ZodOptional<z.ZodEnum<["high", "xhigh"]>>;
|
|
16
|
+
serviceTier: z.ZodOptional<z.ZodEnum<["default", "fast", "flex"]>>;
|
|
17
|
+
}, "strip", z.ZodTypeAny, {
|
|
18
|
+
workingDir: string;
|
|
19
|
+
ccOutput: string;
|
|
20
|
+
outputType: "plan" | "findings" | "analysis" | "proposal";
|
|
21
|
+
reasoningEffort?: "high" | "xhigh" | undefined;
|
|
22
|
+
serviceTier?: "default" | "fast" | "flex" | undefined;
|
|
23
|
+
customPrompt?: string | undefined;
|
|
24
|
+
focusAreas?: ("security" | "performance" | "architecture" | "correctness" | "maintainability" | "scalability" | "testing" | "documentation")[] | undefined;
|
|
25
|
+
analyzedFiles?: string[] | undefined;
|
|
26
|
+
}, {
|
|
27
|
+
workingDir: string;
|
|
28
|
+
ccOutput: string;
|
|
29
|
+
outputType: "plan" | "findings" | "analysis" | "proposal";
|
|
30
|
+
reasoningEffort?: "high" | "xhigh" | undefined;
|
|
31
|
+
serviceTier?: "default" | "fast" | "flex" | undefined;
|
|
32
|
+
customPrompt?: string | undefined;
|
|
33
|
+
focusAreas?: ("security" | "performance" | "architecture" | "correctness" | "maintainability" | "scalability" | "testing" | "documentation")[] | undefined;
|
|
34
|
+
analyzedFiles?: string[] | undefined;
|
|
35
|
+
}>;
|
|
36
|
+
export type ReviewInput = z.infer<typeof ReviewInputSchema>;
|
|
37
|
+
export declare function handleMultiReview(input: ReviewInput): Promise<{
|
|
38
|
+
content: Array<{
|
|
39
|
+
type: 'text';
|
|
40
|
+
text: string;
|
|
41
|
+
}>;
|
|
42
|
+
}>;
|
|
43
|
+
export declare const TOOL_DEFINITIONS: {
|
|
44
|
+
multi_review: {
|
|
45
|
+
name: string;
|
|
46
|
+
description: string;
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: string;
|
|
49
|
+
properties: {
|
|
50
|
+
workingDir: {
|
|
51
|
+
type: string;
|
|
52
|
+
description: string;
|
|
53
|
+
};
|
|
54
|
+
ccOutput: {
|
|
55
|
+
type: string;
|
|
56
|
+
description: string;
|
|
57
|
+
};
|
|
58
|
+
outputType: {
|
|
59
|
+
type: string;
|
|
60
|
+
enum: string[];
|
|
61
|
+
description: string;
|
|
62
|
+
};
|
|
63
|
+
analyzedFiles: {
|
|
64
|
+
type: string;
|
|
65
|
+
items: {
|
|
66
|
+
type: string;
|
|
67
|
+
};
|
|
68
|
+
description: string;
|
|
69
|
+
};
|
|
70
|
+
focusAreas: {
|
|
71
|
+
type: string;
|
|
72
|
+
items: {
|
|
73
|
+
type: string;
|
|
74
|
+
enum: string[];
|
|
75
|
+
};
|
|
76
|
+
description: string;
|
|
77
|
+
};
|
|
78
|
+
customPrompt: {
|
|
79
|
+
type: string;
|
|
80
|
+
description: string;
|
|
81
|
+
};
|
|
82
|
+
serviceTier: {
|
|
83
|
+
type: string;
|
|
84
|
+
enum: string[];
|
|
85
|
+
description: string;
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
required: string[];
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Implementations — Review Tools
|
|
3
|
+
*
|
|
4
|
+
* Returns raw reviewer text to CC. No JSON parsing, no reformatting.
|
|
5
|
+
* CC handles interpretation and synthesis.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { getAvailableAdapters, } from '../adapters/index.js';
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// INPUT SCHEMAS
|
|
11
|
+
// =============================================================================
|
|
12
|
+
export const ReviewInputSchema = z.object({
|
|
13
|
+
workingDir: z.string().describe('Working directory for the CLI to operate in'),
|
|
14
|
+
ccOutput: z.string().describe("Claude Code's output to review (findings, plan, analysis)"),
|
|
15
|
+
outputType: z.enum(['plan', 'findings', 'analysis', 'proposal']).describe('Type of output being reviewed'),
|
|
16
|
+
analyzedFiles: z.array(z.string()).optional().describe('File paths that CC analyzed'),
|
|
17
|
+
focusAreas: z.array(z.enum([
|
|
18
|
+
'security', 'performance', 'architecture', 'correctness',
|
|
19
|
+
'maintainability', 'scalability', 'testing', 'documentation'
|
|
20
|
+
])).optional().describe('Areas to focus the review on'),
|
|
21
|
+
customPrompt: z.string().optional().describe('Custom instructions for the reviewer'),
|
|
22
|
+
reasoningEffort: z.enum(['high', 'xhigh']).optional().describe('Codex reasoning effort level (default: high, use xhigh for deeper analysis)'),
|
|
23
|
+
serviceTier: z.enum(['default', 'fast', 'flex']).optional().describe('Codex service tier (default when omitted: fast = priority processing, ~2x cost; flex = 50% cheaper/slower; default = API default tier)')
|
|
24
|
+
});
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// HELPERS
|
|
27
|
+
// =============================================================================
|
|
28
|
+
function toReviewRequest(input) {
|
|
29
|
+
return {
|
|
30
|
+
workingDir: input.workingDir,
|
|
31
|
+
ccOutput: input.ccOutput,
|
|
32
|
+
outputType: input.outputType,
|
|
33
|
+
analyzedFiles: input.analyzedFiles,
|
|
34
|
+
focusAreas: input.focusAreas,
|
|
35
|
+
customPrompt: input.customPrompt,
|
|
36
|
+
reasoningEffort: input.reasoningEffort,
|
|
37
|
+
serviceTier: input.serviceTier,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function formatResult(result, modelName) {
|
|
41
|
+
if (!result.success) {
|
|
42
|
+
const emoji = {
|
|
43
|
+
cli_not_found: '❌', timeout: '⏱️', rate_limit: '🚫',
|
|
44
|
+
auth_error: '🔐', cli_error: '❌',
|
|
45
|
+
};
|
|
46
|
+
let msg = `${emoji[result.error.type] || '❌'} **${result.error.type}**: ${result.error.message}`;
|
|
47
|
+
if (result.suggestion)
|
|
48
|
+
msg += `\n\n💡 ${result.suggestion}`;
|
|
49
|
+
return msg;
|
|
50
|
+
}
|
|
51
|
+
return `## ${modelName} Review\n\n**Execution Time:** ${(result.executionTimeMs / 1000).toFixed(1)}s\n\n${result.output}`;
|
|
52
|
+
}
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// MULTI-MODEL HANDLER
|
|
55
|
+
// =============================================================================
|
|
56
|
+
export async function handleMultiReview(input) {
|
|
57
|
+
const request = toReviewRequest(input);
|
|
58
|
+
const availableAdapters = await getAvailableAdapters();
|
|
59
|
+
if (availableAdapters.length === 0) {
|
|
60
|
+
return { content: [{ type: 'text', text: '❌ No AI CLIs found.\n\nInstall at least one:\n - Codex: npm install -g @openai/codex-cli\n - Gemini: npm install -g @google/gemini-cli' }] };
|
|
61
|
+
}
|
|
62
|
+
// Spawn 2 reviews per adapter: standard + adversarial (all in parallel)
|
|
63
|
+
// customPrompt steers the adversarial focus only — strip it from standard pass to avoid bias
|
|
64
|
+
const { customPrompt, ...standardRequest } = request;
|
|
65
|
+
const reviewPromises = availableAdapters.flatMap((adapter) => [
|
|
66
|
+
adapter.runReview({ ...standardRequest }).then(result => ({ adapter, result, mode: 'standard' })),
|
|
67
|
+
adapter.runReview({ ...request, reviewMode: 'adversarial' }).then(result => ({ adapter, result, mode: 'adversarial' })),
|
|
68
|
+
]);
|
|
69
|
+
const results = await Promise.all(reviewPromises);
|
|
70
|
+
const standardResults = results.filter(r => r.mode === 'standard');
|
|
71
|
+
const adversarialResults = results.filter(r => r.mode === 'adversarial');
|
|
72
|
+
const allFailed = results.every(r => !r.result.success);
|
|
73
|
+
const someFailed = results.some(r => !r.result.success);
|
|
74
|
+
const lines = [];
|
|
75
|
+
if (allFailed)
|
|
76
|
+
lines.push('## Multi-Model Review ❌ All Failed\n');
|
|
77
|
+
else if (someFailed)
|
|
78
|
+
lines.push('## Multi-Model Review ⚠️ Partial Success\n');
|
|
79
|
+
else
|
|
80
|
+
lines.push('## Multi-Model Review ✓\n');
|
|
81
|
+
lines.push(`**Models:** ${availableAdapters.map(a => a.id).join(', ')} (standard + adversarial)\n`);
|
|
82
|
+
// Standard section
|
|
83
|
+
lines.push('## Standard Review Findings\n');
|
|
84
|
+
for (const { adapter, result } of standardResults) {
|
|
85
|
+
lines.push(formatResult(result, adapter.getCapabilities().name));
|
|
86
|
+
lines.push('');
|
|
87
|
+
}
|
|
88
|
+
// Adversarial section
|
|
89
|
+
lines.push('## Challenge Review Findings\n');
|
|
90
|
+
for (const { adapter, result } of adversarialResults) {
|
|
91
|
+
lines.push(formatResult(result, `${adapter.getCapabilities().name} (Adversarial)`));
|
|
92
|
+
lines.push('');
|
|
93
|
+
}
|
|
94
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
95
|
+
}
|
|
96
|
+
// =============================================================================
|
|
97
|
+
// TOOL DEFINITIONS
|
|
98
|
+
// =============================================================================
|
|
99
|
+
export const TOOL_DEFINITIONS = {
|
|
100
|
+
multi_review: {
|
|
101
|
+
name: 'multi_review',
|
|
102
|
+
description: "Use when reviewing existing CC-produced work (plan, findings, code). Requires 'ccOutput' — CC's prior output to evaluate. Runs parallel standard AND adversarial reviews from all available models. For asking the panel an open question with no prior CC-produced work to review, use 'multi_consult' instead. The discriminator is the shape of the input, not the user's phrasing.",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
workingDir: { type: 'string', description: 'Working directory for the CLI to operate in' },
|
|
107
|
+
ccOutput: { type: 'string', description: "Claude Code's output to review (findings, plan, analysis)" },
|
|
108
|
+
outputType: { type: 'string', enum: ['plan', 'findings', 'analysis', 'proposal'], description: 'Type of output being reviewed' },
|
|
109
|
+
analyzedFiles: { type: 'array', items: { type: 'string' }, description: 'File paths that CC analyzed' },
|
|
110
|
+
focusAreas: { type: 'array', items: { type: 'string', enum: ['security', 'performance', 'architecture', 'correctness', 'maintainability', 'scalability', 'testing', 'documentation'] }, description: 'Areas to focus the review on' },
|
|
111
|
+
customPrompt: { type: 'string', description: 'Custom instructions for standard review + adversarial focus steering' },
|
|
112
|
+
serviceTier: { type: 'string', enum: ['default', 'fast', 'flex'], description: 'Codex service tier — only applies to Codex. Omit for fast default; fast = priority ~2x cost, flex = 50% cheaper/slower, default = API default tier.' }
|
|
113
|
+
},
|
|
114
|
+
required: ['workingDir', 'ccOutput', 'outputType']
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for AI Reviewer MCP Server
|
|
3
|
+
*/
|
|
4
|
+
export type OutputType = 'plan' | 'findings' | 'analysis' | 'proposal';
|
|
5
|
+
export type FocusArea = 'security' | 'performance' | 'architecture' | 'correctness' | 'maintainability' | 'scalability' | 'testing' | 'documentation';
|
|
6
|
+
export type CliType = 'codex' | 'gemini' | 'claude';
|
|
7
|
+
export type ReasoningEffort = 'high' | 'xhigh';
|
|
8
|
+
export type ServiceTier = 'default' | 'fast' | 'flex';
|
|
9
|
+
export interface FeedbackRequest {
|
|
10
|
+
workingDir: string;
|
|
11
|
+
ccOutput: string;
|
|
12
|
+
outputType: OutputType;
|
|
13
|
+
analyzedFiles?: string[];
|
|
14
|
+
focusAreas?: FocusArea[];
|
|
15
|
+
customPrompt?: string;
|
|
16
|
+
reasoningEffort?: ReasoningEffort;
|
|
17
|
+
serviceTier?: ServiceTier;
|
|
18
|
+
}
|
|
19
|
+
export interface FeedbackSuccess {
|
|
20
|
+
success: true;
|
|
21
|
+
feedback: string;
|
|
22
|
+
model: CliType;
|
|
23
|
+
}
|
|
24
|
+
export interface FeedbackFailure {
|
|
25
|
+
success: false;
|
|
26
|
+
error: FeedbackError;
|
|
27
|
+
suggestion?: string;
|
|
28
|
+
model: CliType;
|
|
29
|
+
}
|
|
30
|
+
export type FeedbackResult = FeedbackSuccess | FeedbackFailure;
|
|
31
|
+
export type FeedbackError = {
|
|
32
|
+
type: 'cli_not_found';
|
|
33
|
+
cli: CliType;
|
|
34
|
+
installCmd: string;
|
|
35
|
+
} | {
|
|
36
|
+
type: 'timeout';
|
|
37
|
+
cli: CliType;
|
|
38
|
+
durationMs: number;
|
|
39
|
+
} | {
|
|
40
|
+
type: 'rate_limit';
|
|
41
|
+
cli: CliType;
|
|
42
|
+
retryAfterMs?: number;
|
|
43
|
+
} | {
|
|
44
|
+
type: 'auth_error';
|
|
45
|
+
cli: CliType;
|
|
46
|
+
message: string;
|
|
47
|
+
} | {
|
|
48
|
+
type: 'invalid_response';
|
|
49
|
+
cli: CliType;
|
|
50
|
+
rawOutput: string;
|
|
51
|
+
} | {
|
|
52
|
+
type: 'cli_error';
|
|
53
|
+
cli: CliType;
|
|
54
|
+
exitCode: number;
|
|
55
|
+
stderr: string;
|
|
56
|
+
};
|
|
57
|
+
export interface MultiFeedbackResult {
|
|
58
|
+
successful: Array<{
|
|
59
|
+
model: CliType;
|
|
60
|
+
feedback: string;
|
|
61
|
+
}>;
|
|
62
|
+
failed: Array<{
|
|
63
|
+
model: CliType;
|
|
64
|
+
error: FeedbackError;
|
|
65
|
+
}>;
|
|
66
|
+
partialSuccess: boolean;
|
|
67
|
+
allFailed: boolean;
|
|
68
|
+
}
|
|
69
|
+
export interface CliStatus {
|
|
70
|
+
codex: boolean;
|
|
71
|
+
gemini: boolean;
|
|
72
|
+
claude: boolean;
|
|
73
|
+
}
|
|
74
|
+
export interface StructuredFeedback {
|
|
75
|
+
agreements: Array<{
|
|
76
|
+
finding: string;
|
|
77
|
+
reason: string;
|
|
78
|
+
}>;
|
|
79
|
+
disagreements: Array<{
|
|
80
|
+
finding: string;
|
|
81
|
+
reason: string;
|
|
82
|
+
correction: string;
|
|
83
|
+
}>;
|
|
84
|
+
additions: Array<{
|
|
85
|
+
finding: string;
|
|
86
|
+
location: string;
|
|
87
|
+
impact: string;
|
|
88
|
+
}>;
|
|
89
|
+
alternatives: Array<{
|
|
90
|
+
topic: string;
|
|
91
|
+
alternative: string;
|
|
92
|
+
tradeoffs: string;
|
|
93
|
+
}>;
|
|
94
|
+
riskAssessment: {
|
|
95
|
+
level: 'Low' | 'Medium' | 'High';
|
|
96
|
+
reason: string;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export interface ReviewerPersona {
|
|
100
|
+
name: string;
|
|
101
|
+
focus: string;
|
|
102
|
+
style: string;
|
|
103
|
+
}
|
|
104
|
+
export declare const REVIEWER_PERSONAS: Record<CliType, ReviewerPersona>;
|
|
105
|
+
export declare const FOCUS_AREA_DESCRIPTIONS: Record<FocusArea, string>;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for AI Reviewer MCP Server
|
|
3
|
+
*/
|
|
4
|
+
export const REVIEWER_PERSONAS = {
|
|
5
|
+
codex: {
|
|
6
|
+
name: 'Codex',
|
|
7
|
+
focus: 'correctness, edge cases, performance',
|
|
8
|
+
style: 'Apply pragmatic skepticism - verify before agreeing.'
|
|
9
|
+
},
|
|
10
|
+
gemini: {
|
|
11
|
+
name: 'Gemini',
|
|
12
|
+
focus: 'design patterns, scalability, tech debt',
|
|
13
|
+
style: 'Think holistically - consider broader context.'
|
|
14
|
+
},
|
|
15
|
+
claude: {
|
|
16
|
+
name: 'Claude',
|
|
17
|
+
focus: 'deep analysis, correctness, security, architecture',
|
|
18
|
+
style: 'Fresh perspective with clean context - challenge assumptions.'
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
// Focus area descriptions
|
|
22
|
+
export const FOCUS_AREA_DESCRIPTIONS = {
|
|
23
|
+
security: 'Vulnerabilities, auth, input validation',
|
|
24
|
+
performance: 'Speed, memory, efficiency',
|
|
25
|
+
architecture: 'Design patterns, structure, coupling',
|
|
26
|
+
correctness: 'Logic errors, edge cases, bugs',
|
|
27
|
+
maintainability: 'Code clarity, documentation, complexity',
|
|
28
|
+
scalability: 'Load handling, bottlenecks',
|
|
29
|
+
testing: 'Test coverage, test quality',
|
|
30
|
+
documentation: 'Comments, docs, API docs'
|
|
31
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@simonren/quorum",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "MCP server for Claude Code — a quorum of AI models (Codex, Gemini, Claude) for adversarial review and consultation, synthesized by Claude Code",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"quorum": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**/*",
|
|
12
|
+
"commands/**/*",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"start": "node dist/index.js",
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"claude",
|
|
27
|
+
"claude-code",
|
|
28
|
+
"codex",
|
|
29
|
+
"gemini",
|
|
30
|
+
"quorum",
|
|
31
|
+
"ai-review",
|
|
32
|
+
"ai-consult",
|
|
33
|
+
"code-review"
|
|
34
|
+
],
|
|
35
|
+
"author": "SimonRen",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/SimonRen/quorum.git"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/SimonRen/quorum#readme",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
47
|
+
"zod": "^3.22.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^20.0.0",
|
|
51
|
+
"typescript": "^5.0.0",
|
|
52
|
+
"vitest": "^2.0.0"
|
|
53
|
+
}
|
|
54
|
+
}
|