@in-the-loop-labs/pair-review 3.2.3 → 3.3.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.
@@ -198,6 +198,22 @@ Examples:
198
198
  </div>
199
199
  </section>
200
200
 
201
+ <!-- Provider Skill Discovery Section -->
202
+ <section class="settings-section">
203
+ <div class="section-header">
204
+ <h2>Provider Skill Discovery</h2>
205
+ <p class="section-description">Controls whether AI providers load environment-level skills automatically. Currently applies to Pi's skill auto-discovery. When disabled, Pi runs with <code>--no-skills</code>.</p>
206
+ </div>
207
+ <div class="form-group" style="max-width: 300px;">
208
+ <label for="load-skills-select" class="settings-label">Load Skills</label>
209
+ <select class="settings-select" id="load-skills-select">
210
+ <option value="">Default (inherit from config)</option>
211
+ <option value="1">Enabled</option>
212
+ <option value="0">Disabled</option>
213
+ </select>
214
+ </div>
215
+ </section>
216
+
201
217
  <!-- Repository Location Section -->
202
218
  <section class="settings-section" id="local-path-section">
203
219
  <div class="section-header">
@@ -227,6 +243,19 @@ Examples:
227
243
  </div>
228
244
  </section>
229
245
 
246
+ <!-- Worktrees Section -->
247
+ <section class="settings-section" id="worktrees-section" style="display: none;">
248
+ <div class="section-header">
249
+ <h2>Worktrees</h2>
250
+ <p class="section-description">
251
+ Manage worktree directories used for reviewing pull requests in this repository.
252
+ </p>
253
+ </div>
254
+ <div id="worktrees-content">
255
+ <!-- Rendered dynamically by JS -->
256
+ </div>
257
+ </section>
258
+
230
259
  <!-- Danger Zone -->
231
260
  <section class="settings-section danger-zone">
232
261
  <div class="section-header">
