@simonren/quorum 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/commands/multi-consult.md +109 -0
  4. package/commands/multi-review.md +139 -0
  5. package/dist/adapters/base.d.ts +120 -0
  6. package/dist/adapters/base.js +98 -0
  7. package/dist/adapters/claude.d.ts +25 -0
  8. package/dist/adapters/claude.js +217 -0
  9. package/dist/adapters/codex.d.ts +20 -0
  10. package/dist/adapters/codex.js +227 -0
  11. package/dist/adapters/gemini.d.ts +20 -0
  12. package/dist/adapters/gemini.js +197 -0
  13. package/dist/adapters/index.d.ts +12 -0
  14. package/dist/adapters/index.js +15 -0
  15. package/dist/cli/check.d.ts +20 -0
  16. package/dist/cli/check.js +78 -0
  17. package/dist/cli/codex.d.ts +11 -0
  18. package/dist/cli/codex.js +255 -0
  19. package/dist/cli/gemini.d.ts +12 -0
  20. package/dist/cli/gemini.js +253 -0
  21. package/dist/commands.d.ts +28 -0
  22. package/dist/commands.js +105 -0
  23. package/dist/config.d.ts +244 -0
  24. package/dist/config.js +179 -0
  25. package/dist/consult-prompt.d.ts +10 -0
  26. package/dist/consult-prompt.js +72 -0
  27. package/dist/context.d.ts +1538 -0
  28. package/dist/context.js +383 -0
  29. package/dist/decoders/claude.d.ts +53 -0
  30. package/dist/decoders/claude.js +106 -0
  31. package/dist/decoders/codex.d.ts +71 -0
  32. package/dist/decoders/codex.js +145 -0
  33. package/dist/decoders/gemini.d.ts +33 -0
  34. package/dist/decoders/gemini.js +58 -0
  35. package/dist/decoders/index.d.ts +6 -0
  36. package/dist/decoders/index.js +3 -0
  37. package/dist/errors.d.ts +46 -0
  38. package/dist/errors.js +192 -0
  39. package/dist/executor.d.ts +103 -0
  40. package/dist/executor.js +244 -0
  41. package/dist/handoff.d.ts +270 -0
  42. package/dist/handoff.js +599 -0
  43. package/dist/index.d.ts +18 -0
  44. package/dist/index.js +134 -0
  45. package/dist/pipeline.d.ts +135 -0
  46. package/dist/pipeline.js +462 -0
  47. package/dist/prompt-v2.d.ts +38 -0
  48. package/dist/prompt-v2.js +391 -0
  49. package/dist/prompt.d.ts +71 -0
  50. package/dist/prompt.js +309 -0
  51. package/dist/schema.d.ts +660 -0
  52. package/dist/schema.js +536 -0
  53. package/dist/tools/consult.d.ts +104 -0
  54. package/dist/tools/consult.js +220 -0
  55. package/dist/tools/feedback.d.ts +91 -0
  56. package/dist/tools/feedback.js +117 -0
  57. package/dist/types.d.ts +105 -0
  58. package/dist/types.js +31 -0
  59. package/package.json +54 -0
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Claude CLI Adapter
3
+ *
4
+ * Implements the ReviewerAdapter interface for Anthropic's Claude CLI.
5
+ * Spawns a FRESH Claude Code instance with zero session context.
6
+ * Returns raw text — CC handles interpretation.
7
+ *
8
+ * Read-only enforcement (defense-in-depth):
9
+ * 1. --permission-mode plan (CLI-level read-only)
10
+ * 2. --disallowed-tools (write tools explicitly blocked)
11
+ * 3. Handoff prompt (explicit READ-ONLY instruction)
12
+ */
13
+ import { spawn } from 'child_process';
14
+ import { existsSync } from 'fs';
15
+ import { registerAdapter, } from './base.js';
16
+ import { CliExecutor } from '../executor.js';
17
+ import { ClaudeEventDecoder } from '../decoders/index.js';
18
+ import { buildSimpleHandoff, buildHandoffPrompt, buildAdversarialHandoffPrompt, selectRole, } from '../handoff.js';
19
+ import { buildConsultPrompt } from '../consult-prompt.js';
20
+ import { getConfig } from '../config.js';
21
+ // Write tools explicitly blocked as defense-in-depth
22
+ const DISALLOWED_TOOLS = 'Edit Write NotebookEdit';
23
+ // =============================================================================
24
+ // CLAUDE ADAPTER
25
+ // =============================================================================
26
+ export class ClaudeAdapter {
27
+ id = 'claude';
28
+ getCapabilities() {
29
+ return {
30
+ name: 'Claude',
31
+ description: 'Anthropic Claude (Opus) - fresh instance with clean context, excels at deep analysis across all dimensions',
32
+ strengths: ['correctness', 'security', 'architecture', 'maintainability'],
33
+ weaknesses: [],
34
+ hasFilesystemAccess: true,
35
+ supportsStructuredOutput: false,
36
+ maxContextTokens: 200000,
37
+ reasoningLevels: undefined,
38
+ };
39
+ }
40
+ async isAvailable() {
41
+ return new Promise((resolve) => {
42
+ let settled = false;
43
+ const done = (result) => { if (!settled) {
44
+ settled = true;
45
+ clearTimeout(timer);
46
+ resolve(result);
47
+ } };
48
+ const proc = spawn('claude', ['--version'], {
49
+ stdio: ['ignore', 'pipe', 'pipe'],
50
+ });
51
+ proc.on('close', (code) => done(code === 0));
52
+ proc.on('error', () => done(false));
53
+ const timer = setTimeout(() => { proc.kill(); done(false); }, 5000);
54
+ });
55
+ }
56
+ async runReview(request) {
57
+ const startTime = Date.now();
58
+ if (!existsSync(request.workingDir)) {
59
+ return {
60
+ success: false,
61
+ error: { type: 'cli_error', message: `Working directory does not exist: ${request.workingDir}` },
62
+ suggestion: 'Check that the working directory path is correct',
63
+ executionTimeMs: Date.now() - startTime,
64
+ };
65
+ }
66
+ try {
67
+ const handoff = buildSimpleHandoff(request.workingDir, request.ccOutput, request.analyzedFiles, request.focusAreas, request.customPrompt);
68
+ const prompt = request.reviewMode === 'adversarial'
69
+ ? buildAdversarialHandoffPrompt({ handoff })
70
+ : buildHandoffPrompt({ handoff, role: selectRole(request.focusAreas) });
71
+ const result = await this.runCli(prompt, request.workingDir);
72
+ if (result.exitCode !== 0) {
73
+ const error = this.categorizeError(result.stderr);
74
+ return { success: false, error, suggestion: this.getSuggestion(error), executionTimeMs: Date.now() - startTime };
75
+ }
76
+ if (!result.stdout.trim()) {
77
+ return {
78
+ success: false,
79
+ error: { type: 'cli_error', message: 'Claude returned empty response' },
80
+ suggestion: 'Try again or use /multi-review instead',
81
+ executionTimeMs: Date.now() - startTime,
82
+ };
83
+ }
84
+ return { success: true, output: result.stdout, executionTimeMs: Date.now() - startTime };
85
+ }
86
+ catch (error) {
87
+ return this.handleException(error, startTime);
88
+ }
89
+ }
90
+ async runCli(prompt, workingDir) {
91
+ const cfg = getConfig().claude;
92
+ const args = [
93
+ '-p', // Non-interactive, print and exit
94
+ '--model', cfg.model, // Model from config (default: opus)
95
+ '--setting-sources', '', // Skip hooks, plugins, CLAUDE.md (preserves OAuth auth; --bare kills keychain)
96
+ '--permission-mode', 'plan', // Read-only enforcement (layer 1)
97
+ '--verbose', // Required for stream-json
98
+ '--output-format', 'stream-json', // Structured streaming events
99
+ '--no-session-persistence', // Ephemeral — no trace
100
+ '--disable-slash-commands', // No skills — minimal startup
101
+ '--disallowed-tools', DISALLOWED_TOOLS, // Block write tools (layer 2)
102
+ '-', // Read prompt from stdin
103
+ ];
104
+ const decoder = new ClaudeEventDecoder();
105
+ const cliStartTime = Date.now();
106
+ console.error('[claude] Running Opus review...');
107
+ decoder.onProgress = (eventType, detail) => {
108
+ const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
109
+ const detailStr = detail ? ` — ${detail}` : '';
110
+ console.error(`[claude] ${eventType}${detailStr} (${elapsed}s)`);
111
+ };
112
+ const executor = new CliExecutor({
113
+ command: 'claude',
114
+ args,
115
+ cwd: workingDir,
116
+ stdin: prompt,
117
+ inactivityTimeoutMs: cfg.inactivityTimeoutMs,
118
+ maxTimeoutMs: cfg.maxTimeoutMs,
119
+ maxBufferSize: cfg.maxBufferSize,
120
+ onLine: (line) => {
121
+ decoder.processLine(line);
122
+ },
123
+ });
124
+ const result = await executor.run();
125
+ const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
126
+ console.error(`[claude] ✓ complete (${elapsed}s)`);
127
+ // Check for errors captured from stream events
128
+ const decoderError = decoder.getError();
129
+ if (decoderError) {
130
+ const combined = result.stderr ? `${decoderError}\n\nCLI stderr: ${result.stderr}` : decoderError;
131
+ return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
132
+ }
133
+ const finalResponse = decoder.getFinalResponse();
134
+ if (!finalResponse && decoder.hasNoOutput()) {
135
+ const combined = result.stderr ? `No output from Claude\n\nCLI stderr: ${result.stderr}` : 'No output from Claude';
136
+ return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
137
+ }
138
+ if (!finalResponse) {
139
+ const combined = result.stderr ? `No result event from Claude\n\nCLI stderr: ${result.stderr}` : 'No result event from Claude';
140
+ return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
141
+ }
142
+ return {
143
+ stdout: finalResponse,
144
+ stderr: result.stderr,
145
+ exitCode: result.exitCode,
146
+ truncated: result.truncated,
147
+ };
148
+ }
149
+ handleException(error, startTime) {
150
+ const err = error;
151
+ if (err.code === 'ENOENT') {
152
+ return { success: false, error: { type: 'cli_not_found', message: 'Claude CLI not found' },
153
+ suggestion: 'Install Claude Code: https://docs.anthropic.com/en/docs/claude-code', executionTimeMs: Date.now() - startTime };
154
+ }
155
+ if (err.message === 'TIMEOUT') {
156
+ return { success: false, error: { type: 'timeout', message: 'Claude timed out — no events received' },
157
+ suggestion: 'Try a smaller scope or use /multi-review', executionTimeMs: Date.now() - startTime };
158
+ }
159
+ if (err.message === 'MAX_TIMEOUT') {
160
+ return { success: false, error: { type: 'timeout', message: 'Task exceeded 60 minute maximum' },
161
+ suggestion: 'Try a smaller scope', executionTimeMs: Date.now() - startTime };
162
+ }
163
+ return { success: false, error: { type: 'cli_error', message: err.message }, executionTimeMs: Date.now() - startTime };
164
+ }
165
+ categorizeError(stderr) {
166
+ const lower = stderr.toLowerCase();
167
+ if (lower.includes('rate limit') || lower.includes('rate_limit') || lower.includes('quota')) {
168
+ return { type: 'rate_limit', message: `Claude rate limit: ${stderr.slice(0, 500)}` };
169
+ }
170
+ if (lower.includes('unauthorized') || lower.includes('authentication') || lower.includes('not logged in') || lower.includes('api key') || stderr.includes('401') || stderr.includes('403')) {
171
+ return { type: 'auth_error', message: `Authentication failed: ${stderr.slice(0, 500)}`, details: { stderr } };
172
+ }
173
+ return { type: 'cli_error', message: stderr.slice(0, 500) || 'Unknown error' };
174
+ }
175
+ getSuggestion(error) {
176
+ switch (error.type) {
177
+ case 'rate_limit': return 'Wait and retry, or use /multi-review instead';
178
+ case 'auth_error': return 'Run `claude auth` to authenticate';
179
+ case 'cli_not_found': return 'Install Claude Code: https://docs.anthropic.com/en/docs/claude-code';
180
+ default: return 'Check the error message and try again';
181
+ }
182
+ }
183
+ async runConsult(request) {
184
+ const startTime = Date.now();
185
+ if (!existsSync(request.workingDir)) {
186
+ return {
187
+ success: false,
188
+ error: { type: 'cli_error', message: `Working directory does not exist: ${request.workingDir}` },
189
+ suggestion: 'Check that the working directory path is correct',
190
+ executionTimeMs: Date.now() - startTime,
191
+ };
192
+ }
193
+ try {
194
+ const prompt = buildConsultPrompt(request);
195
+ const result = await this.runCli(prompt, request.workingDir);
196
+ if (result.exitCode !== 0) {
197
+ const error = this.categorizeError(result.stderr);
198
+ return { success: false, error, suggestion: this.getSuggestion(error), executionTimeMs: Date.now() - startTime };
199
+ }
200
+ if (!result.stdout.trim()) {
201
+ return {
202
+ success: false,
203
+ error: { type: 'cli_error', message: 'Claude returned empty response' },
204
+ suggestion: 'Try again or use /multi-review instead',
205
+ executionTimeMs: Date.now() - startTime,
206
+ };
207
+ }
208
+ return { success: true, output: result.stdout, executionTimeMs: Date.now() - startTime };
209
+ }
210
+ catch (error) {
211
+ return this.handleException(error, startTime);
212
+ }
213
+ }
214
+ }
215
+ // Register the adapter
216
+ registerAdapter(new ClaudeAdapter());
217
+ export const claudeAdapter = new ClaudeAdapter();
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Codex CLI Adapter
3
+ *
4
+ * Implements the ReviewerAdapter interface for OpenAI's Codex CLI.
5
+ * Returns raw text — no JSON parsing or schema enforcement.
6
+ * CC handles interpretation of the reviewer's response.
7
+ */
8
+ import { ReviewerAdapter, ReviewerCapabilities, ReviewRequest, ReviewResult, ConsultRequest, ConsultResult } from './base.js';
9
+ export declare class CodexAdapter implements ReviewerAdapter {
10
+ readonly id = "codex";
11
+ getCapabilities(): ReviewerCapabilities;
12
+ isAvailable(): Promise<boolean>;
13
+ runReview(request: ReviewRequest): Promise<ReviewResult>;
14
+ private runCli;
15
+ private handleException;
16
+ private categorizeError;
17
+ private getSuggestion;
18
+ runConsult(request: ConsultRequest): Promise<ConsultResult>;
19
+ }
20
+ export declare const codexAdapter: CodexAdapter;
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Codex CLI Adapter
3
+ *
4
+ * Implements the ReviewerAdapter interface for OpenAI's Codex CLI.
5
+ * Returns raw text — no JSON parsing or schema enforcement.
6
+ * CC handles interpretation of the reviewer's response.
7
+ */
8
+ import { spawn } from 'child_process';
9
+ import { existsSync } from 'fs';
10
+ import { registerAdapter, } from './base.js';
11
+ import { CliExecutor } from '../executor.js';
12
+ import { CodexEventDecoder } from '../decoders/index.js';
13
+ import { buildSimpleHandoff, buildHandoffPrompt, buildAdversarialHandoffPrompt, selectRole, } from '../handoff.js';
14
+ import { buildConsultPrompt } from '../consult-prompt.js';
15
+ import { getConfig } from '../config.js';
16
+ // =============================================================================
17
+ // CODEX ADAPTER
18
+ // =============================================================================
19
+ export class CodexAdapter {
20
+ id = 'codex';
21
+ getCapabilities() {
22
+ return {
23
+ name: 'Codex',
24
+ description: 'OpenAI Codex - excels at correctness analysis, edge cases, and performance optimization',
25
+ strengths: ['correctness', 'performance', 'security', 'testing'],
26
+ weaknesses: ['documentation'],
27
+ hasFilesystemAccess: true,
28
+ supportsStructuredOutput: false,
29
+ maxContextTokens: 128000,
30
+ reasoningLevels: ['high', 'xhigh'],
31
+ };
32
+ }
33
+ async isAvailable() {
34
+ return new Promise((resolve) => {
35
+ let settled = false;
36
+ const done = (result) => { if (!settled) {
37
+ settled = true;
38
+ clearTimeout(timer);
39
+ resolve(result);
40
+ } };
41
+ const proc = spawn('codex', ['--version'], {
42
+ stdio: ['ignore', 'pipe', 'pipe'],
43
+ });
44
+ proc.on('close', (code) => done(code === 0));
45
+ proc.on('error', () => done(false));
46
+ const timer = setTimeout(() => { proc.kill(); done(false); }, 5000);
47
+ });
48
+ }
49
+ async runReview(request) {
50
+ const startTime = Date.now();
51
+ if (!existsSync(request.workingDir)) {
52
+ return {
53
+ success: false,
54
+ error: { type: 'cli_error', message: `Working directory does not exist: ${request.workingDir}` },
55
+ suggestion: 'Check that the working directory path is correct',
56
+ executionTimeMs: Date.now() - startTime,
57
+ };
58
+ }
59
+ try {
60
+ const handoff = buildSimpleHandoff(request.workingDir, request.ccOutput, request.analyzedFiles, request.focusAreas, request.customPrompt);
61
+ const prompt = request.reviewMode === 'adversarial'
62
+ ? buildAdversarialHandoffPrompt({ handoff })
63
+ : buildHandoffPrompt({ handoff, role: selectRole(request.focusAreas) });
64
+ const cfg = getConfig().codex;
65
+ const result = await this.runCli(prompt, request.workingDir, request.reasoningEffort ?? cfg.reasoningEffort, request.serviceTier);
66
+ if (result.exitCode !== 0) {
67
+ const error = this.categorizeError(result.stderr);
68
+ return { success: false, error, suggestion: this.getSuggestion(error), executionTimeMs: Date.now() - startTime };
69
+ }
70
+ if (!result.stdout.trim()) {
71
+ return {
72
+ success: false,
73
+ error: { type: 'cli_error', message: 'Codex returned empty response' },
74
+ suggestion: 'Try again or use /multi-review instead',
75
+ executionTimeMs: Date.now() - startTime,
76
+ };
77
+ }
78
+ return { success: true, output: result.stdout, executionTimeMs: Date.now() - startTime };
79
+ }
80
+ catch (error) {
81
+ return this.handleException(error, startTime);
82
+ }
83
+ }
84
+ async runCli(prompt, workingDir, reasoningEffort, serviceTier) {
85
+ const cfg = getConfig().codex;
86
+ const args = [
87
+ 'exec',
88
+ '--json', // JSONL streaming events
89
+ '-m', cfg.model,
90
+ '-c', `model_reasoning_effort=${reasoningEffort}`,
91
+ '-c', 'model_reasoning_summary_format=experimental',
92
+ '--full-auto',
93
+ '--sandbox', 'read-only',
94
+ '--skip-git-repo-check',
95
+ '-C', workingDir,
96
+ '-', // Read prompt from stdin
97
+ ];
98
+ // Caller-supplied serviceTier overrides config. Explicit 'default' is an
99
+ // opt-out and emits no flag (uses Codex API default).
100
+ const effectiveTier = serviceTier ?? cfg.serviceTier;
101
+ if (effectiveTier !== 'default') {
102
+ args.push('-c', `service_tier=${effectiveTier}`);
103
+ }
104
+ const decoder = new CodexEventDecoder();
105
+ const cliStartTime = Date.now();
106
+ const tierLabel = effectiveTier !== 'default' ? ` [${effectiveTier}]` : '';
107
+ console.error(`[codex] Running with ${reasoningEffort} reasoning${tierLabel}...`);
108
+ decoder.onProgress = (eventType, detail) => {
109
+ const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
110
+ const detailStr = detail ? ` — ${detail}` : '';
111
+ console.error(`[codex] ${eventType}${detailStr} (${elapsed}s)`);
112
+ };
113
+ const executor = new CliExecutor({
114
+ command: 'codex',
115
+ args,
116
+ cwd: workingDir,
117
+ stdin: prompt,
118
+ inactivityTimeoutMs: cfg.inactivityTimeoutMs[reasoningEffort] ?? cfg.inactivityTimeoutMs.high,
119
+ maxTimeoutMs: cfg.maxTimeoutMs,
120
+ maxBufferSize: cfg.maxBufferSize,
121
+ onLine: (line) => {
122
+ decoder.processLine(line);
123
+ },
124
+ });
125
+ const result = await executor.run();
126
+ const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
127
+ console.error(`[codex] ✓ complete (${elapsed}s)`);
128
+ // Check for errors captured from JSONL events
129
+ const decoderError = decoder.getError();
130
+ if (decoderError) {
131
+ const combined = result.stderr ? `${decoderError}\n\nCLI stderr: ${result.stderr}` : decoderError;
132
+ return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
133
+ }
134
+ const finalResponse = decoder.getFinalResponse();
135
+ if (!finalResponse && decoder.hasNoOutput()) {
136
+ const combined = result.stderr ? `No output from Codex\n\nCLI stderr: ${result.stderr}` : 'No output from Codex';
137
+ return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
138
+ }
139
+ if (!finalResponse) {
140
+ const combined = result.stderr ? `No result event from Codex\n\nCLI stderr: ${result.stderr}` : 'No result event from Codex';
141
+ return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
142
+ }
143
+ return {
144
+ stdout: finalResponse,
145
+ stderr: result.stderr,
146
+ exitCode: result.exitCode,
147
+ truncated: result.truncated,
148
+ };
149
+ }
150
+ handleException(error, startTime) {
151
+ const err = error;
152
+ if (err.code === 'ENOENT') {
153
+ return { success: false, error: { type: 'cli_not_found', message: 'Codex CLI not found' },
154
+ suggestion: 'Install with: npm install -g @openai/codex-cli', executionTimeMs: Date.now() - startTime };
155
+ }
156
+ if (err.message === 'TIMEOUT') {
157
+ return { success: false, error: { type: 'timeout', message: 'Codex timed out — no events received' },
158
+ suggestion: 'Try a smaller scope or use /multi-review', executionTimeMs: Date.now() - startTime };
159
+ }
160
+ if (err.message === 'MAX_TIMEOUT') {
161
+ return { success: false, error: { type: 'timeout', message: 'Task exceeded 60 minute maximum' },
162
+ suggestion: 'Try a smaller scope', executionTimeMs: Date.now() - startTime };
163
+ }
164
+ return { success: false, error: { type: 'cli_error', message: err.message }, executionTimeMs: Date.now() - startTime };
165
+ }
166
+ categorizeError(stderr) {
167
+ const lower = stderr.toLowerCase();
168
+ if (lower.includes('rate limit') || lower.includes('rate_limit')) {
169
+ return { type: 'rate_limit', message: `Codex rate limit: ${stderr.slice(0, 500)}` };
170
+ }
171
+ if (lower.includes('unauthorized') || lower.includes('authentication') || stderr.includes('401') || stderr.includes('403')) {
172
+ return { type: 'auth_error', message: `Authentication failed: ${stderr.slice(0, 500)}`, details: { stderr } };
173
+ }
174
+ if (lower.includes('invalid_json_schema') || lower.includes('invalid_request_error')) {
175
+ return { type: 'cli_error', message: `API error: ${stderr.slice(0, 500)}` };
176
+ }
177
+ return { type: 'cli_error', message: stderr.slice(0, 500) || 'Unknown error' };
178
+ }
179
+ getSuggestion(error) {
180
+ switch (error.type) {
181
+ case 'rate_limit': return 'Wait and retry, or use /multi-review instead';
182
+ case 'auth_error': return 'Run `codex login` to authenticate';
183
+ case 'cli_not_found': return 'Install with: npm install -g @openai/codex-cli';
184
+ default: return 'Check the error message and try again';
185
+ }
186
+ }
187
+ async runConsult(request) {
188
+ const startTime = Date.now();
189
+ if (!existsSync(request.workingDir)) {
190
+ return {
191
+ success: false,
192
+ error: { type: 'cli_error', message: `Working directory does not exist: ${request.workingDir}` },
193
+ suggestion: 'Check that the working directory path is correct',
194
+ executionTimeMs: Date.now() - startTime,
195
+ };
196
+ }
197
+ try {
198
+ const prompt = buildConsultPrompt(request);
199
+ // Consult-specific defaults live in config (Zod defaults to xhigh + fast).
200
+ // Request value > config value > Zod default. Users who want to cap cost
201
+ // can set codex.consultServiceTier: "flex" without touching review.
202
+ const cfg = getConfig().codex;
203
+ const reasoningEffort = request.reasoningEffort ?? cfg.consultReasoningEffort;
204
+ const serviceTier = request.serviceTier ?? cfg.consultServiceTier;
205
+ const result = await this.runCli(prompt, request.workingDir, reasoningEffort, serviceTier);
206
+ if (result.exitCode !== 0) {
207
+ const error = this.categorizeError(result.stderr);
208
+ return { success: false, error, suggestion: this.getSuggestion(error), executionTimeMs: Date.now() - startTime };
209
+ }
210
+ if (!result.stdout.trim()) {
211
+ return {
212
+ success: false,
213
+ error: { type: 'cli_error', message: 'Codex returned empty response' },
214
+ suggestion: 'Try again or use /multi-review instead',
215
+ executionTimeMs: Date.now() - startTime,
216
+ };
217
+ }
218
+ return { success: true, output: result.stdout, executionTimeMs: Date.now() - startTime };
219
+ }
220
+ catch (error) {
221
+ return this.handleException(error, startTime);
222
+ }
223
+ }
224
+ }
225
+ // Register the adapter
226
+ registerAdapter(new CodexAdapter());
227
+ export const codexAdapter = new CodexAdapter();
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Gemini CLI Adapter
3
+ *
4
+ * Implements the ReviewerAdapter interface for Google's Gemini CLI.
5
+ * Returns raw text — no JSON parsing or schema enforcement.
6
+ * CC handles interpretation of the reviewer's response.
7
+ */
8
+ import { ReviewerAdapter, ReviewerCapabilities, ReviewRequest, ReviewResult, ConsultRequest, ConsultResult } from './base.js';
9
+ export declare class GeminiAdapter implements ReviewerAdapter {
10
+ readonly id = "gemini";
11
+ getCapabilities(): ReviewerCapabilities;
12
+ isAvailable(): Promise<boolean>;
13
+ runReview(request: ReviewRequest): Promise<ReviewResult>;
14
+ private runCli;
15
+ private handleException;
16
+ private categorizeError;
17
+ private getSuggestion;
18
+ runConsult(request: ConsultRequest): Promise<ConsultResult>;
19
+ }
20
+ export declare const geminiAdapter: GeminiAdapter;