@in-the-loop-labs/pair-review 3.4.1 → 3.5.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.
@@ -34,6 +34,29 @@ const BIN_DIR = path.join(__dirname, '..', '..', 'bin');
34
34
  * Deprecated (April 2026): gpt-5.1-codex-mini, gpt-5.1-codex-max, gpt-5.1-codex
35
35
  */
36
36
  const CODEX_MODELS = [
37
+ {
38
+ id: 'gpt-5.5-high',
39
+ cli_model: 'gpt-5.5',
40
+ extra_args: ['-c', 'model_reasoning_effort="high"'],
41
+ name: 'GPT-5.5 High',
42
+ tier: 'thorough',
43
+ tagline: 'Latest Deep',
44
+ description: 'Latest-generation GPT model with high reasoning effort for demanding PR reviews, strong code understanding, and careful cross-file analysis.',
45
+ badge: 'Recommended',
46
+ badgeClass: 'badge-recommended',
47
+ default: true
48
+ },
49
+ {
50
+ id: 'gpt-5.5-xhigh',
51
+ cli_model: 'gpt-5.5',
52
+ extra_args: ['-c', 'model_reasoning_effort="xhigh"'],
53
+ name: 'GPT-5.5 XHigh',
54
+ tier: 'thorough',
55
+ tagline: 'Frontier Depth',
56
+ description: 'GPT-5.5 with extra-high reasoning effort for the hardest reviews: architecture, concurrency, security-sensitive changes, and large codebase context.',
57
+ badge: 'Max Reasoning',
58
+ badgeClass: 'badge-power'
59
+ },
37
60
  {
38
61
  id: 'gpt-5.4-high',
39
62
  // Alias keeps results/councils saved under the previous bare `gpt-5.4`
@@ -45,9 +68,8 @@ const CODEX_MODELS = [
45
68
  tier: 'thorough',
46
69
  tagline: 'Deep Review',
47
70
  description: 'GPT-5.4 with high reasoning effort for complex multi-file reviews, architectural consistency, and subtle behavioral regressions.',
48
- badge: 'Recommended',
49
- badgeClass: 'badge-recommended',
50
- default: true
71
+ badge: 'Previous Gen',
72
+ badgeClass: 'badge-power'
51
73
  },
52
74
  {
53
75
  id: 'gpt-5.4-xhigh',
@@ -60,28 +82,6 @@ const CODEX_MODELS = [
60
82
  badge: 'Extra High',
61
83
  badgeClass: 'badge-power'
62
84
  },
63
- {
64
- id: 'gpt-5.5-high',
65
- cli_model: 'gpt-5.5',
66
- extra_args: ['-c', 'model_reasoning_effort="high"'],
67
- name: 'GPT-5.5 High',
68
- tier: 'thorough',
69
- tagline: 'Latest Deep',
70
- description: 'Latest-generation GPT model with high reasoning effort for demanding PR reviews, strong code understanding, and careful cross-file analysis.',
71
- badge: 'High Effort',
72
- badgeClass: 'badge-power'
73
- },
74
- {
75
- id: 'gpt-5.5-xhigh',
76
- cli_model: 'gpt-5.5',
77
- extra_args: ['-c', 'model_reasoning_effort="xhigh"'],
78
- name: 'GPT-5.5 XHigh',
79
- tier: 'thorough',
80
- tagline: 'Frontier Depth',
81
- description: 'GPT-5.5 with extra-high reasoning effort for the hardest reviews: architecture, concurrency, security-sensitive changes, and large codebase context.',
82
- badge: 'Max Reasoning',
83
- badgeClass: 'badge-power'
84
- },
85
85
  {
86
86
  id: 'gpt-5.3-codex',
87
87
  name: 'GPT-5.3 Codex',
@@ -121,7 +121,7 @@ class CodexProvider extends AIProvider {
121
121
  * @param {Object} configOverrides.env - Additional environment variables
122
122
  * @param {Object[]} configOverrides.models - Custom model definitions
123
123
  */
124
- constructor(model = 'gpt-5.4-high', configOverrides = {}) {
124
+ constructor(model = 'gpt-5.5-high', configOverrides = {}) {
125
125
  super(model);
126
126
 
127
127
  // Command precedence: ENV > config > default
@@ -149,9 +149,9 @@ class CodexProvider extends AIProvider {
149
149
  // 2. "read-only" prevents ALL shell commands including git-diff-lines
150
150
  // 3. The AI is instructed to only analyze code, not modify it
151
151
  //
152
- // --full-auto: Non-interactive mode that auto-approves within sandbox bounds.
153
- // Combined with workspace-write sandbox, this limits damage to the worktree only.
154
- // Note: The -a flag is for interactive mode only; exec subcommand uses --full-auto.
152
+ // Newer Codex CLI versions deprecate --full-auto; `codex exec` is already
153
+ // non-interactive, and `--sandbox workspace-write` selects the required
154
+ // sandbox policy.
155
155
  //
156
156
  // Shell environment config:
157
157
  // - allow_login_shell=false: Prevents zsh from using -l flag, which would
@@ -164,7 +164,7 @@ class CodexProvider extends AIProvider {
164
164
  // (--dangerously-bypass-approvals-and-sandbox is the Codex CLI equivalent of Claude's --dangerously-skip-permissions)
165
165
  const sandboxArgs = configOverrides.yolo
166
166
  ? ['--dangerously-bypass-approvals-and-sandbox']
167
- : ['--sandbox', 'workspace-write', '--full-auto'];
167
+ : ['--sandbox', 'workspace-write'];
168
168
  // Shell env args prevent login shell from reconstructing PATH (orthogonal to
169
169
  // sandbox permissions). Overridable via configOverrides.args following the
170
170
  // same two-tier pattern as chat-providers.js: args replaces, extra_args appends.
@@ -352,7 +352,7 @@ class CodexProvider extends AIProvider {
352
352
 
353
353
  if (code !== 0) {
354
354
  logger.error(`${levelPrefix} Codex CLI exited with code ${code}`);
355
- settle(reject, new Error(`${levelPrefix} Codex CLI exited with code ${code}: ${stderr}`));
355
+ settle(reject, this.createExitError(code, stderr, levelPrefix));
356
356
  return;
357
357
  }
358
358
 
@@ -433,6 +433,37 @@ class CodexProvider extends AIProvider {
433
433
  });
434
434
  }
435
435
 
436
+ /**
437
+ * Build an actionable error for Codex CLI process failures.
438
+ *
439
+ * @param {number} code - Process exit code
440
+ * @param {string} stderr - Captured stderr
441
+ * @param {string} levelPrefix - Logging prefix
442
+ * @returns {Error}
443
+ */
444
+ createExitError(code, stderr, levelPrefix) {
445
+ const stderrText = stderr.trim();
446
+
447
+ if (this.isAuthError(stderrText)) {
448
+ return new Error(
449
+ `${levelPrefix} Codex CLI authentication failed. Check Codex CLI authentication and try again. ` +
450
+ `Original stderr: ${stderrText}`
451
+ );
452
+ }
453
+
454
+ return new Error(`${levelPrefix} Codex CLI exited with code ${code}: ${stderr}`);
455
+ }
456
+
457
+ /**
458
+ * Detect authentication failures reported by the Codex CLI.
459
+ *
460
+ * @param {string} stderr - Captured stderr
461
+ * @returns {boolean}
462
+ */
463
+ isAuthError(stderr) {
464
+ return /(?:401\s+Unauthorized|HTTP error:\s*401|Unauthorized)/i.test(stderr);
465
+ }
466
+
436
467
  /**
437
468
  * Parse Codex CLI JSONL response
438
469
  * Codex outputs JSONL with multiple event types:
@@ -664,7 +695,7 @@ class CodexProvider extends AIProvider {
664
695
 
665
696
  // Base args for extraction (read-only sandbox, no shell access needed)
666
697
  // Note: '-' (stdin marker) must come LAST, after any extra_args
667
- const baseArgs = ['exec', '-m', cliModel, '--json', '--sandbox', 'read-only', '--full-auto'];
698
+ const baseArgs = ['exec', '-m', cliModel, '--json', '--sandbox', 'read-only'];
668
699
 
669
700
  // Append stdin marker '-' at the end after all other args
670
701
  return [...baseArgs, ...extraArgs, '-'];
@@ -790,7 +821,7 @@ class CodexProvider extends AIProvider {
790
821
  }
791
822
 
792
823
  static getDefaultModel() {
793
- return 'gpt-5.4-high';
824
+ return 'gpt-5.5-high';
794
825
  }
795
826
 
796
827
  static getInstallInstructions() {
@@ -235,7 +235,7 @@ curl -s -X POST http://localhost:{{PORT}}/api/pr/OWNER/REPO/PR_NUMBER/analyses \
235
235
  -H 'Content-Type: application/json' \\
236
236
  -d '{
237
237
  "provider": "claude",
238
- "model": "claude-sonnet-4-5-20250929",
238
+ "model": "claude-opus-4-7",
239
239
  "tier": "balanced",
240
240
  "customInstructions": "Focus on security issues."
241
241
  }'
@@ -12,6 +12,7 @@ const logger = require('../utils/logger');
12
12
 
13
13
  // Default dependencies (overridable for testing)
14
14
  const defaults = { spawn };
15
+ const CODEX_SANDBOX_MODES = new Set(['workspace-write', 'read-only']);
15
16
 
16
17
  /**
17
18
  * Built-in chat provider definitions.
@@ -68,6 +69,7 @@ const CHAT_PROVIDERS = {
68
69
  name: 'Codex (JSON-RPC)',
69
70
  type: 'codex',
70
71
  command: 'codex',
72
+ sandbox: 'workspace-write',
71
73
  // Shell environment config prevents zsh -l from reconstructing PATH,
72
74
  // ensuring git-diff-lines and other bin/ scripts remain findable.
73
75
  args: [
@@ -118,11 +120,17 @@ function getChatProvider(id) {
118
120
  };
119
121
  if (overrides.model) provider.model = overrides.model;
120
122
  if (overrides.provider) provider.provider = overrides.provider;
123
+ if (overrides.availability_command !== undefined) {
124
+ provider.availability_command = overrides.availability_command;
125
+ }
121
126
  if (overrides.extra_args && Array.isArray(overrides.extra_args)) {
122
127
  provider.args = [...provider.args, ...overrides.extra_args];
123
128
  }
124
129
  if (overrides.load_skills !== undefined) provider.load_skills = overrides.load_skills;
125
130
  if (overrides.app_extensions !== undefined) provider.app_extensions = overrides.app_extensions;
131
+ if (provider.type === 'codex' && overrides.sandbox !== undefined) {
132
+ provider.sandbox = normalizeCodexSandbox(overrides.sandbox, id);
133
+ }
126
134
  if (provider.command.includes(' ')) {
127
135
  provider.useShell = true;
128
136
  }
@@ -136,6 +144,9 @@ function getChatProvider(id) {
136
144
  if (overrides.command) merged.command = overrides.command;
137
145
  if (overrides.model) merged.model = overrides.model;
138
146
  if (overrides.provider) merged.provider = overrides.provider;
147
+ if (overrides.availability_command !== undefined) {
148
+ merged.availability_command = overrides.availability_command;
149
+ }
139
150
  if (overrides.env) merged.env = { ...merged.env, ...overrides.env };
140
151
  if (overrides.args) {
141
152
  merged.args = overrides.args;
@@ -146,6 +157,9 @@ function getChatProvider(id) {
146
157
  }
147
158
  if (overrides.load_skills !== undefined) merged.load_skills = overrides.load_skills;
148
159
  if (overrides.app_extensions !== undefined) merged.app_extensions = overrides.app_extensions;
160
+ if (base.type === 'codex' && overrides.sandbox !== undefined) {
161
+ merged.sandbox = normalizeCodexSandbox(overrides.sandbox, id);
162
+ }
149
163
  // For multi-word commands (e.g. "devx claude"), use shell mode
150
164
  if (merged.command && merged.command.includes(' ')) {
151
165
  merged.useShell = true;
@@ -153,6 +167,24 @@ function getChatProvider(id) {
153
167
  return merged;
154
168
  }
155
169
 
170
+ /**
171
+ * Validate the small user-facing Codex sandbox config surface.
172
+ * @param {string} sandbox
173
+ * @param {string} providerId
174
+ * @returns {string}
175
+ */
176
+ function normalizeCodexSandbox(sandbox, providerId = 'codex') {
177
+ if (CODEX_SANDBOX_MODES.has(sandbox)) {
178
+ return sandbox;
179
+ }
180
+
181
+ logger.warn(
182
+ `[ChatProviders] Invalid sandbox "${sandbox}" for ${providerId}; ` +
183
+ 'falling back to workspace-write. Supported values: workspace-write, read-only.'
184
+ );
185
+ return 'workspace-write';
186
+ }
187
+
156
188
  /**
157
189
  * Get all chat provider definitions (built-in + dynamic from config).
158
190
  * @returns {Array<Object>}
@@ -197,8 +229,10 @@ function isCodexProvider(id) {
197
229
 
198
230
  /**
199
231
  * Check availability of a single chat provider.
200
- * For Pi, delegates to the existing AI provider availability cache.
201
- * For ACP providers, spawns `<command> --version` to verify the binary exists.
232
+ * Providers with `availability_command` run that command first.
233
+ * Without an availability command, Pi delegates to the existing AI provider
234
+ * availability cache and other providers spawn `<command> --version` to verify
235
+ * the binary exists.
202
236
  * @param {string} id - Provider ID
203
237
  * @param {Object} [_deps] - Dependency overrides for testing
204
238
  * @returns {Promise<{available: boolean, error?: string}>}
@@ -209,6 +243,19 @@ async function checkChatProviderAvailability(id, _deps) {
209
243
  return { available: false, error: `Unknown provider: ${id}` };
210
244
  }
211
245
 
246
+ const deps = { ...defaults, ..._deps };
247
+
248
+ if (provider.availability_command) {
249
+ return runCommandAvailabilityCheck({
250
+ deps,
251
+ command: provider.availability_command,
252
+ args: [],
253
+ displayCommand: 'availability command',
254
+ shell: true,
255
+ env: provider.env,
256
+ });
257
+ }
258
+
212
259
  // Pi delegates to existing AI provider availability
213
260
  if (provider.type === 'pi') {
214
261
  const cached = getCachedAvailability('pi');
@@ -218,30 +265,61 @@ async function checkChatProviderAvailability(id, _deps) {
218
265
  // Codex uses the same binary-check pattern as ACP providers
219
266
  // (falls through to the spawn check below)
220
267
 
221
- const deps = { ...defaults, ..._deps };
222
268
  const command = provider.command;
223
269
  const useShell = provider.useShell || false;
224
270
 
271
+ // For multi-word commands, use shell mode
272
+ const spawnCmd = useShell ? `${command} --version` : command;
273
+ const spawnArgs = useShell ? [] : ['--version'];
274
+ return runCommandAvailabilityCheck({
275
+ deps,
276
+ command: spawnCmd,
277
+ args: spawnArgs,
278
+ displayCommand: `${command} --version`,
279
+ shell: useShell,
280
+ env: provider.env,
281
+ });
282
+ }
283
+
284
+ /**
285
+ * Spawn a command and resolve based on its exit status. Shared by the
286
+ * configured `availability_command` path and the legacy `<command> --version`
287
+ * fallback.
288
+ *
289
+ * Notes on the option choices:
290
+ * - `stdio: ['ignore', 'ignore', 'ignore']` discards output so a verbose probe
291
+ * cannot fill an OS pipe buffer and block while waiting for a reader.
292
+ * - `shell: true` allows multi-word configured commands to run through the
293
+ * user's shell.
294
+ * - `once()` avoids leaking listeners or resolving twice if multiple child
295
+ * process events fire.
296
+ * - `displayCommand` is used in error messages so user-configured shell strings
297
+ * do not need to be printed verbatim.
298
+ *
299
+ * @param {{deps: {spawn: Function}, command: string, args: string[], displayCommand: string, shell: boolean, env?: Object}} opts
300
+ * @returns {Promise<{available: boolean, error?: string}>}
301
+ */
302
+ function runCommandAvailabilityCheck({ deps, command, args, displayCommand, shell, env }) {
225
303
  return new Promise((resolve) => {
226
304
  try {
227
- // For multi-word commands, use shell mode
228
- const spawnCmd = useShell ? `${command} --version` : command;
229
- const spawnArgs = useShell ? [] : ['--version'];
230
- const proc = deps.spawn(spawnCmd, spawnArgs, {
231
- stdio: ['ignore', 'pipe', 'pipe'],
305
+ const proc = deps.spawn(command, args, {
306
+ stdio: ['ignore', 'ignore', 'ignore'],
232
307
  timeout: 10000,
233
- shell: useShell,
308
+ shell,
309
+ env: { ...process.env, ...(env || {}) },
234
310
  });
235
311
 
236
- proc.on('error', (err) => {
312
+ proc.once('error', (err) => {
237
313
  resolve({ available: false, error: err.message });
238
314
  });
239
315
 
240
- proc.on('close', (code) => {
316
+ proc.once('close', (code, signal) => {
241
317
  if (code === 0) {
242
318
  resolve({ available: true });
319
+ } else if (signal) {
320
+ resolve({ available: false, error: `${displayCommand} timed out or was terminated (${signal})` });
243
321
  } else {
244
- resolve({ available: false, error: `${command} --version exited with code ${code}` });
322
+ resolve({ available: false, error: `${displayCommand} exited with code ${code}` });
245
323
  }
246
324
  });
247
325
  } catch (err) {