@in-the-loop-labs/pair-review 3.2.3 → 3.3.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 +7 -6
- package/package.json +5 -4
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/repo-settings.css +347 -0
- package/public/index.html +46 -9
- package/public/js/components/AIPanel.js +79 -37
- package/public/js/components/DiffOptionsDropdown.js +84 -1
- package/public/js/index.js +31 -6
- package/public/js/pr.js +22 -0
- package/public/js/repo-settings.js +334 -6
- package/public/repo-settings.html +29 -0
- package/src/ai/analyzer.js +28 -19
- package/src/ai/claude-cli.js +2 -0
- package/src/ai/claude-provider.js +4 -1
- package/src/ai/provider.js +7 -6
- package/src/chat/session-manager.js +6 -3
- package/src/config.js +230 -38
- package/src/database.js +766 -38
- package/src/git/worktree-pool-lifecycle.js +679 -0
- package/src/git/worktree-pool-usage.js +216 -0
- package/src/git/worktree.js +157 -32
- package/src/main.js +185 -26
- package/src/routes/analyses.js +48 -26
- package/src/routes/chat.js +27 -3
- package/src/routes/config.js +17 -5
- package/src/routes/executable-analysis.js +38 -19
- package/src/routes/local.js +19 -6
- package/src/routes/mcp.js +13 -2
- package/src/routes/pr.js +72 -29
- package/src/routes/setup.js +41 -4
- package/src/routes/stack-analysis.js +29 -10
- package/src/routes/worktrees.js +294 -9
- package/src/server.js +20 -3
- package/src/setup/pr-setup.js +161 -27
- package/src/ws/server.js +51 -1
package/src/config.js
CHANGED
|
@@ -34,7 +34,7 @@ const DEFAULT_CONFIG = {
|
|
|
34
34
|
chat: { enable_shortcuts: true, enter_to_send: true }, // Chat panel settings (enable_shortcuts: show action shortcut buttons, enter_to_send: Enter sends message instead of newline)
|
|
35
35
|
providers: {}, // Custom AI analysis provider configurations (overrides built-in defaults)
|
|
36
36
|
chat_providers: {}, // Custom chat provider configurations (overrides built-in defaults)
|
|
37
|
-
|
|
37
|
+
repos: {}, // Repository configurations: { "owner/repo": { path: "~/path/to/clone" } }
|
|
38
38
|
assisted_by_url: "https://github.com/in-the-loop-labs/pair-review", // URL for "Review assisted by" footer link
|
|
39
39
|
hooks: {}, // Hook commands per event: { "review.started": { "my_hook": { "command": "..." } } }
|
|
40
40
|
enable_graphite: false, // When true, shows Graphite links alongside GitHub links
|
|
@@ -221,6 +221,20 @@ async function loadConfig() {
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
// Normalize legacy monorepos key into repos (monorepos values are overridden by repos)
|
|
225
|
+
if (mergedConfig.monorepos) {
|
|
226
|
+
mergedConfig.repos = deepMerge(mergedConfig.monorepos, mergedConfig.repos);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Normalize repo keys to lowercase to match the database's COLLATE NOCASE identity
|
|
230
|
+
if (mergedConfig.repos) {
|
|
231
|
+
const normalized = {};
|
|
232
|
+
for (const [key, value] of Object.entries(mergedConfig.repos)) {
|
|
233
|
+
normalized[key.toLowerCase()] = value;
|
|
234
|
+
}
|
|
235
|
+
mergedConfig.repos = normalized;
|
|
236
|
+
}
|
|
237
|
+
|
|
224
238
|
// Validate port
|
|
225
239
|
if (!validatePort(mergedConfig.port)) {
|
|
226
240
|
console.error(`Invalid port number ${mergedConfig.port}`);
|
|
@@ -393,83 +407,248 @@ function expandPath(p) {
|
|
|
393
407
|
}
|
|
394
408
|
|
|
395
409
|
/**
|
|
396
|
-
*
|
|
410
|
+
* Get repository configuration, checking `repos` key first, falling back to `monorepos`.
|
|
411
|
+
* @param {object} config
|
|
412
|
+
* @param {string} repository - owner/repo
|
|
413
|
+
* @returns {object|null}
|
|
414
|
+
*/
|
|
415
|
+
function getRepoConfig(config, repository) {
|
|
416
|
+
const reposSection = config.repos || {};
|
|
417
|
+
const entry = reposSection[repository];
|
|
418
|
+
if (entry) return entry;
|
|
419
|
+
|
|
420
|
+
const legacySection = config.monorepos || {};
|
|
421
|
+
return legacySection[repository] || null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Gets the configured repository path
|
|
397
426
|
* @param {Object} config - Configuration object from loadConfig()
|
|
398
427
|
* @param {string} repository - Repository in "owner/repo" format
|
|
399
428
|
* @returns {string|null} - Expanded path or null if not configured
|
|
400
429
|
*/
|
|
401
|
-
function
|
|
402
|
-
const
|
|
403
|
-
if (
|
|
404
|
-
return expandPath(
|
|
430
|
+
function getRepoPath(config, repository) {
|
|
431
|
+
const repoConfig = getRepoConfig(config, repository);
|
|
432
|
+
if (repoConfig?.path) {
|
|
433
|
+
return expandPath(repoConfig.path);
|
|
405
434
|
}
|
|
406
435
|
return null;
|
|
407
436
|
}
|
|
408
437
|
|
|
409
438
|
/**
|
|
410
|
-
* Gets the configured checkout script for a
|
|
439
|
+
* Gets the configured checkout script for a repository
|
|
411
440
|
* @param {Object} config - Configuration object from loadConfig()
|
|
412
441
|
* @param {string} repository - Repository in "owner/repo" format
|
|
413
442
|
* @returns {string|null} - Checkout script path or null if not configured
|
|
414
443
|
*/
|
|
415
|
-
function
|
|
416
|
-
const
|
|
417
|
-
return
|
|
444
|
+
function getRepoCheckoutScript(config, repository) {
|
|
445
|
+
const repoConfig = getRepoConfig(config, repository);
|
|
446
|
+
return repoConfig?.checkout_script || null;
|
|
418
447
|
}
|
|
419
448
|
|
|
420
449
|
/**
|
|
421
|
-
* Gets the configured worktree directory for a
|
|
450
|
+
* Gets the configured worktree directory for a repository
|
|
422
451
|
* @param {Object} config - Configuration object from loadConfig()
|
|
423
452
|
* @param {string} repository - Repository in "owner/repo" format
|
|
424
453
|
* @returns {string|null} - Expanded worktree directory path or null if not configured
|
|
425
454
|
*/
|
|
426
|
-
function
|
|
427
|
-
const
|
|
428
|
-
if (
|
|
429
|
-
return expandPath(
|
|
455
|
+
function getRepoWorktreeDirectory(config, repository) {
|
|
456
|
+
const repoConfig = getRepoConfig(config, repository);
|
|
457
|
+
if (repoConfig?.worktree_directory) {
|
|
458
|
+
return expandPath(repoConfig.worktree_directory);
|
|
430
459
|
}
|
|
431
460
|
return null;
|
|
432
461
|
}
|
|
433
462
|
|
|
434
463
|
/**
|
|
435
|
-
* Gets the configured worktree name template for a
|
|
464
|
+
* Gets the configured worktree name template for a repository
|
|
436
465
|
* @param {Object} config - Configuration object from loadConfig()
|
|
437
466
|
* @param {string} repository - Repository in "owner/repo" format
|
|
438
467
|
* @returns {string|null} - Template string or null if not configured
|
|
439
468
|
*/
|
|
440
|
-
function
|
|
441
|
-
const
|
|
442
|
-
return
|
|
469
|
+
function getRepoWorktreeNameTemplate(config, repository) {
|
|
470
|
+
const repoConfig = getRepoConfig(config, repository);
|
|
471
|
+
return repoConfig?.worktree_name_template || null;
|
|
443
472
|
}
|
|
444
473
|
|
|
445
474
|
/**
|
|
446
|
-
*
|
|
475
|
+
* Computes the display name for a worktree path by deriving the relative
|
|
476
|
+
* path from the configured (or default) worktree base directory.
|
|
477
|
+
* Falls back to the basename when the path lies outside the base directory.
|
|
478
|
+
*
|
|
479
|
+
* @param {string} worktreePath - Absolute path to the worktree
|
|
480
|
+
* @param {Object} config - Configuration object from loadConfig()
|
|
481
|
+
* @param {string} repository - Repository in "owner/repo" format
|
|
482
|
+
* @returns {string|null} - Relative display name (e.g. "abc123/src") or basename fallback
|
|
483
|
+
*/
|
|
484
|
+
function getWorktreeDisplayName(worktreePath, config, repository) {
|
|
485
|
+
if (!worktreePath) return null;
|
|
486
|
+
const worktreeBaseDir = getRepoWorktreeDirectory(config, repository)
|
|
487
|
+
|| path.join(getConfigDir(), 'worktrees');
|
|
488
|
+
const relativePath = path.relative(worktreeBaseDir, worktreePath);
|
|
489
|
+
if (relativePath.startsWith('..')) {
|
|
490
|
+
return path.basename(worktreePath);
|
|
491
|
+
}
|
|
492
|
+
return relativePath;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Gets the configured checkout script timeout for a repository
|
|
447
497
|
* @param {Object} config - Configuration object from loadConfig()
|
|
448
498
|
* @param {string} repository - Repository in "owner/repo" format
|
|
449
499
|
* @returns {number} - Timeout in milliseconds (default: 300000 = 5 minutes)
|
|
450
500
|
*/
|
|
451
|
-
function
|
|
452
|
-
const
|
|
453
|
-
if (
|
|
454
|
-
return
|
|
501
|
+
function getRepoCheckoutTimeout(config, repository) {
|
|
502
|
+
const repoConfig = getRepoConfig(config, repository);
|
|
503
|
+
if (repoConfig?.checkout_timeout_seconds > 0) {
|
|
504
|
+
return repoConfig.checkout_timeout_seconds * 1000;
|
|
455
505
|
}
|
|
456
506
|
return DEFAULT_CHECKOUT_TIMEOUT_MS; // 5 minutes default
|
|
457
507
|
}
|
|
458
508
|
|
|
459
509
|
/**
|
|
460
|
-
*
|
|
510
|
+
* Gets the configured reset script for a repository
|
|
511
|
+
* @param {Object} config - Configuration object from loadConfig()
|
|
512
|
+
* @param {string} repository - Repository in "owner/repo" format
|
|
513
|
+
* @returns {string|null} - Reset script path or null if not configured
|
|
514
|
+
*/
|
|
515
|
+
function getRepoResetScript(config, repository) {
|
|
516
|
+
const repoConfig = getRepoConfig(config, repository);
|
|
517
|
+
return repoConfig?.reset_script || null;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Gets the configured pool size for a repository from file config only.
|
|
522
|
+
* Prefer resolvePoolConfig() when DB repo_settings are available.
|
|
523
|
+
* @param {Object} config - Configuration object from loadConfig()
|
|
524
|
+
* @param {string} repository - Repository in "owner/repo" format
|
|
525
|
+
* @returns {number} - Pool size (0 if not configured or invalid)
|
|
526
|
+
*/
|
|
527
|
+
function getRepoPoolSize(config, repository) {
|
|
528
|
+
const repoConfig = getRepoConfig(config, repository);
|
|
529
|
+
const size = repoConfig?.pool_size;
|
|
530
|
+
return (typeof size === 'number' && size > 0) ? size : 0;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Gets the configured pool fetch interval for a repository from file config only.
|
|
535
|
+
* Prefer resolvePoolConfig() when DB repo_settings are available.
|
|
536
|
+
* @param {Object} config - Configuration object from loadConfig()
|
|
537
|
+
* @param {string} repository - Repository in "owner/repo" format
|
|
538
|
+
* @returns {number|null} - Interval in minutes or null if not configured
|
|
539
|
+
*/
|
|
540
|
+
function getRepoPoolFetchInterval(config, repository) {
|
|
541
|
+
const repoConfig = getRepoConfig(config, repository);
|
|
542
|
+
const minutes = repoConfig?.pool_fetch_interval_minutes;
|
|
543
|
+
return (typeof minutes === 'number' && minutes > 0) ? minutes : null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Gets the configured load_skills setting for a repository from file config.
|
|
548
|
+
* @param {Object} config - Configuration object from loadConfig()
|
|
549
|
+
* @param {string} repository - Repository in "owner/repo" format
|
|
550
|
+
* @returns {boolean|null} - true/false if set, null if not configured
|
|
551
|
+
*/
|
|
552
|
+
function getRepoLoadSkills(config, repository) {
|
|
553
|
+
const repoConfig = getRepoConfig(config, repository);
|
|
554
|
+
const val = repoConfig?.load_skills;
|
|
555
|
+
return typeof val === 'boolean' ? val : null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Resolves the load_skills setting for a repository, checking DB repo_settings first,
|
|
560
|
+
* then repo JSON config, then provider config. Returns a boolean suitable for passing
|
|
561
|
+
* directly to provider constructors (which check `!== false`).
|
|
562
|
+
*
|
|
563
|
+
* @param {Object} config - Configuration object from loadConfig()
|
|
564
|
+
* @param {string} repository - Repository in "owner/repo" format
|
|
565
|
+
* @param {Object|null} repoSettings - DB repo_settings row (from RepoSettingsRepository.getRepoSettings)
|
|
566
|
+
* @param {boolean} [providerLoadSkills] - Provider-level load_skills from config.providers
|
|
567
|
+
* @returns {boolean} - Resolved load_skills value
|
|
568
|
+
*/
|
|
569
|
+
function resolveLoadSkills(config, repository, repoSettings, providerLoadSkills) {
|
|
570
|
+
// Tier 1: DB repo settings (1 = true, 0 = false, null = not set)
|
|
571
|
+
const dbVal = repoSettings?.load_skills;
|
|
572
|
+
if (typeof dbVal === 'number' && (dbVal === 0 || dbVal === 1)) {
|
|
573
|
+
return dbVal === 1;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Tier 2: Repo JSON config (config.repos["owner/repo"].load_skills)
|
|
577
|
+
const repoVal = getRepoLoadSkills(config, repository);
|
|
578
|
+
if (repoVal !== null) {
|
|
579
|
+
return repoVal;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Tier 3: Provider-level config
|
|
583
|
+
if (typeof providerLoadSkills === 'boolean') {
|
|
584
|
+
return providerLoadSkills;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Tier 4: Default
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Builds council-mode provider overrides: a shared (tier 1+2) base and a per-provider
|
|
593
|
+
* map that includes tier 3 resolution for each configured provider.
|
|
594
|
+
*
|
|
595
|
+
* @param {Object} config - Configuration object from loadConfig()
|
|
596
|
+
* @param {string} repository - Repository in "owner/repo" format
|
|
597
|
+
* @param {Object|null} repoSettings - DB repo_settings row
|
|
598
|
+
* @returns {{ providerOverrides: Object, providerOverridesMap: Object }}
|
|
599
|
+
*/
|
|
600
|
+
function buildCouncilProviderOverrides(config, repository, repoSettings) {
|
|
601
|
+
const baseLoadSkills = resolveLoadSkills(config, repository, repoSettings);
|
|
602
|
+
const providerOverrides = { load_skills: baseLoadSkills };
|
|
603
|
+
const providerOverridesMap = {};
|
|
604
|
+
if (config.providers) {
|
|
605
|
+
for (const [pid, pconf] of Object.entries(config.providers)) {
|
|
606
|
+
providerOverridesMap[pid] = {
|
|
607
|
+
load_skills: resolveLoadSkills(config, repository, repoSettings, pconf?.load_skills)
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return { providerOverrides, providerOverridesMap };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Resolves pool configuration for a repository, checking DB repo_settings first,
|
|
616
|
+
* then falling back to file config. DB values take precedence when set (non-null).
|
|
617
|
+
* @param {Object} config - Configuration object from loadConfig()
|
|
618
|
+
* @param {string} repository - Repository in "owner/repo" format
|
|
619
|
+
* @param {Object|null} repoSettings - DB repo_settings row (from RepoSettingsRepository.getRepoSettings)
|
|
620
|
+
* @returns {{ poolSize: number, poolFetchIntervalMinutes: number|null }}
|
|
621
|
+
*/
|
|
622
|
+
function resolvePoolConfig(config, repository, repoSettings) {
|
|
623
|
+
const dbPoolSize = repoSettings?.pool_size;
|
|
624
|
+
const dbFetchInterval = repoSettings?.pool_fetch_interval_minutes;
|
|
625
|
+
|
|
626
|
+
const poolSize = (typeof dbPoolSize === 'number' && dbPoolSize >= 0)
|
|
627
|
+
? dbPoolSize
|
|
628
|
+
: getRepoPoolSize(config, repository);
|
|
629
|
+
|
|
630
|
+
const poolFetchIntervalMinutes = (typeof dbFetchInterval === 'number' && dbFetchInterval >= 0)
|
|
631
|
+
? (dbFetchInterval > 0 ? dbFetchInterval : null)
|
|
632
|
+
: getRepoPoolFetchInterval(config, repository);
|
|
633
|
+
|
|
634
|
+
return { poolSize, poolFetchIntervalMinutes };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Resolves all repository worktree options into a single object.
|
|
461
639
|
* Composite helper that combines the individual getters into the shape expected
|
|
462
640
|
* by GitWorktreeManager and createWorktreeForPR.
|
|
463
641
|
*
|
|
464
642
|
* @param {Object} config - Configuration object from loadConfig()
|
|
465
643
|
* @param {string} repository - Repository in "owner/repo" format
|
|
466
|
-
* @
|
|
644
|
+
* @param {Object|null} [repoSettings=null] - DB repo_settings row (from RepoSettingsRepository.getRepoSettings)
|
|
645
|
+
* @returns {{ checkoutScript: string|null, checkoutTimeout: number, worktreeConfig: Object|null, resetScript: string|null, poolSize: number, poolFetchIntervalMinutes: number|null }}
|
|
467
646
|
*/
|
|
468
|
-
function
|
|
469
|
-
const checkoutScript =
|
|
470
|
-
const checkoutTimeout =
|
|
471
|
-
const worktreeDirectory =
|
|
472
|
-
const nameTemplate =
|
|
647
|
+
function resolveRepoOptions(config, repository, repoSettings = null) {
|
|
648
|
+
const checkoutScript = getRepoCheckoutScript(config, repository);
|
|
649
|
+
const checkoutTimeout = getRepoCheckoutTimeout(config, repository);
|
|
650
|
+
const worktreeDirectory = getRepoWorktreeDirectory(config, repository);
|
|
651
|
+
const nameTemplate = getRepoWorktreeNameTemplate(config, repository);
|
|
473
652
|
|
|
474
653
|
let worktreeConfig = null;
|
|
475
654
|
if (worktreeDirectory || nameTemplate) {
|
|
@@ -478,7 +657,10 @@ function resolveMonorepoOptions(config, repository) {
|
|
|
478
657
|
if (nameTemplate) worktreeConfig.nameTemplate = nameTemplate;
|
|
479
658
|
}
|
|
480
659
|
|
|
481
|
-
|
|
660
|
+
const resetScript = getRepoResetScript(config, repository);
|
|
661
|
+
const { poolSize, poolFetchIntervalMinutes } = resolvePoolConfig(config, repository, repoSettings);
|
|
662
|
+
|
|
663
|
+
return { checkoutScript, checkoutTimeout, worktreeConfig, resetScript, poolSize, poolFetchIntervalMinutes };
|
|
482
664
|
}
|
|
483
665
|
|
|
484
666
|
/**
|
|
@@ -558,12 +740,22 @@ module.exports = {
|
|
|
558
740
|
isRunningViaNpx,
|
|
559
741
|
showWelcomeMessage,
|
|
560
742
|
expandPath,
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
743
|
+
// New repo-prefixed names
|
|
744
|
+
getRepoConfig,
|
|
745
|
+
getRepoPath,
|
|
746
|
+
getRepoCheckoutScript,
|
|
747
|
+
getRepoWorktreeDirectory,
|
|
748
|
+
getRepoWorktreeNameTemplate,
|
|
749
|
+
getWorktreeDisplayName,
|
|
750
|
+
getRepoCheckoutTimeout,
|
|
751
|
+
resolveRepoOptions,
|
|
752
|
+
getRepoResetScript,
|
|
753
|
+
getRepoPoolSize,
|
|
754
|
+
getRepoPoolFetchInterval,
|
|
755
|
+
getRepoLoadSkills,
|
|
756
|
+
resolvePoolConfig,
|
|
757
|
+
resolveLoadSkills,
|
|
758
|
+
buildCouncilProviderOverrides,
|
|
567
759
|
resolveDbName,
|
|
568
760
|
warnIfDevModeWithoutDbName,
|
|
569
761
|
shouldSkipUpdateNotifier,
|