codex-review-mcp 1.4.0 → 2.0.1

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.
@@ -0,0 +1,334 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { gatherContext } from './gatherContext.js';
3
+ import { promises as fs } from 'node:fs';
4
+ vi.mock('node:fs', () => ({
5
+ promises: {
6
+ readFile: vi.fn(),
7
+ readdir: vi.fn(),
8
+ realpath: vi.fn(),
9
+ },
10
+ }));
11
+ describe('gatherContext', () => {
12
+ const originalCwd = process.cwd();
13
+ const mockCwd = '/test/workspace';
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ process.chdir = vi.fn(() => mockCwd);
17
+ vi.spyOn(process, 'cwd').mockReturnValue(mockCwd);
18
+ });
19
+ afterEach(() => {
20
+ vi.restoreAllMocks();
21
+ });
22
+ describe('Project Guidelines Priority', () => {
23
+ it('should prioritize .cursor/rules files first', async () => {
24
+ const mockReadFile = vi.mocked(fs.readFile);
25
+ const mockReaddir = vi.mocked(fs.readdir);
26
+ const mockRealpath = vi.mocked(fs.realpath);
27
+ // Mock .cursor/rules directory scanning
28
+ mockRealpath.mockResolvedValue('/test/workspace/.cursor/rules');
29
+ mockReaddir.mockResolvedValue([
30
+ { name: 'project.mdc', isFile: () => true, isDirectory: () => false },
31
+ { name: 'tsx.mdc', isFile: () => true, isDirectory: () => false },
32
+ ]);
33
+ mockReadFile.mockImplementation(async (path) => {
34
+ if (path.includes('.cursor/rules/project.mdc'))
35
+ return '# Project Rules\nStrict guidelines';
36
+ if (path.includes('.cursor/rules/tsx.mdc'))
37
+ return '# TSX Rules\nComponent patterns';
38
+ if (path.includes('CODE_REVIEW.md'))
39
+ return '# Code Review Guidelines';
40
+ return Promise.reject(new Error('ENOENT'));
41
+ });
42
+ const context = await gatherContext();
43
+ // Verify .cursor/rules files are included
44
+ expect(context).toContain('Project Rules');
45
+ expect(context).toContain('TSX Rules');
46
+ // Verify files are marked with their paths
47
+ expect(context).toMatch(/<!--.*\.cursor\/rules.*-->/);
48
+ });
49
+ it('should include CODE_REVIEW.md when present', async () => {
50
+ const mockReadFile = vi.mocked(fs.readFile);
51
+ const mockReaddir = vi.mocked(fs.readdir);
52
+ const mockRealpath = vi.mocked(fs.realpath);
53
+ mockRealpath.mockRejectedValue(new Error('ENOENT'));
54
+ mockReaddir.mockResolvedValue([]);
55
+ mockReadFile.mockImplementation(async (path) => {
56
+ if (path.includes('CODE_REVIEW.md')) {
57
+ return '# Code Review Standards\n\n## Conventions\n- Use TypeScript strict mode\n- Follow ESLint rules';
58
+ }
59
+ return Promise.reject(new Error('ENOENT'));
60
+ });
61
+ const context = await gatherContext();
62
+ expect(context).toContain('Code Review Standards');
63
+ expect(context).toContain('<!-- CODE_REVIEW.md -->');
64
+ });
65
+ it('should include CONTRIBUTING.md guidelines', async () => {
66
+ const mockReadFile = vi.mocked(fs.readFile);
67
+ const mockReaddir = vi.mocked(fs.readdir);
68
+ const mockRealpath = vi.mocked(fs.realpath);
69
+ mockRealpath.mockRejectedValue(new Error('ENOENT'));
70
+ mockReaddir.mockResolvedValue([]);
71
+ mockReadFile.mockImplementation(async (path) => {
72
+ if (path.includes('CONTRIBUTING.md')) {
73
+ return '# Contributing\n\n## Code Style\n- 2 spaces for indentation';
74
+ }
75
+ return Promise.reject(new Error('ENOENT'));
76
+ });
77
+ const context = await gatherContext();
78
+ expect(context).toContain('Contributing');
79
+ expect(context).toContain('<!-- CONTRIBUTING.md -->');
80
+ });
81
+ });
82
+ describe('Configuration Files', () => {
83
+ it('should include ESLint configuration', async () => {
84
+ const mockReadFile = vi.mocked(fs.readFile);
85
+ const mockReaddir = vi.mocked(fs.readdir);
86
+ const mockRealpath = vi.mocked(fs.realpath);
87
+ mockRealpath.mockRejectedValue(new Error('ENOENT'));
88
+ mockReaddir.mockResolvedValue([]);
89
+ mockReadFile.mockImplementation(async (path) => {
90
+ if (path.includes('.eslintrc.json')) {
91
+ return JSON.stringify({
92
+ extends: ['eslint:recommended'],
93
+ rules: { 'no-console': 'error' }
94
+ });
95
+ }
96
+ return Promise.reject(new Error('ENOENT'));
97
+ });
98
+ const context = await gatherContext();
99
+ expect(context).toContain('eslintrc');
100
+ expect(context).toContain('no-console');
101
+ });
102
+ it('should include TypeScript configuration', async () => {
103
+ const mockReadFile = vi.mocked(fs.readFile);
104
+ const mockReaddir = vi.mocked(fs.readdir);
105
+ const mockRealpath = vi.mocked(fs.realpath);
106
+ mockRealpath.mockRejectedValue(new Error('ENOENT'));
107
+ mockReaddir.mockResolvedValue([]);
108
+ mockReadFile.mockImplementation(async (path) => {
109
+ if (path.includes('tsconfig.json')) {
110
+ return JSON.stringify({
111
+ compilerOptions: { strict: true, noImplicitAny: true }
112
+ });
113
+ }
114
+ return Promise.reject(new Error('ENOENT'));
115
+ });
116
+ const context = await gatherContext();
117
+ expect(context).toContain('tsconfig');
118
+ expect(context).toContain('strict');
119
+ });
120
+ it('should include package.json for dependency info', async () => {
121
+ const mockReadFile = vi.mocked(fs.readFile);
122
+ const mockReaddir = vi.mocked(fs.readdir);
123
+ const mockRealpath = vi.mocked(fs.realpath);
124
+ mockRealpath.mockRejectedValue(new Error('ENOENT'));
125
+ mockReaddir.mockResolvedValue([]);
126
+ mockReadFile.mockImplementation(async (path) => {
127
+ if (path.includes('package.json')) {
128
+ return JSON.stringify({
129
+ name: 'test-project',
130
+ dependencies: { react: '^18.0.0', typescript: '^5.0.0' }
131
+ });
132
+ }
133
+ return Promise.reject(new Error('ENOENT'));
134
+ });
135
+ const context = await gatherContext();
136
+ expect(context).toContain('package.json');
137
+ expect(context).toContain('react');
138
+ });
139
+ });
140
+ describe('Size and Safety Limits', () => {
141
+ it('should cap individual files at 8000 characters', async () => {
142
+ const mockReadFile = vi.mocked(fs.readFile);
143
+ const mockReaddir = vi.mocked(fs.readdir);
144
+ const mockRealpath = vi.mocked(fs.realpath);
145
+ mockRealpath.mockRejectedValue(new Error('ENOENT'));
146
+ mockReaddir.mockResolvedValue([]);
147
+ const largeContent = 'x'.repeat(10000);
148
+ mockReadFile.mockImplementation(async (path) => {
149
+ if (path.includes('CODE_REVIEW.md'))
150
+ return largeContent;
151
+ return Promise.reject(new Error('ENOENT'));
152
+ });
153
+ const context = await gatherContext();
154
+ // Should be capped at 8000 + overhead for comments
155
+ const contentLength = context.length;
156
+ expect(contentLength).toBeLessThan(10000);
157
+ expect(context).toContain('CODE_REVIEW.md');
158
+ });
159
+ it('should cap total context at ~50000 characters', async () => {
160
+ const mockReadFile = vi.mocked(fs.readFile);
161
+ const mockReaddir = vi.mocked(fs.readdir);
162
+ const mockRealpath = vi.mocked(fs.realpath);
163
+ mockRealpath.mockRejectedValue(new Error('ENOENT'));
164
+ mockReaddir.mockResolvedValue([]);
165
+ // Create many files with max content
166
+ const largeContent = 'x'.repeat(8000);
167
+ mockReadFile.mockResolvedValue(largeContent);
168
+ const context = await gatherContext();
169
+ // Should stop before processing all files
170
+ expect(context.length).toBeLessThanOrEqual(60000); // Allow some overhead
171
+ });
172
+ it('should skip node_modules and other ignored directories', async () => {
173
+ const mockReadFile = vi.mocked(fs.readFile);
174
+ const mockReaddir = vi.mocked(fs.readdir);
175
+ const mockRealpath = vi.mocked(fs.realpath);
176
+ mockRealpath.mockResolvedValue('/test/workspace/.cursor/rules');
177
+ mockReaddir.mockResolvedValue([
178
+ { name: 'node_modules', isDirectory: () => true, isFile: () => false },
179
+ { name: '.git', isDirectory: () => true, isFile: () => false },
180
+ { name: 'project.mdc', isFile: () => true, isDirectory: () => false },
181
+ ]);
182
+ mockReadFile.mockImplementation(async (path) => {
183
+ if (path.includes('project.mdc'))
184
+ return '# Project Rules';
185
+ return Promise.reject(new Error('ENOENT'));
186
+ });
187
+ const context = await gatherContext();
188
+ // Should include project.mdc but not traverse node_modules or .git
189
+ expect(context).toContain('Project Rules');
190
+ expect(mockReaddir).toHaveBeenCalledTimes(1); // Only the .cursor/rules directory
191
+ });
192
+ it('should handle circular symlinks gracefully', async () => {
193
+ const mockReadFile = vi.mocked(fs.readFile);
194
+ const mockReaddir = vi.mocked(fs.readdir);
195
+ const mockRealpath = vi.mocked(fs.realpath);
196
+ // Simulate circular symlink by returning same realpath
197
+ mockRealpath.mockResolvedValue('/test/workspace/.cursor/rules');
198
+ mockReaddir.mockResolvedValue([
199
+ { name: 'link', isDirectory: () => true, isFile: () => false },
200
+ ]);
201
+ const context = await gatherContext();
202
+ // Should not hang or error, should handle gracefully
203
+ expect(context).toBeDefined();
204
+ });
205
+ });
206
+ describe('Error Handling', () => {
207
+ it('should continue when individual files are missing', async () => {
208
+ const mockReadFile = vi.mocked(fs.readFile);
209
+ const mockReaddir = vi.mocked(fs.readdir);
210
+ const mockRealpath = vi.mocked(fs.realpath);
211
+ mockRealpath.mockRejectedValue(new Error('ENOENT'));
212
+ mockReaddir.mockResolvedValue([]);
213
+ mockReadFile.mockImplementation(async (path) => {
214
+ if (path.includes('CODE_REVIEW.md'))
215
+ return '# Code Review';
216
+ if (path.includes('CONTRIBUTING.md'))
217
+ return '# Contributing';
218
+ // All other files throw ENOENT
219
+ return Promise.reject(new Error('ENOENT'));
220
+ });
221
+ const context = await gatherContext();
222
+ // Should still include the files that exist
223
+ expect(context).toContain('Code Review');
224
+ expect(context).toContain('Contributing');
225
+ });
226
+ it('should return empty string when no files exist', async () => {
227
+ const mockReadFile = vi.mocked(fs.readFile);
228
+ const mockReaddir = vi.mocked(fs.readdir);
229
+ const mockRealpath = vi.mocked(fs.realpath);
230
+ mockRealpath.mockRejectedValue(new Error('ENOENT'));
231
+ mockReaddir.mockResolvedValue([]);
232
+ mockReadFile.mockRejectedValue(new Error('ENOENT'));
233
+ const context = await gatherContext();
234
+ expect(context).toBe('');
235
+ });
236
+ it('should handle permission errors gracefully', async () => {
237
+ const mockReadFile = vi.mocked(fs.readFile);
238
+ const mockReaddir = vi.mocked(fs.readdir);
239
+ const mockRealpath = vi.mocked(fs.realpath);
240
+ mockRealpath.mockRejectedValue(new Error('ENOENT'));
241
+ mockReaddir.mockResolvedValue([]);
242
+ mockReadFile.mockImplementation(async (path) => {
243
+ if (path.includes('CODE_REVIEW.md'))
244
+ return '# Code Review';
245
+ if (path.includes('.eslintrc.json')) {
246
+ const error = new Error('EACCES: permission denied');
247
+ error.code = 'EACCES';
248
+ throw error;
249
+ }
250
+ return Promise.reject(new Error('ENOENT'));
251
+ });
252
+ const context = await gatherContext();
253
+ // Should still include accessible files
254
+ expect(context).toContain('Code Review');
255
+ // Should not throw an error
256
+ expect(context).toBeDefined();
257
+ });
258
+ });
259
+ describe('File Format Support', () => {
260
+ it('should scan .cursor/rules directory for .md and .mdc files', async () => {
261
+ const mockReadFile = vi.mocked(fs.readFile);
262
+ const mockReaddir = vi.mocked(fs.readdir);
263
+ const mockRealpath = vi.mocked(fs.realpath);
264
+ mockRealpath.mockResolvedValue('/test/workspace/.cursor/rules');
265
+ mockReaddir.mockResolvedValue([
266
+ { name: 'project.mdc', isFile: () => true, isDirectory: () => false },
267
+ { name: 'readme.md', isFile: () => true, isDirectory: () => false },
268
+ { name: 'config.json', isFile: () => true, isDirectory: () => false },
269
+ ]);
270
+ mockReadFile.mockImplementation(async (path) => {
271
+ if (path.includes('project.mdc'))
272
+ return '# Project';
273
+ if (path.includes('readme.md'))
274
+ return '# Readme';
275
+ if (path.includes('config.json'))
276
+ return '{}';
277
+ return Promise.reject(new Error('ENOENT'));
278
+ });
279
+ const context = await gatherContext();
280
+ expect(context).toContain('Project');
281
+ expect(context).toContain('Readme');
282
+ // Verify .json files from directory scan are not included (checking for the file marker)
283
+ expect(context).not.toMatch(/<!--.*\.cursor\/rules\/config\.json.*-->/);
284
+ // But ts config from the root candidate list should still be included
285
+ expect(context).toContain('tsconfig.json');
286
+ });
287
+ it('should support multiple ESLint config formats', async () => {
288
+ const mockReadFile = vi.mocked(fs.readFile);
289
+ const mockReaddir = vi.mocked(fs.readdir);
290
+ const mockRealpath = vi.mocked(fs.realpath);
291
+ mockRealpath.mockRejectedValue(new Error('ENOENT'));
292
+ mockReaddir.mockResolvedValue([]);
293
+ let callCount = 0;
294
+ mockReadFile.mockImplementation(async (path) => {
295
+ if (path.includes('.eslintrc.json')) {
296
+ callCount++;
297
+ return JSON.stringify({ rules: { 'no-console': 'error' } });
298
+ }
299
+ if (path.includes('.eslintrc.js') && callCount === 0) {
300
+ // Should only read first found ESLint config
301
+ throw new Error('Should not reach here');
302
+ }
303
+ return Promise.reject(new Error('ENOENT'));
304
+ });
305
+ const context = await gatherContext();
306
+ expect(context).toContain('eslintrc');
307
+ expect(callCount).toBe(1); // Should stop after finding first config
308
+ });
309
+ });
310
+ describe('Deduplication', () => {
311
+ it('should not process the same file twice', async () => {
312
+ const mockReadFile = vi.mocked(fs.readFile);
313
+ const mockReaddir = vi.mocked(fs.readdir);
314
+ const mockRealpath = vi.mocked(fs.realpath);
315
+ mockRealpath.mockResolvedValue('/test/workspace/.cursor/rules');
316
+ mockReaddir.mockResolvedValue([
317
+ { name: 'tsx.mdc', isFile: () => true, isDirectory: () => false },
318
+ ]);
319
+ let readCount = 0;
320
+ mockReadFile.mockImplementation(async (path) => {
321
+ if (path.includes('tsx.mdc') || path.includes('.cursor/rules/tsx.mdc')) {
322
+ readCount++;
323
+ return '# TSX Rules';
324
+ }
325
+ return Promise.reject(new Error('ENOENT'));
326
+ });
327
+ const context = await gatherContext();
328
+ // tsx.mdc appears in CANDIDATE_FILES twice (explicit and in directory scan)
329
+ // Should only be read once
330
+ expect(readCount).toBe(1);
331
+ expect(context).toContain('TSX Rules');
332
+ });
333
+ });
334
+ });
@@ -1,14 +1,52 @@
1
- import { Agent, run } from '@openai/agents';
1
+ import { Agent, run, tool } from '@openai/agents';
2
2
  import { debugLog } from '../util/debug.js';