@@ -73,9 +73,10 @@ async function captureDiffSnapshot(analyzer, worktreePath, prMetadata, logPrefix
73
73
  * @param {Object|null} instructions - Instructions object { repoInstructions, requestInstructions }
74
74
  * @param {Function|null} progressCallback - Parent progress callback to wrap
75
75
  * @param {Object} db - Database instance
76
+ * @param {Object} providerOverrides - Per-call config overrides passed to createProvider (optional)
76
77
  * @returns {Object} { voiceAnalyzer, voiceKey, reviewerLabel, voiceRequestInstructions, voiceProgressCallback, voiceTier, voiceTimeout }
77
78
  */
78
- function buildVoiceContext(voice, idx, instructions, progressCallback, db) {
79
+ function buildVoiceContext(voice, idx, instructions, progressCallback, db, providerOverrides = {}, providerOverridesMap = null) {
79
80
  const voiceKey = `${voice.provider}-${voice.model}${idx > 0 ? `-${idx}` : ''}`;
80
81
  const reviewerLabel = buildReviewerLabel(idx, voice);
81
82
 
@@ -87,13 +88,16 @@ function buildVoiceContext(voice, idx, instructions, progressCallback, db) {
87
88
  : voice.customInstructions;
88
89
  }
89
90
 
91
+ // Resolve per-voice overrides: prefer provider-specific from map, fall back to shared overrides
92
+ const effectiveOverrides = providerOverridesMap?.[voice.provider] || providerOverrides;
93
+
90
94
  const ProviderClass = getProviderClass(voice.provider);
91
95
  const isExecutable = ProviderClass?.isExecutable || false;
92
96
 
93
97
  // Only create Analyzer for native voices
94
- const voiceAnalyzer = isExecutable ? null : new Analyzer(db, voice.model, voice.provider);
98
+ const voiceAnalyzer = isExecutable ? null : new Analyzer(db, voice.model, voice.provider, effectiveOverrides);
95
99
  // Create provider instance for executable voices (used directly)
96
- const voiceProvider = isExecutable ? createProvider(voice.provider, voice.model) : null;
100
+ const voiceProvider = isExecutable ? createProvider(voice.provider, voice.model, effectiveOverrides) : null;
97
101
 
98
102
  const voiceTier = voice.tier || 'balanced';
99
103
  const voiceTimeout = voice.timeout || ProviderClass?.defaultTimeout || 600000;
@@ -272,12 +276,16 @@ class Analyzer {
272
276
  * @param {Object} database - Database instance
273
277
  * @param {string} model - Model to use (e.g., 'opus', 'gemini-2.5-pro')
274
278
  * @param {string} provider - Provider ID (e.g., 'claude', 'gemini'). Defaults to 'claude'.
279
+ * @param {Object} providerOverrides - Per-call config overrides passed to createProvider (optional)
280
+ * @param {Object|null} providerOverridesMap - Per-provider overrides map for council mode (provider ID → overrides)
275
281
  */
276
- constructor(database, model = 'opus', provider = 'claude') {
282
+ constructor(database, model = 'opus', provider = 'claude', providerOverrides = {}, providerOverridesMap = null) {
277
283
  // Store model and provider for creating provider instances per level
278
284
  this.model = model;
279
285
  this.provider = provider;
280
286
  this.db = database;
287
+ this.providerOverrides = providerOverrides;
288
+ this.providerOverridesMap = providerOverridesMap;
281
289
  this.testContextCache = new Map(); // Cache test detection results per worktree
282
290
  this._worktreeManager = null; // Lazy-initialized for sparse-checkout queries
283
291
  }
@@ -1015,7 +1023,7 @@ Or simply ignore any changes to files matching these patterns in your analysis.
1015
1023
  }
1016
1024
 
1017
1025
  // Create provider instance for this level
1018
- const aiProvider = createProvider(this.provider, this.model);
1026
+ const aiProvider = createProvider(this.provider, this.model, this.providerOverrides);
1019
1027
 
1020
1028
  const updateProgress = (step) => {
1021
1029
  const progress = `${lp}[Level 1] ${step}...`;
@@ -1975,7 +1983,7 @@ If you are unsure, use "NEW" - it is correct for the vast majority of suggestion
1975
1983
  }
1976
1984
 
1977
1985
  // Create provider instance for this level
1978
- const aiProvider = createProvider(this.provider, this.model);
1986
+ const aiProvider = createProvider(this.provider, this.model, this.providerOverrides);
1979
1987
 
1980
1988
  const updateProgress = (step) => {
1981
1989
  const progress = `${lp}[Level 2] ${step}...`;
@@ -2085,7 +2093,7 @@ If you are unsure, use "NEW" - it is correct for the vast majority of suggestion
2085
2093
  }
2086
2094
 
2087
2095
  // Create provider instance for this level
2088
- const aiProvider = createProvider(this.provider, this.model);
2096
+ const aiProvider = createProvider(this.provider, this.model, this.providerOverrides);
2089
2097
 
2090
2098
  const updateProgress = (step) => {
2091
2099
  const progress = `${lp}[Level 3] ${step}...`;
@@ -2655,7 +2663,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
2655
2663
  }
2656
2664
 
2657
2665
  // Create provider instance for consolidation (use overrides if provided)
2658
- const aiProvider = createProvider(providerOverride || this.provider, modelOverride || this.model);
2666
+ const aiProvider = createProvider(providerOverride || this.provider, modelOverride || this.model, this.providerOverrides);
2659
2667
 
2660
2668
  // Build the consolidation prompt
2661
2669
  const prompt = this.buildOrchestrationPrompt(allSuggestions, prMetadata, customInstructions, worktreePath, tier, lp, { excludePrevious, dedupContext });
@@ -2932,7 +2940,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
2932
2940
  if (voices.length === 1) {
2933
2941
  const voice = voices[0];
2934
2942
  const { voiceAnalyzer, voiceProvider, isExecutable, voiceKey, reviewerLabel, voiceRequestInstructions, voiceProgressCallback, voiceTier, voiceTimeout } =
2935
- buildVoiceContext(voice, 0, instructions, progressCallback, this.db);
2943
+ buildVoiceContext(voice, 0, instructions, progressCallback, this.db, this.providerOverrides, this.providerOverridesMap);
2936
2944
  logger.info(`[ReviewerCouncil] Single reviewer (${reviewerLabel}) — running directly on parent run, no child run`);
2937
2945
 
2938
2946
  // Report voice-centric progress structure
@@ -3034,7 +3042,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3034
3042
  const commentRepo = new CommentRepository(this.db);
3035
3043
  const voicePromises = voices.map(async (voice, idx) => {
3036
3044
  const { voiceAnalyzer, voiceProvider, isExecutable, voiceKey, reviewerLabel, voiceRequestInstructions, voiceProgressCallback, voiceTier, voiceTimeout } =
3037
- buildVoiceContext(voice, idx, instructions, progressCallback, this.db);
3045
+ buildVoiceContext(voice, idx, instructions, progressCallback, this.db, this.providerOverrides, this.providerOverridesMap);
3038
3046
  const childRunId = uuidv4();
3039
3047
 
3040
3048
  // Create child analysis run record
@@ -3271,7 +3279,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3271
3279
 
3272
3280
  const consolidated = await this._crossVoiceConsolidate(
3273
3281
  voiceReviews, prMetadata, consolInstructions, worktreePath,
3274
- { provider: consolProvider, model: consolModel, tier: consolTier, timeout: consolConfig.timeout, analysisId, progressCallback, excludePrevious, dedupContext }
3282
+ { provider: consolProvider, model: consolModel, tier: consolTier, timeout: consolConfig.timeout, analysisId, progressCallback, excludePrevious, dedupContext, providerOverrides: this.providerOverrides }
3275
3283
  );
3276
3284
 
3277
3285
  const finalSuggestions = this.validateAndFinalizeSuggestions(
@@ -3410,7 +3418,8 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3410
3418
  tier,
3411
3419
  timeout: voice.timeout || VoiceProviderClass?.defaultTimeout || 600000,
3412
3420
  customInstructions: voiceInstructions,
3413
- voiceCustomInstructions: voice.customInstructions || null
3421
+ voiceCustomInstructions: voice.customInstructions || null,
3422
+ providerOverrides: this.providerOverridesMap?.[voice.provider] || this.providerOverrides
3414
3423
  });
3415
3424
  }
3416
3425
  }
@@ -3575,7 +3584,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3575
3584
  }));
3576
3585
  const consolidated = await this._intraLevelConsolidate(
3577
3586
  level, voiceGroups, prMetadata, orchInstructions, worktreePath,
3578
- { provider: orchProvider, model: orchModel, tier: orchTier, timeout: orchConfig.timeout, analysisId, progressCallback, reviewerCount: successfulVoicesForLevel.length }
3587
+ { provider: orchProvider, model: orchModel, tier: orchTier, timeout: orchConfig.timeout, analysisId, progressCallback, reviewerCount: successfulVoicesForLevel.length, providerOverrides: this.providerOverrides }
3579
3588
  );
3580
3589
  consolidatedPerLevel[level] = consolidated;
3581
3590
  // Report intra-level consolidation step as completed
@@ -3661,7 +3670,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3661
3670
  * @private
3662
3671
  */
3663
3672
  async _executeCouncilVoice(task, context) {
3664
- const { voiceId, reviewerLabel, reviewerLogPrefix, level, provider, model, tier, timeout = 600000, customInstructions } = task;
3673
+ const { voiceId, reviewerLabel, reviewerLogPrefix, level, provider, model, tier, timeout = 600000, customInstructions, providerOverrides } = task;
3665
3674
  const { reviewId, runId, worktreePath, prMetadata, generatedPatterns, validFiles, analysisId, progressCallback } = context;
3666
3675
  const displayLabel = reviewerLabel || voiceId;
3667
3676
 
@@ -3672,7 +3681,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3672
3681
  }
3673
3682
 
3674
3683
  // Create provider instance for this voice
3675
- const aiProvider = createProvider(provider, model);
3684
+ const aiProvider = createProvider(provider, model, providerOverrides || {});
3676
3685
 
3677
3686
  // Build prompt based on level
3678
3687
  let prompt;
@@ -3748,9 +3757,9 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3748
3757
  * @private
3749
3758
  */
3750
3759
  async _intraLevelConsolidate(level, voiceGroups, prMetadata, customInstructions, worktreePath, orchConfig) {
3751
- const { provider, model, tier, timeout, analysisId, progressCallback, reviewerCount } = orchConfig;
3760
+ const { provider, model, tier, timeout, analysisId, progressCallback, reviewerCount, providerOverrides } = orchConfig;
3752
3761
 
3753
- const aiProvider = createProvider(provider, model);
3762
+ const aiProvider = createProvider(provider, model, providerOverrides || {});
3754
3763
 
3755
3764
  const isLocal = prMetadata.reviewType === 'local';
3756
3765
  const reviewDescription = isLocal
@@ -3916,9 +3925,9 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3916
3925
  * @private
3917
3926
  */
3918
3927
  async _crossVoiceConsolidate(voiceReviews, prMetadata, customInstructions, worktreePath, config) {
3919
- const { provider, model, tier, timeout, analysisId, progressCallback, excludePrevious, dedupContext } = config;
3928
+ const { provider, model, tier, timeout, analysisId, progressCallback, excludePrevious, dedupContext, providerOverrides } = config;
3920
3929
 
3921
- const aiProvider = createProvider(provider, model);
3930
+ const aiProvider = createProvider(provider, model, providerOverrides || {});
3922
3931
 
3923
3932
  const voiceDescriptions = voiceReviews.map(v => {
3924
3933
  let desc = `### Reviewer: ${v.voiceKey}`;
@@ -53,6 +53,8 @@ class ClaudeCLI {
53
53
  });
54
54
 
55
55
  const pid = claude.pid;
56
+ const fullCommand = this.useShell ? this.command : `${this.command} ${this.args.join(' ')}`;
57
+ logger.debug(`${levelPrefix} Claude CLI command: ${fullCommand}`);
56
58
  logger.info(`${levelPrefix} Spawned Claude CLI process: PID ${pid}`);
57
59
 
58
60
  let stdout = '';
@@ -257,7 +257,8 @@ class ClaudeProvider extends AIProvider {
257
257
  logger.info(`${levelPrefix} Executing Claude CLI...`);
258
258
  logger.info(`${levelPrefix} Writing prompt: ${prompt.length} bytes`);
259
259
 
260
- const claude = spawn(this.command, this.args, {
260
+ const spawnArgs = [...this.args];
261
+ const claude = spawn(this.command, spawnArgs, {
261
262
  cwd,
262
263
  env: {
263
264
  ...process.env,
@@ -268,6 +269,8 @@ class ClaudeProvider extends AIProvider {
268
269
  });
269
270
 
270
271
  const pid = claude.pid;
272
+ const fullCommand = this.useShell ? this.command : `${this.command} ${spawnArgs.join(' ')}`;
273
+ logger.debug(`${levelPrefix} Claude CLI command: ${fullCommand}`);
271
274
  logger.info(`${levelPrefix} Spawned Claude CLI process: PID ${pid}`);
272
275
 
273
276
  // Register process for cancellation tracking if analysisId provided
@@ -671,10 +671,11 @@ function getAllProvidersInfo() {
671
671
  * Create a provider instance
672
672
  * @param {string} providerId - Provider ID (e.g., 'claude', 'gemini')
673
673
  * @param {string} model - Model to use (optional, uses default if not specified)
674
+ * @param {Object} overrides - Per-call config overrides that supersede global providerConfigOverrides (optional)
674
675
  * @returns {AIProvider}
675
676
  * @throws {Error} If provider is not registered
676
677
  */
677
- function createProvider(providerId, model = null) {
678
+ function createProvider(providerId, model = null, overrides = {}) {
678
679
  const ProviderClass = providerRegistry.get(providerId);
679
680
 
680
681
  if (!ProviderClass) {
@@ -683,7 +684,7 @@ function createProvider(providerId, model = null) {
683
684
  }
684
685
 
685
686
  // Get config overrides for this provider
686
- const overrides = providerConfigOverrides.get(providerId);
687
+ const configOverrides = providerConfigOverrides.get(providerId);
687
688
 
688
689
  // Determine the actual model to use
689
690
  let actualModel = model;
@@ -691,8 +692,8 @@ function createProvider(providerId, model = null) {
691
692
  // Resolve default from merged models (config + built-in).
692
693
  // Checks both sources because some providers (e.g., Pi) define built-in
693
694
  // modes with default:true that aren't in config overrides.
694
- if (overrides?.models || ProviderClass.getModels().length > 0) {
695
- actualModel = resolveDefaultModel(mergeModels(ProviderClass.getModels(), overrides?.models));
695
+ if (configOverrides?.models || ProviderClass.getModels().length > 0) {
696
+ actualModel = resolveDefaultModel(mergeModels(ProviderClass.getModels(), configOverrides?.models));
696
697
  }
697
698
  // Fall back to provider's built-in default
698
699
  if (!actualModel) {
@@ -700,8 +701,8 @@ function createProvider(providerId, model = null) {
700
701
  }
701
702
  }
702
703
 
703
- // Create provider instance with config overrides
704
- return new ProviderClass(actualModel, { ...(overrides || {}), yolo: yoloMode });
704
+ // Create provider instance with config overrides, per-call overrides, and yolo mode
705
+ return new ProviderClass(actualModel, { ...(configOverrides || {}), ...overrides, yolo: yoloMode });
705
706
  }
706
707
 
707
708
  /**
@@ -43,7 +43,7 @@ class ChatSessionManager {
43
43
  * @param {string} [options.initialContext] - Initial context to prepend to the first user message
44
44
  * @returns {Promise<{id: number, status: string}>}
45
45
  */
46
- async createSession({ provider, model, reviewId, contextCommentId, systemPrompt, cwd, initialContext }) {
46
+ async createSession({ provider, model, reviewId, contextCommentId, systemPrompt, cwd, initialContext, loadSkills }) {
47
47
  // Resolve provider definition once — used for model fallback and bridge construction
48
48
  const providerDef = getChatProvider(provider);
49
49
 
@@ -72,6 +72,7 @@ class ChatSessionManager {
72
72
  model: resolvedModel,
73
73
  cwd,
74
74
  systemPrompt,
75
+ loadSkills,
75
76
  }, providerDef);
76
77
 
77
78
  const listeners = {
@@ -413,9 +414,10 @@ class ChatSessionManager {
413
414
  * @param {Object} options
414
415
  * @param {string} [options.systemPrompt] - System prompt text
415
416
  * @param {string} [options.cwd] - Working directory for agent
417
+ * @param {boolean} [options.loadSkills] - Resolved load_skills override for the session
416
418
  * @returns {Promise<{id: number, status: string}>}
417
419
  */
418
- async resumeSession(sessionId, { systemPrompt, cwd } = {}) {
420
+ async resumeSession(sessionId, { systemPrompt, cwd, loadSkills } = {}) {
419
421
  // Already active — return immediately
420
422
  if (this._sessions.has(sessionId)) {
421
423
  return { id: sessionId, status: 'active' };
@@ -459,6 +461,7 @@ class ChatSessionManager {
459
461
  model: row.model,
460
462
  cwd,
461
463
  systemPrompt,
464
+ loadSkills,
462
465
  ...resumeOptions,
463
466
  });
464
467
 
@@ -588,7 +591,7 @@ class ChatSessionManager {
588
591
  useShell: def?.useShell,
589
592
  tools: CHAT_TOOLS,
590
593
  extensions: appExtensions ? [taskExtensionDir] : [],
591
- loadSkills: def?.load_skills,
594
+ loadSkills: options.loadSkills ?? def?.load_skills,
592
595
  });
593
596
  }
594
597