@in-the-loop-labs/pair-review 3.0.6 → 3.1.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.
Files changed (81) hide show
  1. package/package.json +2 -1
  2. package/plugin/.claude-plugin/plugin.json +1 -1
  3. package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
  4. package/plugin-code-critic/skills/analyze/references/level1-balanced.md +8 -0
  5. package/plugin-code-critic/skills/analyze/references/level1-fast.md +7 -0
  6. package/plugin-code-critic/skills/analyze/references/level1-thorough.md +8 -0
  7. package/plugin-code-critic/skills/analyze/references/level2-balanced.md +9 -0
  8. package/plugin-code-critic/skills/analyze/references/level2-fast.md +8 -0
  9. package/plugin-code-critic/skills/analyze/references/level2-thorough.md +9 -0
  10. package/plugin-code-critic/skills/analyze/references/level3-balanced.md +9 -0
  11. package/plugin-code-critic/skills/analyze/references/level3-fast.md +8 -0
  12. package/plugin-code-critic/skills/analyze/references/level3-thorough.md +9 -0
  13. package/plugin-code-critic/skills/analyze/references/orchestration-balanced.md +9 -0
  14. package/plugin-code-critic/skills/analyze/references/orchestration-fast.md +5 -0
  15. package/plugin-code-critic/skills/analyze/references/orchestration-thorough.md +9 -0
  16. package/public/css/analysis-config.css +103 -0
  17. package/public/css/pr.css +191 -4
  18. package/public/index.html +20 -0
  19. package/public/js/components/AIPanel.js +1 -1
  20. package/public/js/components/AdvancedConfigTab.js +87 -9
  21. package/public/js/components/AnalysisConfigModal.js +206 -5
  22. package/public/js/components/ChatPanel.js +22 -5
  23. package/public/js/components/CouncilProgressModal.js +241 -23
  24. package/public/js/components/TimeoutSelect.js +2 -0
  25. package/public/js/components/VoiceCentricConfigTab.js +183 -13
  26. package/public/js/index.js +119 -1
  27. package/public/js/local.js +166 -51
  28. package/public/js/modules/suggestion-manager.js +2 -1
  29. package/public/js/pr.js +71 -12
  30. package/public/js/repo-settings.js +2 -2
  31. package/public/local.html +32 -11
  32. package/public/pr.html +2 -0
  33. package/src/ai/analyzer.js +371 -111
  34. package/src/ai/claude-provider.js +2 -0
  35. package/src/ai/codex-provider.js +1 -1
  36. package/src/ai/copilot-provider.js +2 -0
  37. package/src/ai/executable-provider.js +538 -0
  38. package/src/ai/gemini-provider.js +2 -0
  39. package/src/ai/index.js +9 -1
  40. package/src/ai/pi-provider.js +10 -8
  41. package/src/ai/prompts/baseline/consolidation/balanced.js +54 -2
  42. package/src/ai/prompts/baseline/consolidation/fast.js +31 -1
  43. package/src/ai/prompts/baseline/consolidation/thorough.js +46 -3
  44. package/src/ai/prompts/baseline/level1/balanced.js +12 -0
  45. package/src/ai/prompts/baseline/level1/fast.js +11 -0
  46. package/src/ai/prompts/baseline/level1/thorough.js +12 -0
  47. package/src/ai/prompts/baseline/level2/balanced.js +13 -0
  48. package/src/ai/prompts/baseline/level2/fast.js +12 -0
  49. package/src/ai/prompts/baseline/level2/thorough.js +13 -0
  50. package/src/ai/prompts/baseline/level3/balanced.js +13 -0
  51. package/src/ai/prompts/baseline/level3/fast.js +12 -0
  52. package/src/ai/prompts/baseline/level3/thorough.js +13 -0
  53. package/src/ai/prompts/baseline/orchestration/balanced.js +15 -0
  54. package/src/ai/prompts/baseline/orchestration/fast.js +11 -0
  55. package/src/ai/prompts/baseline/orchestration/thorough.js +15 -0
  56. package/src/ai/prompts/render-for-skill.js +3 -0
  57. package/src/ai/prompts/shared/output-schema.js +8 -0
  58. package/src/ai/provider.js +91 -4
  59. package/src/chat/prompt-builder.js +39 -4
  60. package/src/chat/session-manager.js +32 -28
  61. package/src/config.js +15 -2
  62. package/src/database.js +59 -15
  63. package/src/git/base-branch.js +113 -29
  64. package/src/github/parser.js +1 -1
  65. package/src/local-review.js +15 -9
  66. package/src/local-scope.js +83 -0
  67. package/src/main.js +3 -2
  68. package/src/routes/analyses.js +34 -8
  69. package/src/routes/chat.js +15 -8
  70. package/src/routes/config.js +3 -120
  71. package/src/routes/councils.js +15 -6
  72. package/src/routes/executable-analysis.js +494 -0
  73. package/src/routes/local.js +152 -15
  74. package/src/routes/mcp.js +9 -4
  75. package/src/routes/pr.js +166 -29
  76. package/src/routes/reviews.js +31 -5
  77. package/src/routes/shared.js +72 -5
  78. package/src/routes/worktrees.js +4 -2
  79. package/src/utils/comment-formatter.js +28 -11
  80. package/src/utils/instructions.js +22 -8
  81. package/src/utils/logger.js +20 -10
