@in-the-loop-labs/pair-review 1.3.1 → 1.3.2

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/README.md CHANGED
@@ -144,7 +144,7 @@ Configuration is stored in `~/.pair-review/config.json`:
144
144
  "port": 7247,
145
145
  "theme": "light",
146
146
  "default_provider": "claude",
147
- "default_model": "sonnet"
147
+ "default_model": "opus"
148
148
  }
149
149
  ```
150
150
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@in-the-loop-labs/pair-review",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "Your AI-powered code review partner - Close the feedback loop with AI coding agents",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pair-review",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "pair-review app integration — Open PRs and local changes in the pair-review web UI, run server-side AI analysis, and address review feedback. Requires the pair-review MCP server.",
5
5
  "author": {
6
6
  "name": "in-the-loop-labs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-critic",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "AI-powered code review analysis — Run three-level AI analysis and implement-review-fix loops directly in your coding agent. Works standalone, no server required.",
5
5
  "author": {
6
6
  "name": "in-the-loop-labs",
@@ -11,7 +11,7 @@ class AnalysisConfigModal {
11
11
  this.onCancel = null;
12
12
  this.escapeHandler = null;
13
13
  this.selectedProvider = 'claude';
14
- this.selectedModel = 'sonnet';
14
+ this.selectedModel = 'opus';
15
15
  this.selectedPresets = new Set();
16
16
  this.rememberModel = false;
17
17
  this.repoInstructions = '';
@@ -92,9 +92,9 @@ class AnalysisConfigModal {
92
92
  id: 'claude',
93
93
  name: 'Claude',
94
94
  models: [
95
- { id: 'sonnet', name: 'Sonnet', tier: 'balanced', default: true }
95
+ { id: 'opus', name: 'Opus 4.6 High', tier: 'thorough', default: true }
96
96
  ],
97
- defaultModel: 'sonnet'
97
+ defaultModel: 'opus'
98
98
  }
99
99
  };
100
100
  this.models = this.providers.claude.models;
@@ -363,7 +363,7 @@ class LocalManager {
363
363
  const providerStorageKey = `pair-review-provider:local-${reviewId}`;
364
364
  const rememberedModel = localStorage.getItem(modelStorageKey);
365
365
  const rememberedProvider = localStorage.getItem(providerStorageKey);
366
- const currentModel = rememberedModel || repoSettings?.default_model || 'sonnet';
366
+ const currentModel = rememberedModel || repoSettings?.default_model || 'opus';
367
367
  const currentProvider = rememberedProvider || repoSettings?.default_provider || 'claude';
368
368
 
369
369
  // Show config modal
@@ -1027,7 +1027,7 @@ class LocalManager {
1027
1027
  },
1028
1028
  body: JSON.stringify({
1029
1029
  provider: config.provider || 'claude',
1030
- model: config.model || 'sonnet',
1030
+ model: config.model || 'opus',
1031
1031
  tier: config.tier || 'balanced',
1032
1032
  customInstructions: config.customInstructions || null,
1033
1033
  skipLevel3: config.skipLevel3 || false
@@ -734,6 +734,10 @@ class AnalysisHistoryManager {
734
734
  'haiku': 'fast',
735
735
  'sonnet': 'balanced',
736
736
  'opus': 'thorough',
737
+ 'opus-4.5': 'balanced',
738
+ 'opus-4.6-low': 'balanced',
739
+ 'opus-4.6-medium': 'balanced',
740
+ 'opus-4.6-1m': 'balanced',
737
741
  // Gemini models
738
742
  'flash': 'fast',
739
743
  'pro': 'balanced',
package/public/js/pr.js CHANGED
@@ -3515,7 +3515,7 @@ class PRManager {
3515
3515
  const providerStorageKey = PRManager.getRepoStorageKey('pair-review-provider', owner, repo);
3516
3516
  const rememberedModel = localStorage.getItem(modelStorageKey);
3517
3517
  const rememberedProvider = localStorage.getItem(providerStorageKey);
3518
- const currentModel = rememberedModel || repoSettings?.default_model || 'sonnet';
3518
+ const currentModel = rememberedModel || repoSettings?.default_model || 'opus';
3519
3519
  const currentProvider = rememberedProvider || repoSettings?.default_provider || 'claude';
3520
3520
 
3521
3521
  // Show the config modal
@@ -3593,7 +3593,7 @@ class PRManager {
3593
3593
  },
3594
3594
  body: JSON.stringify({
3595
3595
  provider: config.provider || 'claude',
3596
- model: config.model || 'sonnet',
3596
+ model: config.model || 'opus',
3597
3597
  tier: config.tier || 'balanced',
3598
3598
  customInstructions: config.customInstructions || null,
3599
3599
  skipLevel3: config.skipLevel3 || false
@@ -208,23 +208,11 @@ class RepoSettingsPage {
208
208
 
209
209
  } catch (error) {
210
210
  console.error('Error loading providers:', error);
211
- // Last-resort degraded mode: hardcoded Claude fallback when the /api/providers
212
- // endpoint is unavailable. This allows basic functionality even if the backend
213
- // is partially broken. The canonical provider definitions live in
214
- // src/ai/claude-provider.js - this fallback should mirror those values.
215
- this.providers = {
216
- claude: {
217
- id: 'claude',
218
- name: 'Claude',
219
- models: [
220
- { id: 'haiku', name: 'Haiku', tier: 'fast', badge: 'Fastest', badgeClass: 'badge-speed', tagline: 'Lightning Fast', description: 'Quick analysis for simple changes' },
221
- { id: 'sonnet', name: 'Sonnet', tier: 'balanced', default: true, badge: 'Recommended', badgeClass: 'badge-recommended', tagline: 'Best Balance', description: 'Recommended for most reviews' },
222
- { id: 'opus', name: 'Opus', tier: 'thorough', badge: 'Most Thorough', badgeClass: 'badge-power', tagline: 'Most Capable', description: 'Deep analysis for complex code' }
223
- ],
224
- defaultModel: 'sonnet'
225
- }
226
- };
211
+ // No hardcoded fallback rely on the /api/providers endpoint as the single source of truth.
212
+ // If the endpoint is unavailable, show an empty state rather than stale data.
213
+ this.providers = {};
227
214
  this.renderProviderButtons();
215
+ this.showToast('error', 'Failed to load AI providers. Please refresh the page.');
228
216
  }
229
217
  }
230
218
 
@@ -26,10 +26,10 @@ const { buildSparseCheckoutGuidance } = require('./prompts/sparse-checkout-guida
26
26
  class Analyzer {
27
27
  /**
28
28
  * @param {Object} database - Database instance
29
- * @param {string} model - Model to use (e.g., 'sonnet', 'gemini-2.5-pro')
29
+ * @param {string} model - Model to use (e.g., 'opus', 'gemini-2.5-pro')
30
30
  * @param {string} provider - Provider ID (e.g., 'claude', 'gemini'). Defaults to 'claude'.
31
31
  */
32
- constructor(database, model = 'sonnet', provider = 'claude') {
32
+ constructor(database, model = 'opus', provider = 'claude') {
33
33
  // Store model and provider for creating provider instances per level
34
34
  this.model = model;
35
35
  this.provider = provider;
@@ -5,7 +5,7 @@ const logger = require('../utils/logger');
5
5
  const { extractJSON } = require('../utils/json-extractor');
6
6
 
7
7
  class ClaudeCLI {
8
- constructor(model = 'sonnet') {
8
+ constructor(model = 'opus') {
9
9
  // Check for environment variable to override default command
10
10
  // Use PAIR_REVIEW_CLAUDE_CMD environment variable if set, otherwise default to 'claude'
11
11
  const claudeCmd = process.env.PAIR_REVIEW_CLAUDE_CMD || 'claude';
@@ -123,6 +123,11 @@ class ClaudeCLI {
123
123
  }
124
124
  });
125
125
 
126
+ // Handle stdin errors (e.g., EPIPE if process exits before write completes)
127
+ claude.stdin.on('error', (err) => {
128
+ logger.error(`${levelPrefix} stdin error: ${err.message}`);
129
+ });
130
+
126
131
  // Send the prompt to stdin with backpressure handling
127
132
  claude.stdin.write(prompt, (err) => {
128
133
  if (err) {
@@ -22,7 +22,7 @@ const BIN_DIR = path.join(__dirname, '..', '..', 'bin');
22
22
  const CLAUDE_MODELS = [
23
23
  {
24
24
  id: 'haiku',
25
- name: 'Haiku',
25
+ name: 'Haiku 4.5',
26
26
  tier: 'fast',
27
27
  tagline: 'Lightning Fast',
28
28
  description: 'Quick analysis for simple changes',
@@ -31,21 +31,65 @@ const CLAUDE_MODELS = [
31
31
  },
32
32
  {
33
33
  id: 'sonnet',
34
- name: 'Sonnet',
34
+ name: 'Sonnet 4.5',
35
35
  tier: 'balanced',
36
36
  tagline: 'Best Balance',
37
37
  description: 'Recommended for most reviews',
38
- badge: 'Recommended',
39
- badgeClass: 'badge-recommended',
40
- default: true
38
+ badge: 'Standard',
39
+ badgeClass: 'badge-recommended'
40
+ },
41
+ {
42
+ id: 'opus-4.5',
43
+ cli_model: 'claude-opus-4-5-20251101',
44
+ name: 'Opus 4.5',
45
+ tier: 'balanced',
46
+ tagline: 'Deep Thinker',
47
+ description: 'Extended thinking for complex analysis',
48
+ badge: 'Previous Gen',
49
+ badgeClass: 'badge-power'
50
+ },
51
+ {
52
+ id: 'opus-4.6-low',
53
+ cli_model: 'opus',
54
+ env: { CLAUDE_CODE_EFFORT_LEVEL: 'low' },
55
+ name: 'Opus 4.6 Low',
56
+ tier: 'balanced',
57
+ tagline: 'Fast Opus',
58
+ description: 'Opus 4.6 with low effort — quick and capable',
59
+ badge: 'Balanced',
60
+ badgeClass: 'badge-recommended'
61
+ },
62
+ {
63
+ id: 'opus-4.6-medium',
64
+ cli_model: 'opus',
65
+ env: { CLAUDE_CODE_EFFORT_LEVEL: 'medium' },
66
+ name: 'Opus 4.6 Medium',
67
+ tier: 'balanced',
68
+ tagline: 'Balanced Opus',
69
+ description: 'Opus 4.6 with medium effort — balanced depth',
70
+ badge: 'Thorough',
71
+ badgeClass: 'badge-power'
41
72
  },
42
73
  {
43
74
  id: 'opus',
44
- name: 'Opus',
75
+ aliases: ['opus-4.6-high'],
76
+ env: { CLAUDE_CODE_EFFORT_LEVEL: 'high' },
77
+ name: 'Opus 4.6 High',
45
78
  tier: 'thorough',
46
- tagline: 'Most Capable',
47
- description: 'Deep analysis for complex code',
79
+ tagline: 'Maximum Depth',
80
+ description: 'Opus 4.6 with high effort — deepest analysis',
48
81
  badge: 'Most Thorough',
82
+ badgeClass: 'badge-power',
83
+ default: true
84
+ },
85
+ {
86
+ id: 'opus-4.6-1m',
87
+ cli_model: 'opus[1m]',
88
+ name: 'Opus 4.6 1M',
89
+ tier: 'balanced',
90
+ tagline: 'Extended Context',
91
+ description: 'Opus 4.6 high effort with 1M token context window',
92
+ badge: 'More Context',
49
93
  badgeClass: 'badge-power'
50
94
  }
51
95
  ];
@@ -59,7 +103,7 @@ class ClaudeProvider extends AIProvider {
59
103
  * @param {Object} configOverrides.env - Additional environment variables
60
104
  * @param {Object[]} configOverrides.models - Custom model definitions
61
105
  */
62
- constructor(model = 'sonnet', configOverrides = {}) {
106
+ constructor(model = 'opus', configOverrides = {}) {
63
107
  super(model);
64
108
 
65
109
  // Command precedence: ENV > config > default
@@ -67,7 +111,7 @@ class ClaudeProvider extends AIProvider {
67
111
  const configCmd = configOverrides.command;
68
112
  const claudeCmd = envCmd || configCmd || 'claude';
69
113
 
70
- // Store for use in getExtractionConfig and testAvailability
114
+ // Store for use in getExtractionConfig, buildArgsForModel, and testAvailability
71
115
  this.claudeCmd = claudeCmd;
72
116
  this.configOverrides = configOverrides;
73
117
 
@@ -77,6 +121,9 @@ class ClaudeProvider extends AIProvider {
77
121
  // Check for budget limit environment variable
78
122
  const maxBudget = process.env.PAIR_REVIEW_MAX_BUDGET_USD;
79
123
 
124
+ // Resolve model config using shared helper
125
+ const { builtIn, configModel, cliModelArgs, extraArgs, env } = this._resolveModelConfig(model);
126
+
80
127
  // Build args: base args + provider extra_args + model extra_args
81
128
  // Use --output-format stream-json for JSONL streaming output (better debugging visibility)
82
129
  //
@@ -116,43 +163,98 @@ class ClaudeProvider extends AIProvider {
116
163
  ].join(',');
117
164
  permissionArgs = ['--allowedTools', allowedTools];
118
165
  }
119
- const baseArgs = ['-p', '--verbose', '--model', model, '--output-format', 'stream-json', ...permissionArgs];
166
+ const baseArgs = ['-p', '--verbose', ...cliModelArgs, '--output-format', 'stream-json', ...permissionArgs];
120
167
  if (maxBudget) {
121
168
  const budgetNum = parseFloat(maxBudget);
122
169
  if (isNaN(budgetNum) || budgetNum <= 0) {
123
- console.warn(`Warning: PAIR_REVIEW_MAX_BUDGET_USD="${maxBudget}" is not a valid positive number, ignoring`);
170
+ logger.warn(`Warning: PAIR_REVIEW_MAX_BUDGET_USD="${maxBudget}" is not a valid positive number, ignoring`);
124
171
  } else {
125
172
  baseArgs.push('--max-budget-usd', String(budgetNum));
126
173
  }
127
174
  }
128
- const providerArgs = configOverrides.extra_args || [];
129
- const modelConfig = configOverrides.models?.find(m => m.id === model);
130
- const modelArgs = modelConfig?.extra_args || [];
131
175
 
132
- // Merge env: provider env + model env
133
- this.extraEnv = {
134
- ...(configOverrides.env || {}),
135
- ...(modelConfig?.env || {})
136
- };
176
+ // Three-way merge for env: built-in model → provider config per-model config
177
+ this.extraEnv = env;
137
178
 
138
179
  if (this.useShell) {
139
- // Quote the allowedTools value to prevent shell interpretation of special characters
140
- // (commas, parentheses in patterns like "Bash(git diff*)")
141
- const quotedBaseArgs = baseArgs.map((arg, i) => {
142
- // The allowedTools value follows the --allowedTools flag
143
- if (baseArgs[i - 1] === '--allowedTools') {
144
- return `'${arg}'`;
145
- }
146
- return arg;
147
- });
148
- this.command = `${claudeCmd} ${[...quotedBaseArgs, ...providerArgs, ...modelArgs].join(' ')}`;
180
+ const allArgs = [...baseArgs, ...extraArgs];
181
+ this.command = `${claudeCmd} ${this._quoteShellArgs(allArgs).join(' ')}`;
149
182
  this.args = [];
150
183
  } else {
151
184
  this.command = claudeCmd;
152
- this.args = [...baseArgs, ...providerArgs, ...modelArgs];
185
+ this.args = [...baseArgs, ...extraArgs];
153
186
  }
154
187
  }
155
188
 
189
+ /**
190
+ * Quote shell-sensitive arguments for safe shell execution.
191
+ * Any arg containing characters that could be interpreted by the shell
192
+ * (brackets, parentheses, commas, etc.) is wrapped in single quotes
193
+ * with internal single quotes escaped using the POSIX pattern.
194
+ *
195
+ * @param {string[]} args - Array of CLI arguments
196
+ * @returns {string[]} Args with shell-sensitive values quoted
197
+ * @private
198
+ */
199
+ _quoteShellArgs(args) {
200
+ return args.map((arg, i) => {
201
+ const prevArg = args[i - 1];
202
+ if (prevArg === '--allowedTools' || prevArg === '--model') {
203
+ if (/[][*?(){}$!&|;<>,\s']/.test(arg)) {
204
+ return `'${arg.replace(/'/g, "'\\''")}'`;
205
+ }
206
+ }
207
+ return arg;
208
+ });
209
+ }
210
+
211
+ /**
212
+ * Resolve model configuration by looking up built-in and config override definitions.
213
+ * Consolidates the CLAUDE_MODELS.find() and configOverrides.models.find() lookups
214
+ * used across the constructor, buildArgsForModel(), and getExtractionConfig().
215
+ *
216
+ * @param {string} modelId - The model identifier to resolve
217
+ * @returns {Object} Resolved configuration
218
+ * @returns {Object|undefined} .builtIn - Built-in model definition from CLAUDE_MODELS
219
+ * @returns {Object|undefined} .configModel - Config override model definition
220
+ * @returns {string[]} .cliModelArgs - Args array for --model (empty if suppressed)
221
+ * @returns {string[]} .extraArgs - Merged extra_args from built-in, provider, and config model
222
+ * @returns {Object} .env - Merged env from built-in, provider, and config model
223
+ * @private
224
+ */
225
+ _resolveModelConfig(modelId) {
226
+ const configOverrides = this.configOverrides || {};
227
+
228
+ // Resolve cli_model: config model > built-in model > id
229
+ // cli_model decouples the app-level model ID from the CLI --model argument.
230
+ // - undefined: fall through the resolution chain
231
+ // - string: use this exact value for --model
232
+ // - null: explicitly suppress --model (for tools that want the model set via env instead)
233
+ const builtIn = CLAUDE_MODELS.find(m => m.id === modelId || (m.aliases && m.aliases.includes(modelId)));
234
+ const configModel = configOverrides.models?.find(m => m.id === modelId);
235
+ const resolvedCliModel = configModel?.cli_model !== undefined
236
+ ? configModel.cli_model
237
+ : (builtIn?.cli_model !== undefined ? builtIn.cli_model : modelId);
238
+
239
+ // Conditionally include --model in base args (null = suppress, empty string passes through to surface CLI error)
240
+ const cliModelArgs = resolvedCliModel !== null ? ['--model', resolvedCliModel] : [];
241
+
242
+ // Three-way merge for extra_args: built-in model → provider config → per-model config
243
+ const builtInArgs = builtIn?.extra_args || [];
244
+ const providerArgs = configOverrides.extra_args || [];
245
+ const configModelArgs = configModel?.extra_args || [];
246
+ const extraArgs = [...builtInArgs, ...providerArgs, ...configModelArgs];
247
+
248
+ // Three-way merge for env: built-in model → provider config → per-model config
249
+ const env = {
250
+ ...(builtIn?.env || {}),
251
+ ...(configOverrides.env || {}),
252
+ ...(configModel?.env || {})
253
+ };
254
+
255
+ return { builtIn, configModel, cliModelArgs, extraArgs, env };
256
+ }
257
+
156
258
  /**
157
259
  * Execute Claude CLI with a prompt
158
260
  * @param {string} prompt - The prompt to send to Claude
@@ -328,6 +430,11 @@ class ClaudeProvider extends AIProvider {
328
430
  }
329
431
  });
330
432
 
433
+ // Handle stdin errors (e.g., EPIPE if process exits before write completes)
434
+ claude.stdin.on('error', (err) => {
435
+ logger.error(`${levelPrefix} stdin error: ${err.message}`);
436
+ });
437
+
331
438
  // Send the prompt to stdin
332
439
  claude.stdin.write(prompt, (err) => {
333
440
  if (err) {
@@ -351,15 +458,12 @@ class ClaudeProvider extends AIProvider {
351
458
  * @returns {string[]} Complete args array for the CLI
352
459
  */
