@in-the-loop-labs/pair-review 3.7.2 → 3.9.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.
- package/README.md +188 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/js/local.js +28 -8
- package/public/js/pr.js +92 -7
- package/public/js/utils/provider-model.js +9 -2
- package/public/setup.html +8 -0
- package/src/ai/claude-provider.js +31 -30
- package/src/ai/codex-provider.js +4 -3
- package/src/ai/copilot-provider.js +15 -1
- package/src/ai/cursor-agent-provider.js +4 -3
- package/src/ai/executable-provider.js +12 -4
- package/src/ai/gemini-provider.js +15 -1
- package/src/ai/index.js +14 -0
- package/src/ai/opencode-provider.js +15 -1
- package/src/ai/pi-provider.js +4 -3
- package/src/ai/provider.js +294 -38
- package/src/chat/chat-providers.js +30 -9
- package/src/councils/headless-council.js +128 -0
- package/src/councils/resolve-council.js +167 -0
- package/src/database.js +52 -0
- package/src/git/worktree.js +7 -1
- package/src/interactive-analysis-config.js +152 -0
- package/src/local-review.js +54 -23
- package/src/main.js +1125 -25
- package/src/mcp-stdio.js +40 -5
- package/src/review-config.js +164 -0
- package/src/routes/bulk-analysis-configs.js +45 -15
- package/src/routes/config.js +9 -4
- package/src/routes/executable-analysis.js +10 -1
- package/src/routes/local.js +170 -109
- package/src/routes/mcp.js +28 -31
- package/src/routes/pr.js +118 -56
- package/src/single-port.js +131 -5
- package/src/utils/logger.js +25 -4
package/src/ai/codex-provider.js
CHANGED
|
@@ -763,9 +763,10 @@ class CodexProvider extends AIProvider {
|
|
|
763
763
|
* Test if Codex CLI is available
|
|
764
764
|
* Uses fast `--version` check instead of running a prompt.
|
|
765
765
|
* Uses the command configured in the instance (respects ENV > config > default precedence)
|
|
766
|
+
* @param {number} [timeoutMs=10000] - Timeout in milliseconds for the probe
|
|
766
767
|
* @returns {Promise<boolean>}
|
|
767
768
|
*/
|
|
768
|
-
async testAvailability() {
|
|
769
|
+
async testAvailability(timeoutMs = 10000) {
|
|
769
770
|
return new Promise((resolve) => {
|
|
770
771
|
// For availability test, we just need to check --version
|
|
771
772
|
// Use the already-resolved command from the constructor (this.codexCmd)
|
|
@@ -794,10 +795,10 @@ class CodexProvider extends AIProvider {
|
|
|
794
795
|
const availabilityTimeout = setTimeout(() => {
|
|
795
796
|
if (settled) return;
|
|
796
797
|
settled = true;
|
|
797
|
-
logger.warn(
|
|
798
|
+
logger.warn(`Codex CLI availability check timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
798
799
|
try { codex.kill(); } catch { /* ignore */ }
|
|
799
800
|
resolve(false);
|
|
800
|
-
},
|
|
801
|
+
}, timeoutMs);
|
|
801
802
|
|
|
802
803
|
codex.stdout.on('data', (data) => {
|
|
803
804
|
stdout += data.toString();
|
|
@@ -439,9 +439,11 @@ class CopilotProvider extends AIProvider {
|
|
|
439
439
|
/**
|
|
440
440
|
* Test if Copilot CLI is available
|
|
441
441
|
* Uses the command configured in the instance (respects ENV > config > default precedence)
|
|
442
|
+
* @param {number} [timeoutMs=10000] - Timeout in milliseconds for the probe.
|
|
443
|
+
* Production passes the per-provider resolved value; the default is only hit by tests.
|
|
442
444
|
* @returns {Promise<boolean>}
|
|
443
445
|
*/
|
|
444
|
-
async testAvailability() {
|
|
446
|
+
async testAvailability(timeoutMs = 10000) {
|
|
445
447
|
return new Promise((resolve) => {
|
|
446
448
|
// For availability test, check --version
|
|
447
449
|
// Use the already-resolved command from the constructor (this.copilotCmd)
|
|
@@ -465,6 +467,16 @@ class CopilotProvider extends AIProvider {
|
|
|
465
467
|
let stdout = '';
|
|
466
468
|
let settled = false;
|
|
467
469
|
|
|
470
|
+
// Timeout guard: if the CLI hangs, kill it and resolve false so the probe
|
|
471
|
+
// does not leak a child process.
|
|
472
|
+
const availabilityTimeout = setTimeout(() => {
|
|
473
|
+
if (settled) return;
|
|
474
|
+
settled = true;
|
|
475
|
+
logger.warn(`Copilot CLI availability check timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
476
|
+
try { copilot.kill(); } catch { /* ignore */ }
|
|
477
|
+
resolve(false);
|
|
478
|
+
}, timeoutMs);
|
|
479
|
+
|
|
468
480
|
copilot.stdout.on('data', (data) => {
|
|
469
481
|
stdout += data.toString();
|
|
470
482
|
});
|
|
@@ -472,6 +484,7 @@ class CopilotProvider extends AIProvider {
|
|
|
472
484
|
copilot.on('close', (code) => {
|
|
473
485
|
if (settled) return;
|
|
474
486
|
settled = true;
|
|
487
|
+
clearTimeout(availabilityTimeout);
|
|
475
488
|
// Copilot CLI typically outputs version info on success
|
|
476
489
|
if (code === 0) {
|
|
477
490
|
logger.info(`Copilot CLI available: ${stdout.trim()}`);
|
|
@@ -485,6 +498,7 @@ class CopilotProvider extends AIProvider {
|
|
|
485
498
|
copilot.on('error', (error) => {
|
|
486
499
|
if (settled) return;
|
|
487
500
|
settled = true;
|
|
501
|
+
clearTimeout(availabilityTimeout);
|
|
488
502
|
logger.warn(`Copilot CLI not available: ${error.message}`);
|
|
489
503
|
resolve(false);
|
|
490
504
|
});
|
|
@@ -783,9 +783,10 @@ class CursorAgentProvider extends AIProvider {
|
|
|
783
783
|
/**
|
|
784
784
|
* Test if Cursor Agent CLI is available
|
|
785
785
|
* Uses the command configured in the instance (respects ENV > config > default precedence)
|
|
786
|
+
* @param {number} [timeoutMs=10000] - Timeout in milliseconds for the probe
|
|
786
787
|
* @returns {Promise<boolean>}
|
|
787
788
|
*/
|
|
788
|
-
async testAvailability() {
|
|
789
|
+
async testAvailability(timeoutMs = 10000) {
|
|
789
790
|
return new Promise((resolve) => {
|
|
790
791
|
// For availability test, we just need to check --version
|
|
791
792
|
// Use the already-resolved command from the constructor (this.agentCmd)
|
|
@@ -813,10 +814,10 @@ class CursorAgentProvider extends AIProvider {
|
|
|
813
814
|
const availabilityTimeout = setTimeout(() => {
|
|
814
815
|
if (settled) return;
|
|
815
816
|
settled = true;
|
|
816
|
-
logger.warn(
|
|
817
|
+
logger.warn(`Cursor Agent CLI availability check timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
817
818
|
try { agent.kill(); } catch { /* ignore */ }
|
|
818
819
|
resolve(false);
|
|
819
|
-
},
|
|
820
|
+
}, timeoutMs);
|
|
820
821
|
|
|
821
822
|
agent.stdout.on('data', (data) => {
|
|
822
823
|
stdout += data.toString();
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
const providerModule = require('./provider');
|
|
15
|
-
const { AIProvider, resolveDefaultModel, inferModelDefaults } = providerModule;
|
|
15
|
+
const { AIProvider, resolveDefaultModel, inferModelDefaults, secondsToTimeoutMs } = providerModule;
|
|
16
16
|
const { spawn } = require('child_process');
|
|
17
17
|
const { glob } = require('glob');
|
|
18
18
|
const fs = require('fs').promises;
|
|
@@ -136,6 +136,10 @@ function createExecutableProviderClass(id, config) {
|
|
|
136
136
|
this.mappingInstructions = config.mapping_instructions || '';
|
|
137
137
|
this.timeout = config.timeout || 600000; // Default 10 minutes
|
|
138
138
|
this.availabilityCommand = config.availability_command || 'true';
|
|
139
|
+
// Availability-probe timeout. Configured in seconds (mirrors
|
|
140
|
+
// checkout_timeout_seconds); falls back to the shared default when
|
|
141
|
+
// unset/invalid.
|
|
142
|
+
this.availabilityTimeoutMs = secondsToTimeoutMs(config.availability_timeout_seconds);
|
|
139
143
|
this.extraEnv = {
|
|
140
144
|
...(config.env || {}),
|
|
141
145
|
...(configOverrides.env || {}),
|
|
@@ -454,9 +458,13 @@ function createExecutableProviderClass(id, config) {
|
|
|
454
458
|
* Test if the external tool is available.
|
|
455
459
|
* Runs the configured availability_command (defaults to 'true', i.e. always available).
|
|
456
460
|
*
|
|
461
|
+
* @param {number} [timeoutMs] - Timeout in milliseconds for the probe.
|
|
462
|
+
* Defaults to the provider's configured availability_timeout_seconds
|
|
463
|
+
* (or 10s). Tools with slow build-based availability commands can raise
|
|
464
|
+
* this via `availability_timeout_seconds` in their provider config.
|
|
457
465
|
* @returns {Promise<boolean>}
|
|
458
466
|
*/
|
|
459
|
-
async testAvailability() {
|
|
467
|
+
async testAvailability(timeoutMs = this.availabilityTimeoutMs) {
|
|
460
468
|
return new Promise((resolve) => {
|
|
461
469
|
const command = this.availabilityCommand;
|
|
462
470
|
logger.debug(`${id} availability check: ${command}`);
|
|
@@ -471,10 +479,10 @@ function createExecutableProviderClass(id, config) {
|
|
|
471
479
|
const availabilityTimeout = setTimeout(() => {
|
|
472
480
|
if (settled) return;
|
|
473
481
|
settled = true;
|
|
474
|
-
logger.warn(`${id} availability check timed out after
|
|
482
|
+
logger.warn(`${id} availability check timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
475
483
|
try { child.kill(); } catch { /* ignore */ }
|
|
476
484
|
resolve(false);
|
|
477
|
-
},
|
|
485
|
+
}, timeoutMs);
|
|
478
486
|
|
|
479
487
|
child.on('close', (code) => {
|
|
480
488
|
if (settled) return;
|
|
@@ -659,9 +659,11 @@ class GeminiProvider extends AIProvider {
|
|
|
659
659
|
/**
|
|
660
660
|
* Test if Gemini CLI is available
|
|
661
661
|
* Uses the command configured in the instance (respects ENV > config > default precedence)
|
|
662
|
+
* @param {number} [timeoutMs=10000] - Timeout in milliseconds for the probe.
|
|
663
|
+
* Production passes the per-provider resolved value; the default is only hit by tests.
|
|
662
664
|
* @returns {Promise<boolean>}
|
|
663
665
|
*/
|
|
664
|
-
async testAvailability() {
|
|
666
|
+
async testAvailability(timeoutMs = 10000) {
|
|
665
667
|
return new Promise((resolve) => {
|
|
666
668
|
// For availability test, we just need to check --version
|
|
667
669
|
// Use the already-resolved command from the constructor (this.geminiCmd)
|
|
@@ -685,6 +687,16 @@ class GeminiProvider extends AIProvider {
|
|
|
685
687
|
let stdout = '';
|
|
686
688
|
let settled = false;
|
|
687
689
|
|
|
690
|
+
// Timeout guard: if the CLI hangs, kill it and resolve false so the probe
|
|
691
|
+
// does not leak a child process.
|
|
692
|
+
const availabilityTimeout = setTimeout(() => {
|
|
693
|
+
if (settled) return;
|
|
694
|
+
settled = true;
|
|
695
|
+
logger.warn(`Gemini CLI availability check timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
696
|
+
try { gemini.kill(); } catch { /* ignore */ }
|
|
697
|
+
resolve(false);
|
|
698
|
+
}, timeoutMs);
|
|
699
|
+
|
|
688
700
|
gemini.stdout.on('data', (data) => {
|
|
689
701
|
stdout += data.toString();
|
|
690
702
|
});
|
|
@@ -692,6 +704,7 @@ class GeminiProvider extends AIProvider {
|
|
|
692
704
|
gemini.on('close', (code) => {
|
|
693
705
|
if (settled) return;
|
|
694
706
|
settled = true;
|
|
707
|
+
clearTimeout(availabilityTimeout);
|
|
695
708
|
if (code === 0 && stdout.includes('.')) {
|
|
696
709
|
logger.info(`Gemini CLI available: ${stdout.trim()}`);
|
|
697
710
|
resolve(true);
|
|
@@ -704,6 +717,7 @@ class GeminiProvider extends AIProvider {
|
|
|
704
717
|
gemini.on('error', (error) => {
|
|
705
718
|
if (settled) return;
|
|
706
719
|
settled = true;
|
|
720
|
+
clearTimeout(availabilityTimeout);
|
|
707
721
|
logger.warn(`Gemini CLI not available: ${error.message}`);
|
|
708
722
|
resolve(false);
|
|
709
723
|
});
|
package/src/ai/index.js
CHANGED
|
@@ -17,10 +17,17 @@ const {
|
|
|
17
17
|
getAllProvidersInfo,
|
|
18
18
|
createProvider,
|
|
19
19
|
testProviderAvailability,
|
|
20
|
+
resolveAvailabilityTimeoutMs,
|
|
21
|
+
secondsToTimeoutMs,
|
|
22
|
+
DEFAULT_AVAILABILITY_TIMEOUT_MS,
|
|
20
23
|
applyConfigOverrides,
|
|
21
24
|
getProviderConfigOverrides,
|
|
22
25
|
inferModelDefaults,
|
|
23
26
|
resolveDefaultModel,
|
|
27
|
+
modelMatches,
|
|
28
|
+
mergeModels,
|
|
29
|
+
applyModelOverrides,
|
|
30
|
+
normalizeDisabledModels,
|
|
24
31
|
prettifyModelId,
|
|
25
32
|
createAliasedProviderClass,
|
|
26
33
|
getTierForModel
|
|
@@ -69,12 +76,19 @@ module.exports = {
|
|
|
69
76
|
|
|
70
77
|
// Utilities
|
|
71
78
|
testProviderAvailability,
|
|
79
|
+
resolveAvailabilityTimeoutMs,
|
|
80
|
+
secondsToTimeoutMs,
|
|
81
|
+
DEFAULT_AVAILABILITY_TIMEOUT_MS,
|
|
72
82
|
|
|
73
83
|
// Config override support
|
|
74
84
|
applyConfigOverrides,
|
|
75
85
|
getProviderConfigOverrides,
|
|
76
86
|
inferModelDefaults,
|
|
77
87
|
resolveDefaultModel,
|
|
88
|
+
modelMatches,
|
|
89
|
+
mergeModels,
|
|
90
|
+
applyModelOverrides,
|
|
91
|
+
normalizeDisabledModels,
|
|
78
92
|
prettifyModelId,
|
|
79
93
|
getTierForModel,
|
|
80
94
|
|
|
@@ -587,9 +587,11 @@ class OpenCodeProvider extends AIProvider {
|
|
|
587
587
|
/**
|
|
588
588
|
* Test if OpenCode CLI is available
|
|
589
589
|
* Uses the command configured in the instance (respects ENV > config > default precedence)
|
|
590
|
+
* @param {number} [timeoutMs=10000] - Timeout in milliseconds for the probe.
|
|
591
|
+
* Production passes the per-provider resolved value; the default is only hit by tests.
|
|
590
592
|
* @returns {Promise<boolean>}
|
|
591
593
|
*/
|
|
592
|
-
async testAvailability() {
|
|
594
|
+
async testAvailability(timeoutMs = 10000) {
|
|
593
595
|
return new Promise((resolve) => {
|
|
594
596
|
// For availability test, we just need to check --version
|
|
595
597
|
// Use the already-resolved command from the constructor (this.opencodeCmd)
|
|
@@ -613,6 +615,16 @@ class OpenCodeProvider extends AIProvider {
|
|
|
613
615
|
let stdout = '';
|
|
614
616
|
let settled = false;
|
|
615
617
|
|
|
618
|
+
// Timeout guard: if the CLI hangs, kill it and resolve false so the probe
|
|
619
|
+
// does not leak a child process.
|
|
620
|
+
const availabilityTimeout = setTimeout(() => {
|
|
621
|
+
if (settled) return;
|
|
622
|
+
settled = true;
|
|
623
|
+
logger.warn(`OpenCode CLI availability check timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
624
|
+
try { opencode.kill(); } catch { /* ignore */ }
|
|
625
|
+
resolve(false);
|
|
626
|
+
}, timeoutMs);
|
|
627
|
+
|
|
616
628
|
opencode.stdout.on('data', (data) => {
|
|
617
629
|
stdout += data.toString();
|
|
618
630
|
});
|
|
@@ -620,6 +632,7 @@ class OpenCodeProvider extends AIProvider {
|
|
|
620
632
|
opencode.on('close', (code) => {
|
|
621
633
|
if (settled) return;
|
|
622
634
|
settled = true;
|
|
635
|
+
clearTimeout(availabilityTimeout);
|
|
623
636
|
if (code === 0) {
|
|
624
637
|
logger.info(`OpenCode CLI available: ${stdout.trim()}`);
|
|
625
638
|
resolve(true);
|
|
@@ -632,6 +645,7 @@ class OpenCodeProvider extends AIProvider {
|
|
|
632
645
|
opencode.on('error', (error) => {
|
|
633
646
|
if (settled) return;
|
|
634
647
|
settled = true;
|
|
648
|
+
clearTimeout(availabilityTimeout);
|
|
635
649
|
logger.warn(`OpenCode CLI not available: ${error.message}`);
|
|
636
650
|
resolve(false);
|
|
637
651
|
});
|
package/src/ai/pi-provider.js
CHANGED
|
@@ -1103,9 +1103,10 @@ class PiProvider extends AIProvider {
|
|
|
1103
1103
|
/**
|
|
1104
1104
|
* Test if Pi CLI is available
|
|
1105
1105
|
* Uses the command configured in the instance (respects ENV > config > default precedence)
|
|
1106
|
+
* @param {number} [timeoutMs=10000] - Timeout in milliseconds for the probe
|
|
1106
1107
|
* @returns {Promise<boolean>}
|
|
1107
1108
|
*/
|
|
1108
|
-
async testAvailability() {
|
|
1109
|
+
async testAvailability(timeoutMs = 10000) {
|
|
1109
1110
|
return new Promise((resolve) => {
|
|
1110
1111
|
// For availability test, we just need to check --version
|
|
1111
1112
|
// Use the already-resolved command from the constructor (this.piCmd)
|
|
@@ -1135,10 +1136,10 @@ class PiProvider extends AIProvider {
|
|
|
1135
1136
|
const availabilityTimeout = setTimeout(() => {
|
|
1136
1137
|
if (settled) return;
|
|
1137
1138
|
settled = true;
|
|
1138
|
-
logger.warn(`${name} CLI availability check timed out after
|
|
1139
|
+
logger.warn(`${name} CLI availability check timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
1139
1140
|
try { pi.kill(); } catch { /* ignore */ }
|
|
1140
1141
|
resolve(false);
|
|
1141
|
-
},
|
|
1142
|
+
}, timeoutMs);
|
|
1142
1143
|
|
|
1143
1144
|
pi.stdout.on('data', (data) => {
|
|
1144
1145
|
stdout += data.toString();
|