@@ -23,6 +23,11 @@ class CouncilProgressModal {
23
23
  // Track per-voice completion state
24
24
  this._voiceStates = {};
25
25
 
26
+ // Track which voice keys are executable providers.
27
+ // Populated from progress events (levels.exec.voices) and persisted
28
+ // across show()/hide() cycles so reopened modals render correctly.
29
+ this._executableVoices = new Set();
30
+
26
31
  this._createModal();
27
32
  this._setupEventListeners();
28
33
  }
@@ -54,9 +59,12 @@ class CouncilProgressModal {
54
59
  // Detect rendering mode
55
60
  const configType = options.configType || (councilConfig ? 'advanced' : 'single');
56
61
  this._renderMode = configType;
62
+ this._noLevels = options.noLevels || false;
57
63
 
58
64
  // Rebuild DOM based on mode
59
- if (configType === 'single') {
65
+ if (configType === 'single' && this._noLevels) {
66
+ this._rebuildBodyNoLevels();
67
+ } else if (configType === 'single') {
60
68
  const enabledLevels = options.enabledLevels || [1, 2, 3];
61
69
  this._rebuildBodySingleModel(enabledLevels);
62
70
  } else if (configType === 'council') {
@@ -239,11 +247,34 @@ class CouncilProgressModal {
239
247
  * @param {Object} status
240
248
  */
241
249
  updateProgress(status) {
250
+ // No-levels mode: update the single row based on overall status
251
+ if (this._noLevels) {
252
+ this._updateNoLevelsProgress(status);
253
+ // Terminal states
254
+ if (status.status === 'completed') {
255
+ this._handleCompletion(status);
256
+ } else if (status.status === 'failed') {
257
+ this._handleFailure(status);
258
+ } else if (status.status === 'cancelled') {
259
+ this._handleCancellation(status);
260
+ }
261
+ return;
262
+ }
263
+
242
264
  if (!status.levels || typeof status.levels !== 'object') {
243
265
  console.warn('Council progress: invalid status structure', status);
244
266
  return;
245
267
  }
246
268
 
269
+ // Track executable voice keys from exec level data so that reopened
270
+ // modals can render executable reviewers correctly without relying on
271
+ // the config modal's provider cache (which may be empty on reopen).
272
+ if (status.levels.exec?.voices) {
273
+ for (const voiceKey of Object.keys(status.levels.exec.voices)) {
274
+ this._executableVoices.add(voiceKey);
275
+ }
276
+ }
277
+
247
278
  if (this._renderMode === 'single') {
248
279
  // Single-model: update level headers directly
249
280
  for (let level = 1; level <= 3; level++) {
@@ -292,6 +323,63 @@ class CouncilProgressModal {
292
323
  }
293
324
  }
294
325
 
326
+ // ---------------------------------------------------------------------------
327
+ // No-levels progress
328
+ // ---------------------------------------------------------------------------
329
+
330
+ /**
331
+ * Update the single row for no-levels providers.
332
+ * Maps overall analysis status to the single "Running analysis..." row.
333
+ */
334
+ _updateNoLevelsProgress(status) {
335
+ const header = this.modal.querySelector('.council-level-header[data-level="analysis"]');
336
+ if (!header) return;
337
+
338
+ const iconEl = header.querySelector('.council-level-icon');
339
+ const statusEl = header.querySelector('.council-level-status');
340
+ if (!iconEl || !statusEl) return;
341
+
342
+ // Derive state from overall status or from any level status
343
+ let state = 'pending';
344
+ if (status.status === 'running' || status.status === 'in_progress') {
345
+ state = 'running';
346
+ } else if (status.status === 'completed') {
347
+ state = 'completed';
348
+ } else if (status.status === 'failed') {
349
+ state = 'failed';
350
+ } else if (status.status === 'cancelled') {
351
+ state = 'cancelled';
352
+ } else if (status.levels) {
353
+ // Fall back to checking individual levels
354
+ const levelValues = Object.values(status.levels);
355
+ if (levelValues.some(l => l.status === 'running')) state = 'running';
356
+ else if (levelValues.some(l => l.status === 'completed')) state = 'running';
357
+ }
358
+
359
+ this._renderState(iconEl, statusEl, state, 'council-level');
360
+
361
+ // Show stream event text in the snippet element
362
+ const levelEl = header.closest('.council-level');
363
+ const snippetEl = levelEl?.querySelector('.council-level-snippet');
364
+ if (snippetEl) {
365
+ // Check for stream events in any level
366
+ let streamText = null;
367
+ if (status.levels) {
368
+ for (const lvl of Object.values(status.levels)) {
369
+ if (lvl.streamEvent?.text) {
370
+ streamText = lvl.streamEvent.text;
371
+ }
372
+ }
373
+ }
374
+ if (state === 'running' && streamText) {
375
+ snippetEl.textContent = streamText;
376
+ snippetEl.style.display = 'block';
377
+ } else if (state !== 'running') {
378
+ snippetEl.style.display = 'none';
379
+ }
380
+ }
381
+ }
382
+
295
383
  // ---------------------------------------------------------------------------
296
384
  // Per-voice progress
297
385
  // ---------------------------------------------------------------------------
@@ -518,6 +606,55 @@ class CouncilProgressModal {
518
606
  }
519
607
  }
520
608
 
609
+ // Handle executable voice updates (level 'exec'):
610
+ // Executable voices emit progress with level: 'exec' instead of numeric 1/2/3.
611
+ // The single "Running analysis..." row uses data-vc-level="exec".
612
+ const execLevel = status.levels?.exec;
613
+ if (execLevel) {
614
+ if (execLevel.voices) {
615
+ // Check if any exec voice was rendered with native L1/L2/L3 structure
616
+ // instead of the single exec row. This happens when the modal was opened
617
+ // before _executableVoices was populated. If so, re-render the full body.
618
+ let needsRerender = false;
619
+ for (const voiceId of Object.keys(execLevel.voices)) {
620
+ const execEl = this.modal.querySelector(`[data-vc-voice="${voiceId}"][data-vc-level="exec"]`);
621
+ if (!execEl) {
622
+ // Voice exists in exec progress but has no exec DOM element — was likely
623
+ // rendered with native structure. Re-render to fix.
624
+ needsRerender = true;
625
+ break;
626
+ }
627
+ }
628
+ if (needsRerender && this.councilConfig) {
629
+ this._rebuildBodyVoiceCentric(this.councilConfig);
630
+ // Re-apply all progress to the fresh DOM
631
+ this._updateVoiceCentric(status);
632
+ return;
633
+ }
634
+
635
+ for (const [voiceId, vStatus] of Object.entries(execLevel.voices)) {
636
+ this._setVoiceCentricLevelState(voiceId, 'exec', vStatus.status || 'running', vStatus);
637
+ }
638
+ } else if (execLevel.status && execLevel.status !== 'pending') {
639
+ // Aggregate terminal update without per-voice breakdown (completion/cancellation
640
+ // paths may collapse exec progress to just { status, progress }). Propagate the
641
+ // status to every rendered exec row, mirroring the pattern used for numeric levels
642
+ // with shared terminal state in _updateVoiceFromLevelStatus.
643
+ const terminalState = execLevel.status;
644
+ const execEls = this.modal.querySelectorAll('[data-vc-level="exec"]');
645
+ execEls.forEach(el => {
646
+ const voiceKey = el.dataset.vcVoice;
647
+ const stateKey = `${voiceKey}:exec`;
648
+ if (this._voiceStates[stateKey] !== 'completed' && this._voiceStates[stateKey] !== 'failed') {
649
+ this._setVoiceCentricLevelState(voiceKey, 'exec', terminalState, execLevel);
650
+ }
651
+ });
652
+ }
653
+ if (execLevel.streamEvent?.text && execLevel.voiceId) {
654
+ this._setVoiceCentricStreamText(execLevel.voiceId, 'exec', execLevel.streamEvent.text);
655
+ }
656
+ }
657
+
521
658
  // Handle per-voice orchestration updates (level 4):
522
659
  // In voice-centric mode, each reviewer has a consolidation child at data-vc-level="4".
523
660
  // The backend tracks per-voice orchestration state in levels[4].voices, including
@@ -1073,6 +1210,26 @@ class CouncilProgressModal {
1073
1210
  }
1074
1211
  }
1075
1212
 
1213
+ // Executable voices don't require enabled levels — add any not already captured
1214
+ // (handles all-executable councils where no levels are enabled).
1215
+ // Use _executableVoices (populated from progress events) as primary source,
1216
+ // falling back to the config modal's provider cache. This ensures executable
1217
+ // voices render correctly even when the modal is reopened for an already-running
1218
+ // analysis where the config modal cache may be empty.
1219
+ if (config.voices) {
1220
+ const providersInfo = window.analysisConfigModal?.providers || {};
1221
+ for (const voice of config.voices) {
1222
+ const isExec = providersInfo[voice.provider]?.isExecutable || false;
1223
+ if (isExec) {
1224
+ const sig = `${voice.provider}|${voice.model}|${voice.tier || 'balanced'}|${voice.customInstructions || ''}`;
1225
+ if (!seenSignatures.has(sig)) {
1226
+ seenSignatures.add(sig);
1227
+ uniqueVoices.push(voice);
1228
+ }
1229
+ }
1230
+ }
1231
+ }
1232
+
1076
1233
  // Build voiceMap: voiceKey -> { voice, levels } using deduplicated array indices
1077
1234
  const voiceMap = new Map();
1078
1235
  uniqueVoices.forEach((voice, idx) => {
@@ -1080,11 +1237,33 @@ class CouncilProgressModal {
1080
1237
  voiceMap.set(voiceKey, { voice, levels: enabledLevelNums });
1081
1238
  });
1082
1239
 
1240
+ // Also add any executable voices known from progress events but not yet
1241
+ // in the voiceMap (covers the case where config modal providers cache is
1242
+ // empty but we've already seen exec-level progress data).
1243
+ for (const execVoiceKey of this._executableVoices) {
1244
+ if (!voiceMap.has(execVoiceKey)) {
1245
+ // Find the matching voice in config.voices by key pattern
1246
+ const configVoices = config.voices || [];
1247
+ const matchingVoice = configVoices.find((v, idx) => {
1248
+ const candidateKey = `${v.provider}-${v.model}${idx > 0 ? `-${idx}` : ''}`;
1249
+ return candidateKey === execVoiceKey;
1250
+ });
1251
+ if (matchingVoice) {
1252
+ voiceMap.set(execVoiceKey, { voice: matchingVoice, levels: enabledLevelNums });
1253
+ }
1254
+ }
1255
+ }
1256
+
1083
1257
  let html = '<div class="council-progress-tree">';
1084
1258
 
1085
- // Build a parent row for each unique voice
1259
+ // Build a parent row for each unique voice.
1260
+ // Determine executable status from: (1) _executableVoices set (populated from
1261
+ // progress events), or (2) config modal provider cache as fallback.
1262
+ const providersMap = window.analysisConfigModal?.providers || {};
1263
+
1086
1264
  for (const [voiceKey, { voice, levels }] of voiceMap) {
1087
- const label = this._formatVoiceLabel(voice);
1265
+ const isExecutable = this._executableVoices.has(voiceKey) || (providersMap[voice.provider]?.isExecutable || false);
1266
+ const label = this._formatVoiceLabel(voice, { isExecutable });
1088
1267
 
1089
1268
  html += `
1090
1269
  <div class="council-level" data-voice-key="${voiceKey}">
@@ -1096,31 +1275,46 @@ class CouncilProgressModal {
1096
1275
  <div class="council-level-children">
1097
1276
  `;
1098
1277
 
1099
- // Level children (orchestration row is always last, added separately below)
1100
- levels.forEach((levelNum) => {
1101
- const connectorClass = 'connector-mid';
1278
+ if (isExecutable) {
1279
+ // Executable voices: single "Running analysis..." row instead of L1/L2/L3
1102
1280
  html += `
1103
- <div class="council-voice ${connectorClass}" data-vc-voice="${voiceKey}" data-vc-level="${levelNum}">
1104
- <span class="council-voice-connector ${connectorClass}"></span>
1105
- <span class="council-voice-icon running"><span class="council-spinner"></span></span>
1106
- <span class="council-voice-label">Level ${levelNum} \u2014 ${levelNames[levelNum]}</span>
1107
- <span class="council-voice-status running">Running...</span>
1281
+ <div class="council-voice connector-last" data-vc-voice="${voiceKey}" data-vc-level="exec">
1282
+ <span class="council-voice-connector connector-last"></span>
1283
+ <span class="council-voice-icon pending">\u25CB</span>
1284
+ <span class="council-voice-label">Running analysis\u2026</span>
1285
+ <span class="council-voice-status pending">Pending</span>
1108
1286
  <div class="council-voice-detail">
1109
1287
  <div class="council-voice-snippet" style="display: none;"></div>
1110
1288
  </div>
1111
1289
  </div>
1112
1290
  `;
1113
- });
1291
+ } else {
1292
+ // Native voices: L1/L2/L3 children + orchestration
1293
+ levels.forEach((levelNum) => {
1294
+ const connectorClass = 'connector-mid';
1295
+ html += `
1296
+ <div class="council-voice ${connectorClass}" data-vc-voice="${voiceKey}" data-vc-level="${levelNum}">
1297
+ <span class="council-voice-connector ${connectorClass}"></span>
1298
+ <span class="council-voice-icon running"><span class="council-spinner"></span></span>
1299
+ <span class="council-voice-label">Level ${levelNum} \u2014 ${levelNames[levelNum]}</span>
1300
+ <span class="council-voice-status running">Running...</span>
1301
+ <div class="council-voice-detail">
1302
+ <div class="council-voice-snippet" style="display: none;"></div>
1303
+ </div>
1304
+ </div>
1305
+ `;
1306
+ });
1114
1307
 
1115
- // Orchestration child (always last)
1116
- html += `
1117
- <div class="council-voice connector-last" data-vc-voice="${voiceKey}" data-vc-level="4">
1118
- <span class="council-voice-connector connector-last"></span>
1119
- <span class="council-voice-icon pending">\u25CB</span>
1120
- <span class="council-voice-label">Consolidation</span>
1121
- <span class="council-voice-status pending">Pending</span>
1122
- </div>
1123
- `;
1308
+ // Orchestration child (always last)
1309
+ html += `
1310
+ <div class="council-voice connector-last" data-vc-voice="${voiceKey}" data-vc-level="4">
1311
+ <span class="council-voice-connector connector-last"></span>
1312
+ <span class="council-voice-icon pending">\u25CB</span>
1313
+ <span class="council-voice-label">Consolidation</span>
1314
+ <span class="council-voice-status pending">Pending</span>
1315
+ </div>
1316
+ `;
1317
+ }
1124
1318
 
1125
1319
  html += `
1126
1320
  </div>
@@ -1205,6 +1399,28 @@ class CouncilProgressModal {
1205
1399
  body.innerHTML = html;
1206
1400
  }
1207
1401
 
1402
+ /**
1403
+ * Rebuild the modal body for no-levels providers (e.g., executable providers).
1404
+ * Shows a single "Running analysis..." entry instead of the L1/L2/L3 breakdown.
1405
+ */
1406
+ _rebuildBodyNoLevels() {
1407
+ const body = this.modal.querySelector('.council-progress-body');
1408
+ if (!body) return;
1409
+
1410
+ body.innerHTML = `
1411
+ <div class="council-progress-tree">
1412
+ <div class="council-level" data-level="analysis">
1413
+ <div class="council-level-header" data-level="analysis">
1414
+ <span class="council-level-icon pending">\u25CB</span>
1415
+ <span class="council-level-title">Running analysis\u2026</span>
1416
+ <span class="council-level-status pending">Pending</span>
1417
+ </div>
1418
+ <div class="council-level-snippet" style="display: none;"></div>
1419
+ </div>
1420
+ </div>
1421
+ `;
1422
+ }
1423
+
1208
1424
  _resetFooter() {
1209
1425
  const bgBtn = this.modal.querySelector('.council-bg-btn');
1210
1426
  const cancelBtn = this.modal.querySelector('.council-cancel-btn');
@@ -1404,9 +1620,11 @@ class CouncilProgressModal {
1404
1620
  * Example: { provider: 'claude', model: 'sonnet-4-5', tier: 'balanced' }
1405
1621
  * -> "Claude sonnet-4-5 (Balanced)"
1406
1622
  */
1407
- _formatVoiceLabel(voice) {
1408
- const provider = this._capitalize(voice.provider || 'unknown');
1623
+ _formatVoiceLabel(voice, { isExecutable = false } = {}) {
1624
+ const providersMap = window.analysisConfigModal?.providers || {};
1625
+ const provider = providersMap[voice.provider]?.name || this._capitalize(voice.provider || 'unknown');
1409
1626
  const model = voice.model || 'default';
1627
+ if (isExecutable) return `${provider} ${model}`;
1410
1628
  const tier = this._capitalize(voice.tier || 'balanced');
1411
1629
  return `${provider} ${model} (${tier})`;
1412
1630
  }
@@ -37,6 +37,8 @@ class TimeoutSelect {
37
37
  { value: '600000', label: '10m', selected: true },
38
38
  { value: '900000', label: '15m' },
39
39
  { value: '1800000', label: '30m' },
40
+ { value: '2700000', label: '45m' },
41
+ { value: '3600000', label: '60m' },
40
42
  ];
41
43
 
42
44
  /**