353
460
  buildArgsForModel(model) {
461
+ const { cliModelArgs, extraArgs } = this._resolveModelConfig(model);
462
+
354
463
  // Base args for extraction (simple prompt mode, no tools needed)
355
- const baseArgs = ['-p', '--model', model];
356
- // Provider-level extra_args (from configOverrides)
357
- const providerArgs = this.configOverrides?.extra_args || [];
358
- // Model-specific extra_args (from the model config for the given model)
359
- const modelConfig = this.configOverrides?.models?.find(m => m.id === model);
360
- const modelArgs = modelConfig?.extra_args || [];
361
-
362
- return [...baseArgs, ...providerArgs, ...modelArgs];
464
+ const baseArgs = ['-p', ...cliModelArgs];
465
+
466
+ return [...baseArgs, ...extraArgs];
363
467
  }
364
468
 
365
469
  /**
@@ -373,22 +477,26 @@ class ClaudeProvider extends AIProvider {
373
477
  const claudeCmd = this.claudeCmd;
374
478
  const useShell = this.useShell;
375
479
 
376
- // Build args consistently using the shared method, applying provider and model extra_args
377
- const args = this.buildArgsForModel(model);
480
+ // Single call to _resolveModelConfig for both args and env
481
+ const { cliModelArgs, extraArgs, env } = this._resolveModelConfig(model);
482
+ const args = ['-p', ...cliModelArgs, ...extraArgs];
378
483
 
379
484
  if (useShell) {
485
+ const quotedArgs = this._quoteShellArgs(args);
380
486
  return {
381
- command: `${claudeCmd} ${args.join(' ')}`,
487
+ command: `${claudeCmd} ${quotedArgs.join(' ')}`,
382
488
  args: [],
383
489
  useShell: true,
384
- promptViaStdin: true
490
+ promptViaStdin: true,
491
+ env
385
492
  };
386
493
  }
387
494
  return {
388
495
  command: claudeCmd,
389
496
  args,
390
497
  useShell: false,
391
- promptViaStdin: true
498
+ promptViaStdin: true,
499
+ env
392
500
  };
393
501
  }
394
502
 
@@ -729,7 +837,7 @@ class ClaudeProvider extends AIProvider {
729
837
  }
730
838
 
731
839
  static getDefaultModel() {
732
- return 'sonnet';
840
+ return 'opus';
733
841
  }
734
842
 
735
843
  static getInstallInstructions() {
@@ -305,6 +305,11 @@ class CodexProvider extends AIProvider {
305
305
  }
306
306
  });
307
307
 
308
+ // Handle stdin errors (e.g., EPIPE if process exits before write completes)
309
+ codex.stdin.on('error', (err) => {
310
+ logger.error(`${levelPrefix} stdin error: ${err.message}`);
311
+ });
312
+
308
313
  // Send the prompt to stdin
309
314
  codex.stdin.write(prompt, (err) => {
310
315
  if (err) {
@@ -352,6 +352,11 @@ class CursorAgentProvider extends AIProvider {
352
352
  }
353
353
  });
354
354
 
355
+ // Handle stdin errors (e.g., EPIPE if process exits before write completes)
356
+ agent.stdin.on('error', (err) => {
357
+ logger.error(`${levelPrefix} stdin error: ${err.message}`);
358
+ });
359
+
355
360
  // Send the prompt to stdin
356
361
  agent.stdin.write(prompt, (err) => {
357
362
  if (err) {
@@ -354,6 +354,11 @@ class GeminiProvider extends AIProvider {
354
354
  }
355
355
  });
356
356
 
357
+ // Handle stdin errors (e.g., EPIPE if process exits before write completes)
358
+ gemini.stdin.on('error', (err) => {
359
+ logger.error(`${levelPrefix} stdin error: ${err.message}`);
360
+ });
361
+
357
362
  // Send the prompt to stdin
358
363
  gemini.stdin.write(prompt, (err) => {
359
364
  if (err) {
@@ -289,6 +289,11 @@ class OpenCodeProvider extends AIProvider {
289
289
  }
290
290
  });
291
291
 
292
+ // Handle stdin errors (e.g., EPIPE if process exits before write completes)
293
+ opencode.stdin.on('error', (err) => {
294
+ logger.error(`${levelPrefix} stdin error: ${err.message}`);
295
+ });
296
+
292
297
  // Send the prompt to stdin (OpenCode reads from stdin when no positional args)
293
298
  // Note on error handling: When stdin.write fails, we kill the process which
294
299
  // triggers the 'close' event handler. The `settled` guard (line 142) prevents
@@ -192,7 +192,7 @@ class AIProvider {
192
192
  };
193
193
  }
194
194
 
195
- const { command, args, useShell, promptViaStdin } = config;
195
+ const { command, args, useShell, promptViaStdin, env: configEnv } = config;
196
196
  const prompt = `Extract the JSON object from the following text. Return ONLY the valid JSON, nothing else. Do not include any explanation, markdown formatting, or code blocks - just the raw JSON.
197
197
 
198
198
  === BEGIN INPUT TEXT ===
@@ -209,6 +209,7 @@ ${rawResponse}
209
209
  cwd: process.cwd(),
210
210
  env: {
211
211
  ...process.env,
212
+ ...(configEnv || {}),
212
213
  PATH: `${BIN_DIR}:${process.env.PATH}`
213
214
  },
214
215
  shell: useShell
@@ -279,6 +280,11 @@ ${rawResponse}
279
280
 
280
281
  // Send prompt via stdin if configured
281
282
  if (promptViaStdin) {
283
+ // Handle stdin errors (e.g., EPIPE if process exits before write completes)
284
+ proc.stdin.on('error', (err) => {
285
+ logger.warn(`${levelPrefix} extraction stdin error: ${err.message}`);
286
+ });
287
+
282
288
  proc.stdin.write(prompt, (err) => {
283
289
  if (err) {
284
290
  logger.warn(`${levelPrefix} Failed to write extraction prompt: ${err}`);
package/src/config.js CHANGED
@@ -14,7 +14,7 @@ const DEFAULT_CONFIG = {
14
14
  port: 7247,
15
15
  theme: "light",
16
16
  default_provider: "claude", // AI provider: 'claude', 'gemini', 'codex', 'copilot', 'opencode'
17
- default_model: "sonnet", // Model within the provider (e.g., 'sonnet' for Claude, 'gemini-2.5-pro' for Gemini)
17
+ default_model: "opus", // Model within the provider (e.g., 'opus' for Claude, 'gemini-2.5-pro' for Gemini)
18
18
  worktree_retention_days: 7,
19
19
  dev_mode: false, // When true, disables static file caching for development
20
20
  debug_stream: false, // When true, logs AI provider streaming events (equivalent to --debug-stream CLI flag)
package/src/main.js CHANGED
@@ -110,6 +110,7 @@ OPTIONS:
110
110
  The web UI also starts for the human reviewer.
111
111
  --model <name> Override the AI model. Claude Code is the default provider.
112
112
  Available models: opus, sonnet, haiku (Claude Code);
113
+ also: opus-4.5, opus-4.6-low, opus-4.6-medium, opus-4.6-1m
113
114
  or use provider-specific models with Gemini/Codex
114
115
  --use-checkout Use current directory instead of creating worktree
115
116
  (automatic in GitHub Actions)
@@ -129,7 +130,7 @@ ENVIRONMENT VARIABLES:
129
130
  PAIR_REVIEW_CLAUDE_CMD Custom command to invoke Claude CLI (default: claude)
130
131
  PAIR_REVIEW_GEMINI_CMD Custom command to invoke Gemini CLI (default: gemini)
131
132
  PAIR_REVIEW_CODEX_CMD Custom command to invoke Codex CLI (default: codex)
132
- PAIR_REVIEW_MODEL Override the AI model (same as --model flag)
133
+ PAIR_REVIEW_MODEL Override the AI model (same as --model flag, default: opus)
133
134
 
134
135
  CONFIGURATION:
135
136
  Config file: ~/.pair-review/config.json
@@ -852,7 +853,7 @@ async function performHeadlessReview(args, config, db, flags, options) {
852
853
 
853
854
  // Run AI analysis
854
855
  console.log('Running AI analysis (all 3 levels)...');
855
- const model = flags.model || process.env.PAIR_REVIEW_MODEL || 'sonnet';
856
+ const model = flags.model || process.env.PAIR_REVIEW_MODEL || 'opus';
856
857
  const analyzer = new Analyzer(db, model);
857
858
 
858
859
  let analysisSummary = null;
package/src/routes/mcp.js CHANGED
@@ -527,7 +527,7 @@ function createMCPServer(db, options = {}) {
527
527
  // Resolve provider and model
528
528
  const repoSettings = repository ? await repoSettingsRepo.getRepoSettings(repository) : null;
529
529
  const provider = process.env.PAIR_REVIEW_PROVIDER || repoSettings?.default_provider || config.default_provider || config.provider || 'claude';
530
- const model = process.env.PAIR_REVIEW_MODEL || repoSettings?.default_model || config.default_model || config.model || 'sonnet';
530
+ const model = process.env.PAIR_REVIEW_MODEL || repoSettings?.default_model || config.default_model || config.model || 'opus';
531
531
 
532
532
  // Create unified run/analysis ID and DB record immediately
533
533
  const runId = uuidv4();
@@ -676,7 +676,7 @@ function createMCPServer(db, options = {}) {
676
676
  // Resolve provider and model
677
677
  const repoSettings = await repoSettingsRepo.getRepoSettings(repository);
678
678
  const provider = process.env.PAIR_REVIEW_PROVIDER || repoSettings?.default_provider || config.default_provider || config.provider || 'claude';
679
- const model = process.env.PAIR_REVIEW_MODEL || repoSettings?.default_model || config.default_model || config.model || 'sonnet';
679
+ const model = process.env.PAIR_REVIEW_MODEL || repoSettings?.default_model || config.default_model || config.model || 'opus';
680
680
 
681
681
  // Create unified run/analysis ID and DB record immediately
682
682
  const runId = uuidv4();
@@ -70,7 +70,7 @@ function getLocalReviewKey(reviewId) {
70
70
 
71
71
  /**
72
72
  * Get the model to use for AI analysis
73
- * Priority: CLI flag (PAIR_REVIEW_MODEL env var) > config.default_model > 'sonnet' default
73
+ * Priority: CLI flag (PAIR_REVIEW_MODEL env var) > config.default_model > 'opus' default
74
74
  * @param {Object} req - Express request object
75
75
  * @returns {string} Model name to use
76
76
  */
@@ -93,7 +93,7 @@ function getModel(req) {
93
93
  }
94
94
 
95
95
  // Default fallback
96
- return 'sonnet';
96
+ return 'opus';
97
97
  }
98
98
 
99
99
  /**