@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.
- package/README.md +25 -1
- package/package.json +15 -20
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/skills/analyze/scripts/git-diff-lines +0 -0
- package/public/css/pr.css +762 -6
- package/public/js/components/AIPanel.js +486 -42
- package/public/js/components/ChatPanel.js +2002 -528
- package/public/js/modules/comment-minimizer.js +66 -20
- package/public/js/modules/external-comment-manager.js +870 -0
- package/public/js/pr.js +297 -20
- package/public/local.html +21 -5
- package/public/pr.html +31 -5
- package/src/ai/claude-provider.js +68 -56
- package/src/ai/codex-provider.js +64 -33
- package/src/chat/api-reference.js +1 -1
- package/src/chat/chat-providers.js +90 -12
- package/src/chat/codex-bridge.js +238 -29
- package/src/chat/session-manager.js +1 -0
- package/src/config.js +2 -1
- package/src/database.js +566 -2
- package/src/external/github-adapter.js +152 -0
- package/src/external/index.js +37 -0
- package/src/github/client.js +77 -1
- package/src/main.js +3 -2
- package/src/routes/config.js +27 -0
- package/src/routes/external-comments.js +394 -0
- package/src/server.js +9 -0
package/src/ai/codex-provider.js
CHANGED
|
@@ -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: '
|
|
49
|
-
badgeClass: 'badge-
|
|
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.
|
|
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
|
|
153
|
-
//
|
|
154
|
-
//
|
|
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'
|
|
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,
|
|
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'
|
|
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.
|
|
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-
|
|
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
|
-
*
|
|
201
|
-
*
|
|
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
|
-
|
|
228
|
-
|
|
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
|
|
308
|
+
shell,
|
|
309
|
+
env: { ...process.env, ...(env || {}) },
|
|
234
310
|
});
|
|
235
311
|
|
|
236
|
-
proc.
|
|
312
|
+
proc.once('error', (err) => {
|
|
237
313
|
resolve({ available: false, error: err.message });
|
|
238
314
|
});
|
|
239
315
|
|
|
240
|
-
proc.
|
|
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: `${
|
|
322
|
+
resolve({ available: false, error: `${displayCommand} exited with code ${code}` });
|
|
245
323
|
}
|
|
246
324
|
});
|
|
247
325
|
} catch (err) {
|