3
- export async function invokeAgent({ prompt, maxTokens }) {
3
+ import { promises as fs } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { z } from 'zod';
6
+ export async function invokeAgent({ prompt, maxTokens, workspaceDir }) {
4
7
  const model = process.env.CODEX_MODEL || 'gpt-5-codex';
8
+ const cwd = workspaceDir || process.cwd();
9
+ // Tool: Read file from the codebase
10
+ const readFileTool = tool({
11
+ name: 'read_file',
12
+ description: 'Read the contents of a file from the codebase to examine existing patterns, utilities, or configuration. Use this to check for existing code before suggesting changes.',
13
+ parameters: z.object({
14
+ path: z.string().describe('Relative path to the file from the repository root'),
15
+ }),
16
+ execute: async (params) => {
17
+ try {
18
+ // Basic path sandboxing to keep reads under cwd
19
+ const requested = params.path.replace(/\\/g, '/');
20
+ if (requested.includes('..')) {
21
+ return 'Access denied: parent directory traversal is not allowed.';
22
+ }
23
+ const fullPath = join(cwd, requested);
24
+ if (!fullPath.startsWith(cwd)) {
25
+ return 'Access denied: path escapes workspace boundary.';
26
+ }
27
+ const content = await fs.readFile(fullPath, 'utf8');
28
+ const MAX_RETURN = 50_000; // cap very large files
29
+ const body = content.length > MAX_RETURN ? content.slice(0, MAX_RETURN) + '\n... [truncated]' : content;
30
+ await debugLog(`Agent read file: ${params.path} (${content.length} chars)`);
31
+ return `File: ${params.path}\n\n${body}`;
32
+ }
33
+ catch (error) {
34
+ const errorMsg = `Unable to read ${params.path}: ${error.message}`;
35
+ await debugLog(`Agent file read error: ${errorMsg}`);
36
+ return errorMsg;
37
+ }
38
+ },
39
+ });
5
40
  const agent = new Agent({
6
41
  name: 'Code Reviewer',
7
- instructions: 'You are a precise code-review agent. Follow the output contract exactly. Use minimal verbosity.',
42
+ instructions: 'You are a precise code-review agent. Follow the output contract exactly. Use minimal verbosity. When you need to examine existing code patterns, utilities, or configurations to provide accurate recommendations, use the read_file tool.',
8
43
  model,
44
+ tools: [readFileTool],
9
45
  });
10
46
  try {
11
- const result = await run(agent, prompt);
47
+ const result = await run(agent, prompt, {
48
+ maxTurns: 25, // Allow more turns for file reading (default is 10)
49
+ });
12
50
  const out = result.finalOutput ?? '';
13
51
  await debugLog(`Agent model=${model} outputLen=${out.length}`);
14
52
  return out;
@@ -5,6 +5,7 @@ import { run } from '@openai/agents';
5
5
  vi.mock('@openai/agents', () => ({
6
6
  Agent: vi.fn().mockImplementation((config) => config),
7
7
  run: vi.fn(),
8
+ tool: vi.fn().mockImplementation((config) => config),
8
9
  }));
9
10
  vi.mock('../util/debug', () => ({
10
11
  debugLog: vi.fn(),
@@ -74,4 +75,34 @@ describe('invokeAgent', () => {
74
75
  process.env.CODEX_MODEL = originalEnv;
75
76
  }
76
77
  });
78
+ it('should create agent with read_file tool for examining codebase', async () => {
79
+ vi.mocked(run).mockResolvedValue({
80
+ finalOutput: '# Review',
81
+ });
82
+ await invokeAgent({
83
+ prompt: 'Review this code',
84
+ workspaceDir: '/test/workspace'
85
+ });
86
+ const { Agent, tool } = await import('@openai/agents');
87
+ // Verify tool was created
88
+ expect(tool).toHaveBeenCalledWith(expect.objectContaining({
89
+ name: 'read_file',
90
+ description: expect.stringContaining('examine existing patterns'),
91
+ }));
92
+ // Verify agent was created with tools
93
+ expect(Agent).toHaveBeenCalledWith(expect.objectContaining({
94
+ tools: expect.arrayContaining([expect.anything()]),
95
+ }));
96
+ });
97
+ it('should pass workspaceDir to control file reading scope', async () => {
98
+ vi.mocked(run).mockResolvedValue({
99
+ finalOutput: '# Review',
100
+ });
101
+ const result = await invokeAgent({
102
+ prompt: 'Test',
103
+ workspaceDir: '/custom/workspace'
104
+ });
105
+ expect(result).toBe('# Review');
106
+ expect(run).toHaveBeenCalled();
107
+ });
77
108
  });
@@ -1,26 +1,63 @@
1
- import { collectDiff } from '../review/collectDiff.js';
2
1
  import { gatherContext } from '../review/gatherContext.js';
3
2
  import { buildPrompt } from '../review/buildPrompt.js';
4
3
  import { invokeAgent } from '../review/invokeAgent.js';
5
4
  import { formatOutput } from '../review/formatOutput.js';
6
5
  import { debugLog } from '../util/debug.js';
6
+ import { readFileSync } from 'node:fs';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { dirname, join } from 'node:path';
9
+ // Read version from package.json
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ const packageJson = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8'));
13
+ const VERSION = packageJson.version;
14
+ /**
15
+ * Perform code review with GPT-5 Codex
16
+ *
17
+ * The calling agent provides the content (diff or code) to review.
18
+ * We handle: context gathering, expert prompt building, GPT-5 invocation, formatting.
19
+ */
7
20
  export async function performCodeReview(input, onProgress) {
8
- await onProgress?.('Collecting diff…', 10, 100);
9
- const diffText = await collectDiff(input, input.workspaceDir);
10
- if (!diffText.trim()) {
11
- throw new Error('No changes to review. You are on the default branch with a clean working tree.');
21
+ // Validate input
22
+ if (!input.content || !input.content.trim()) {
23
+ throw new Error('content is required and cannot be empty. ' +
24
+ 'The calling agent should provide the diff or code to review.');
12
25
  }
13
- await onProgress?.('Gathering context…', 30, 100);
14
- const context = await gatherContext();
15
- await onProgress?.('Building prompt…', 45, 100);
16
- const prompt = buildPrompt({ diffText, context, focus: input.focus });
17
- // Count lines in the diff for progress message
18
- const lineCount = diffText.split('\n').length;
19
- const addedLines = diffText.split('\n').filter(line => line.startsWith('+')).length;
20
- const removedLines = diffText.split('\n').filter(line => line.startsWith('-')).length;
21
- await onProgress?.(`Calling GPT-5 Codex with ~${lineCount} lines (+${addedLines} -${removedLines})…`, 65, 100);
22
- const agentMd = await invokeAgent({ prompt, maxTokens: input.maxTokens });
26
+ await onProgress?.('Preparing review…', 10, 100);
27
+ // Gather context with smart precedence
28
+ const shouldSkip = input.skipContextGathering === true;
29
+ const usingCustom = Boolean(input.customContext?.trim());
30
+ await onProgress?.(usingCustom
31
+ ? 'Using provided project context…'
32
+ : shouldSkip
33
+ ? 'Skipping context gathering…'
34
+ : 'Gathering project context…', 30, 100);
35
+ const context = usingCustom
36
+ ? input.customContext
37
+ : shouldSkip
38
+ ? ''
39
+ : await gatherContext(input.workspaceDir);
40
+ // Build expert prompt
41
+ await onProgress?.('Building expert prompt…', 50, 100);
42
+ const isStaticReview = input.contentType === 'code';
43
+ const prompt = buildPrompt({
44
+ diffText: input.content,
45
+ context,
46
+ focus: input.focus,
47
+ version: VERSION,
48
+ isStaticReview
49
+ });
50
+ // Invoke GPT-5 Codex
51
+ const lineCount = input.content.split('\n').length;
52
+ const reviewType = isStaticReview ? 'code' : 'diff';
53
+ await onProgress?.(`Calling GPT-5 Codex with ${reviewType} (~${lineCount} lines)…`, 70, 100);
54
+ const agentMd = await invokeAgent({
55
+ prompt,
56
+ maxTokens: input.maxTokens,
57
+ workspaceDir: input.workspaceDir || process.cwd()
58
+ });
23
59
  await debugLog(`Review produced ${agentMd.length} chars`);
60
+ // Format output
24
61
  await onProgress?.('Formatting output…', 90, 100);
25
62
  const out = formatOutput(agentMd);
26
63
  await onProgress?.('Done', 100, 100);