@in-the-loop-labs/pair-review 3.3.3 → 3.3.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@in-the-loop-labs/pair-review",
3
- "version": "3.3.3",
3
+ "version": "3.3.5",
4
4
  "description": "Your AI-powered code review partner - Close the feedback loop with AI coding agents",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pair-review",
3
- "version": "3.3.3",
3
+ "version": "3.3.5",
4
4
  "description": "pair-review app integration — Open PRs and local changes in the pair-review web UI, run server-side AI analysis, and address review feedback. Requires the pair-review MCP server.",
5
5
  "author": {
6
6
  "name": "in-the-loop-labs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-critic",
3
- "version": "3.3.3",
3
+ "version": "3.3.5",
4
4
  "description": "AI-powered code review analysis — Run three-level AI analysis and implement-review-fix loops directly in your coding agent. Works standalone, no server required.",
5
5
  "author": {
6
6
  "name": "in-the-loop-labs",
package/public/css/pr.css CHANGED
@@ -1075,6 +1075,25 @@
1075
1075
  font-style: italic;
1076
1076
  }
1077
1077
 
1078
+ .file-viewed-icon-wrapper {
1079
+ display: inline-flex;
1080
+ align-items: center;
1081
+ flex-shrink: 0;
1082
+ margin-right: 4px;
1083
+ }
1084
+
1085
+ .file-viewed-icon {
1086
+ color: var(--color-fg-muted, #8b949e);
1087
+ vertical-align: middle;
1088
+ flex-shrink: 0;
1089
+ }
1090
+
1091
+ .file-item.viewed .file-name,
1092
+ .file-item.viewed.context-file-item .file-name {
1093
+ color: var(--color-fg-muted, #8b949e);
1094
+ font-style: italic;
1095
+ }
1096
+
1078
1097
  /* Tree view styles */
1079
1098
  .tree-item {
1080
1099
  user-select: none;
@@ -8003,11 +8022,15 @@ body.resizing * {
8003
8022
  padding: 0 2px;
8004
8023
  }
8005
8024
 
8006
- .analysis-history-level.level-on {
8025
+ .analysis-history-level.level-success {
8007
8026
  color: var(--color-success, #22c55e);
8008
8027
  }
8009
8028
 
8010
- .analysis-history-level.level-off {
8029
+ .analysis-history-level.level-failed {
8030
+ color: var(--color-danger, #d1242f);
8031
+ }
8032
+
8033
+ .analysis-history-level.level-skipped {
8011
8034
  color: var(--color-text-muted);
8012
8035
  opacity: 0.5;
8013
8036
  }
@@ -39,6 +39,10 @@ class TimeoutSelect {
39
39
  { value: '1800000', label: '30m' },
40
40
  { value: '2700000', label: '45m' },
41
41
  { value: '3600000', label: '60m' },
42
+ { value: '4500000', label: '75m' },
43
+ { value: '5400000', label: '90m' },
44
+ { value: '6300000', label: '105m' },
45
+ { value: '7200000', label: '120m' },
42
46
  ];
43
47
 
44
48
  /**
@@ -1508,12 +1508,13 @@ class LocalManager {
1508
1508
  fileCount: sortedFiles.length
1509
1509
  });
1510
1510
 
1511
- // Update file list sidebar
1512
- manager.updateFileList(sortedFiles);
1513
-
1514
1511
  // Load viewed state before rendering so files can start collapsed
1512
+ // and so the sidebar viewed indicator renders on first paint
1515
1513
  await manager.loadViewedState();
1516
1514
 
1515
+ // Update file list sidebar
1516
+ manager.updateFileList(sortedFiles);
1517
+
1517
1518
  // Render diff
1518
1519
  manager.renderDiff({ changed_files: sortedFiles });
1519
1520
 
@@ -502,7 +502,7 @@ class AnalysisHistoryManager {
502
502
  `;
503
503
 
504
504
  // Level indicators in preview
505
- if (run.levels_config) {
505
+ if (run.levels_config || run.level_outcomes) {
506
506
  html += `
507
507
  <div class="analysis-preview-row">
508
508
  <span class="analysis-preview-label">Levels</span>
@@ -883,31 +883,56 @@ class AnalysisHistoryManager {
883
883
  }
884
884
 
885
885
  /**
886
- * Render level indicators (L1/L2/L3) based on levels_config.
887
- * @param {Object} run - Analysis run object with optional levels_config
886
+ * Render level indicators (L1/L2/L3/C) with tri-state outcomes
887
+ * (success / failed / skipped).
888
+ *
889
+ * Prefers persisted `level_outcomes` when available. Falls back to
890
+ * `levels_config` for legacy runs — in which case enabled levels render
891
+ * as success, disabled as skipped, and the consolidation slot is omitted
892
+ * (we have no historical data for it).
893
+ *
894
+ * @param {Object} run - Analysis run object
888
895
  * @returns {string} HTML string for level indicators
889
896
  */
890
897
  renderLevelIndicators(run) {
891
- const levelsConfig = run.levels_config;
892
- if (!levelsConfig) return '';
893
-
894
- // levels_config can be:
895
- // - An array like [1, 2] (voice-centric: enabled levels)
896
- // - An object like { level1: true, level2: true, level3: false } (advanced)
897
- const levels = [1, 2, 3];
898
- const indicators = levels.map(level => {
899
- let enabled;
900
- if (Array.isArray(levelsConfig)) {
901
- enabled = levelsConfig.includes(level);
902
- } else {
903
- const key = `level${level}`;
904
- enabled = levelsConfig[key] !== false;
898
+ const outcomes = run.level_outcomes;
899
+ const config = run.levels_config;
900
+
901
+ const slots = [];
902
+ const addSlot = (label, outcome) => {
903
+ if (outcome) slots.push({ label, outcome });
904
+ };
905
+
906
+ if (outcomes) {
907
+ addSlot('L1', outcomes.level1);
908
+ addSlot('L2', outcomes.level2);
909
+ addSlot('L3', outcomes.level3);
910
+ addSlot('C', outcomes.consolidation);
911
+ } else if (config) {
912
+ // Legacy fallback: derive from config only. No failure state, no C slot.
913
+ // levels_config can be an array (voice-centric: enabled levels) or an
914
+ // object (advanced: per-level boolean).
915
+ for (const level of [1, 2, 3]) {
916
+ const enabled = Array.isArray(config)
917
+ ? config.includes(level)
918
+ : config[`level${level}`] !== false;
919
+ addSlot(`L${level}`, enabled ? 'success' : 'skipped');
905
920
  }
906
- const cls = enabled ? 'level-on' : 'level-off';
907
- const icon = enabled ? '\u2713' : '\u2717';
908
- return `<span class="analysis-history-level ${cls}">L${level}${icon}</span>`;
909
- });
910
- return `<span class="analysis-history-levels">${indicators.join('')}</span>`;
921
+ } else {
922
+ return '';
923
+ }
924
+
925
+ const icon = { success: '\u2713', failed: '\u2717', skipped: '\u00B7' };
926
+ const cls = {
927
+ success: 'level-success',
928
+ failed: 'level-failed',
929
+ skipped: 'level-skipped'
930
+ };
931
+
932
+ const html = slots
933
+ .map(s => `<span class="analysis-history-level ${cls[s.outcome] || ''}">${s.label}${icon[s.outcome] || ''}</span>`)
934
+ .join('');
935
+ return `<span class="analysis-history-levels">${html}</span>`;
911
936
  }
912
937
 
913
938
  /**
package/public/js/pr.js CHANGED
@@ -799,12 +799,13 @@ class PRManager {
799
799
  window.aiPanel.setFileOrder(this.canonicalFileOrder);
800
800
  }
801
801
 
802
- // Update sidebar with file list
803
- this.updateFileList(sortedFiles);
804
-
805
802
  // Load viewed state before rendering so files can start collapsed
803
+ // and so the sidebar viewed indicator renders on first paint
806
804
  await this.loadViewedState();
807
805
 
806
+ // Update sidebar with file list
807
+ this.updateFileList(sortedFiles);
808
+
808
809
  // Render diff using the existing renderDiff method
809
810
  this.renderDiff({ changed_files: sortedFiles });
810
811
 
@@ -2083,10 +2084,60 @@ class PRManager {
2083
2084
  }
2084
2085
  }
2085
2086
 
2087
+ // Update sidebar file row to reflect viewed state
2088
+ this.updateFileItemViewedState(filePath, isViewed);
2089
+
2086
2090
  // Persist viewed state
2087
2091
  this.saveViewedState();
2088
2092
  }
2089
2093
 
2094
+ /**
2095
+ * Build the eye-slash icon wrapper element used to mark a sidebar
2096
+ * file row as viewed. Shared by the initial render and in-place updates
2097
+ * so the markup and attributes stay in sync.
2098
+ * @returns {HTMLSpanElement}
2099
+ */
2100
+ _createViewedIcon() {
2101
+ const viewedIcon = document.createElement('span');
2102
+ viewedIcon.className = 'file-viewed-icon-wrapper';
2103
+ viewedIcon.title = 'Marked as viewed';
2104
+ viewedIcon.setAttribute('aria-label', 'Marked as viewed');
2105
+ viewedIcon.innerHTML = '<svg class="file-viewed-icon" viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M1.22 1.22a.75.75 0 0 1 1.06 0l12.5 12.5a.75.75 0 1 1-1.06 1.06l-1.82-1.82A7.44 7.44 0 0 1 8 14c-2.12 0-3.88-.81-5.26-1.94A13.13 13.13 0 0 1 .75 9.44a7.34 7.34 0 0 1-.51-.66.77.77 0 0 1 0-.84 12.52 12.52 0 0 1 .55-.72c.28-.34.66-.79 1.13-1.26L1.22 2.28a.75.75 0 0 1 0-1.06ZM4.5 5.56 6 7.06a2.5 2.5 0 0 0 2.94 2.94l1.22 1.22a4 4 0 0 1-5.66-5.66ZM8 3.5a4 4 0 0 1 3.98 4.46l3.04 3.04.1-.12c.36-.44.65-.87.87-1.22a.77.77 0 0 0 0-.84 13.13 13.13 0 0 0-2-2.62A7.44 7.44 0 0 0 8 2a7.4 7.4 0 0 0-2.3.36L7.1 3.78c.3-.18.62-.28.9-.28Z"/></svg>';
2106
+ return viewedIcon;
2107
+ }
2108
+
2109
+ /**
2110
+ * Update the sidebar file row to reflect the viewed state.
2111
+ * Adds/removes the .viewed class and injects/removes the eye-slash icon
2112
+ * without re-rendering the whole file list.
2113
+ * @param {string} filePath - Path of the file
2114
+ * @param {boolean} isViewed - Whether the file is now viewed
2115
+ */
2116
+ updateFileItemViewedState(filePath, isViewed) {
2117
+ const items = document.querySelectorAll('.file-item');
2118
+ let item = null;
2119
+ for (const candidate of items) {
2120
+ if (candidate.dataset.path === filePath) {
2121
+ item = candidate;
2122
+ break;
2123
+ }
2124
+ }
2125
+ if (!item) return;
2126
+
2127
+ const existingIcon = item.querySelector('.file-viewed-icon-wrapper');
2128
+
2129
+ if (isViewed) {
2130
+ item.classList.add('viewed');
2131
+ if (!existingIcon) {
2132
+ const viewedIcon = this._createViewedIcon();
2133
+ item.insertBefore(viewedIcon, item.firstChild);
2134
+ }
2135
+ } else {
2136
+ item.classList.remove('viewed');
2137
+ if (existingIcon) existingIcon.remove();
2138
+ }
2139
+ }
2140
+
2090
2141
  /**
2091
2142
  * Save viewed files state to storage
2092
2143
  * Persists per PR for later retrieval
@@ -2357,7 +2408,9 @@ class PRManager {
2357
2408
  type: 'context',
2358
2409
  oldNumber: lineNumber,
2359
2410
  newNumber: lineNumber + lineOffset, // Apply offset for correct right-side line number
2360
- content: content || ''
2411
+ // Expanded rows should follow the same contract as parsed diff context
2412
+ // lines so DiffRenderer strips the synthetic diff marker, not real indent.
2413
+ content: ' ' + (content || '')
2361
2414
  };
2362
2415
 
2363
2416
  const lineRow = this.renderDiffLine(fragment, lineData, fileName, null);
@@ -2469,7 +2522,9 @@ class PRManager {
2469
2522
  type: 'context',
2470
2523
  oldNumber: lineNumber,
2471
2524
  newNumber: lineNumber + lineOffset, // Apply offset for correct right-side line number
2472
- content: content || ''
2525
+ // Expanded rows should follow the same contract as parsed diff context
2526
+ // lines so DiffRenderer strips the synthetic diff marker, not real indent.
2527
+ content: ' ' + (content || '')
2473
2528
  };
2474
2529
 
2475
2530
  const lineRow = this.renderDiffLine(fragment, lineData, fileName, null);
@@ -4191,6 +4246,11 @@ class PRManager {
4191
4246
 
4192
4247
  if (file.generated) item.classList.add('generated');
4193
4248
  if (file.contextFile) item.classList.add('context-file-item');
4249
+ if (this.viewedFiles && this.viewedFiles.has(file.fullPath)) {
4250
+ item.classList.add('viewed');
4251
+ const viewedIcon = this._createViewedIcon();
4252
+ item.insertBefore(viewedIcon, item.firstChild);
4253
+ }
4194
4254
  if (file.renamed && file.renamedFrom) {
4195
4255
  item.title = `Renamed from: ${file.renamedFrom}`;
4196
4256
  const renameIcon = document.createElement('span');
@@ -550,13 +550,22 @@ class Analyzer {
550
550
  throw new CancellationError('Analysis was cancelled');
551
551
  }
552
552
 
553
+ // Build per-level outcome record for persistence and UI
554
+ const levelOutcomes = {
555
+ level1: levelResults.level1.status,
556
+ level2: levelResults.level2.status,
557
+ level3: levelResults.level3.status,
558
+ consolidation: 'success'
559
+ };
560
+
553
561
  // Update analysis_run record with completion data
554
562
  try {
555
563
  await analysisRunRepo.update(runId, {
556
564
  status: 'completed',
557
565
  summary: orchestrationResult.summary,
558
566
  totalSuggestions: finalSuggestions.length,
559
- filesAnalyzed: validFiles.length
567
+ filesAnalyzed: validFiles.length,
568
+ levelOutcomes
560
569
  });
561
570
  logger.info(`${logPrefix}Updated analysis_run record to completed: ${finalSuggestions.length} suggestions, ${validFiles.length} files`);
562
571
  } catch (updateError) {
@@ -569,6 +578,7 @@ class Analyzer {
569
578
  runId,
570
579
  suggestions: finalSuggestions,
571
580
  levelResults,
581
+ levelOutcomes,
572
582
  summary: orchestrationResult.summary
573
583
  };
574
584
 
@@ -603,6 +613,14 @@ class Analyzer {
603
613
  throw new CancellationError('Analysis was cancelled');
604
614
  }
605
615
 
616
+ // Build per-level outcome record (consolidation failed, fallback path used)
617
+ const levelOutcomes = {
618
+ level1: levelResults.level1.status,
619
+ level2: levelResults.level2.status,
620
+ level3: levelResults.level3.status,
621
+ consolidation: 'failed'
622
+ };
623
+
606
624
  // Update analysis_run record with completion data (even though orchestration failed)
607
625
  const fallbackSummary = `Analysis complete (consolidation failed): ${finalFallbackSuggestions.length} suggestions`;
608
626
  try {
@@ -610,7 +628,8 @@ class Analyzer {
610
628
  status: 'completed',
611
629
  summary: fallbackSummary,
612
630
  totalSuggestions: finalFallbackSuggestions.length,
613
- filesAnalyzed: validFiles.length
631
+ filesAnalyzed: validFiles.length,
632
+ levelOutcomes
614
633
  });
615
634
  logger.info(`${logPrefix}Updated analysis_run record to completed (fallback): ${finalFallbackSuggestions.length} suggestions`);
616
635
  } catch (updateError) {
@@ -621,6 +640,7 @@ class Analyzer {
621
640
  runId,
622
641
  suggestions: finalFallbackSuggestions,
623
642
  levelResults,
643
+ levelOutcomes,
624
644
  summary: fallbackSummary,
625
645
  orchestrationFailed: true
626
646
  };
@@ -2973,7 +2993,8 @@ File-level suggestions should NOT have a line number. They apply to the entire f
2973
2993
  status: 'completed',
2974
2994
  summary: result.summary,
2975
2995
  totalSuggestions: finalSuggestions.length,
2976
- filesAnalyzed: validFiles.length
2996
+ filesAnalyzed: validFiles.length,
2997
+ levelOutcomes: { consolidation: 'skipped' }
2977
2998
  });
2978
2999
  } catch (err) {
2979
3000
  logger.warn(`[ReviewerCouncil] Failed to update parent run: ${err.message}`);
@@ -2982,7 +3003,8 @@ File-level suggestions should NOT have a line number. They apply to the entire f
2982
3003
  return {
2983
3004
  runId: parentRunId,
2984
3005
  suggestions: finalSuggestions,
2985
- summary: result.summary || `Review council complete: ${finalSuggestions.length} suggestions`
3006
+ summary: result.summary || `Review council complete: ${finalSuggestions.length} suggestions`,
3007
+ levelOutcomes: { consolidation: 'skipped' }
2986
3008
  };
2987
3009
  }
2988
3010
 
@@ -3012,7 +3034,8 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3012
3034
  return {
3013
3035
  runId: parentRunId,
3014
3036
  suggestions: result.suggestions,
3015
- summary: result.summary || `Review council complete: ${result.suggestions?.length || 0} suggestions`
3037
+ summary: result.summary || `Review council complete: ${result.suggestions?.length || 0} suggestions`,
3038
+ levelOutcomes: result.levelOutcomes
3016
3039
  };
3017
3040
  }
3018
3041
 
@@ -3085,7 +3108,8 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3085
3108
  await analysisRunRepo.update(childRunId, {
3086
3109
  status: 'completed',
3087
3110
  summary: result.summary,
3088
- totalSuggestions: validatedSuggestions.length
3111
+ totalSuggestions: validatedSuggestions.length,
3112
+ levelOutcomes: { consolidation: 'skipped' }
3089
3113
  });
3090
3114
  } catch (err) {
3091
3115
  logger.warn(`[ReviewerCouncil] Failed to update child run ${childRunId}: ${err.message}`);
@@ -3217,7 +3241,8 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3217
3241
  status: 'completed',
3218
3242
  summary: singleResult.summary,
3219
3243
  totalSuggestions: finalSuggestions.length,
3220
- filesAnalyzed: validFiles.length
3244
+ filesAnalyzed: validFiles.length,
3245
+ levelOutcomes: { consolidation: 'skipped' }
3221
3246
  });
3222
3247
  } catch (err) {
3223
3248
  logger.warn(`[ReviewerCouncil] Failed to update parent run: ${err.message}`);
@@ -3226,7 +3251,8 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3226
3251
  return {
3227
3252
  runId: parentRunId,
3228
3253
  suggestions: finalSuggestions,
3229
- summary: singleResult.summary || `Review council complete: ${finalSuggestions.length} suggestions`
3254
+ summary: singleResult.summary || `Review council complete: ${finalSuggestions.length} suggestions`,
3255
+ levelOutcomes: { consolidation: 'skipped' }
3230
3256
  };
3231
3257
  }
3232
3258
 
@@ -3295,7 +3321,8 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3295
3321
  status: 'completed',
3296
3322
  summary,
3297
3323
  totalSuggestions: finalSuggestions.length,
3298
- filesAnalyzed: validFiles.length
3324
+ filesAnalyzed: validFiles.length,
3325
+ levelOutcomes: { consolidation: 'success' }
3299
3326
  });
3300
3327
  } catch (err) {
3301
3328
  logger.warn(`[ReviewerCouncil] Failed to update parent run: ${err.message}`);
@@ -3306,7 +3333,8 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3306
3333
  return {
3307
3334
  runId: parentRunId,
3308
3335
  suggestions: finalSuggestions,
3309
- summary
3336
+ summary,
3337
+ levelOutcomes: { consolidation: 'success' }
3310
3338
  };
3311
3339
  } catch (error) {
3312
3340
  logger.error(`[ReviewerCouncil] Cross-reviewer consolidation failed: ${error.message}`);
@@ -3324,7 +3352,8 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3324
3352
  status: 'completed',
3325
3353
  summary: fallbackSummary,
3326
3354
  totalSuggestions: fallbackSuggestions.length,
3327
- filesAnalyzed: validFiles.length
3355
+ filesAnalyzed: validFiles.length,
3356
+ levelOutcomes: { consolidation: 'failed' }
3328
3357
  });
3329
3358
  } catch (err) {
3330
3359
  logger.warn(`[ReviewerCouncil] Failed to update parent run: ${err.message}`);
@@ -3334,7 +3363,8 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3334
3363
  runId: parentRunId,
3335
3364
  suggestions: fallbackSuggestions,
3336
3365
  summary: fallbackSummary,
3337
- orchestrationFailed: true
3366
+ orchestrationFailed: true,
3367
+ levelOutcomes: { consolidation: 'failed' }
3338
3368
  };
3339
3369
  }
3340
3370
  }
@@ -3523,7 +3553,8 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3523
3553
  return {
3524
3554
  runId,
3525
3555
  suggestions: finalSuggestions,
3526
- summary: bestVoiceSummary || `Council analysis complete: ${finalSuggestions.length} suggestions from single reviewer`
3556
+ summary: bestVoiceSummary || `Council analysis complete: ${finalSuggestions.length} suggestions from single reviewer`,
3557
+ levelOutcomes: { consolidation: 'skipped' }
3527
3558
  };
3528
3559
  }
3529
3560
 
@@ -3636,7 +3667,8 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3636
3667
  return {
3637
3668
  runId,
3638
3669
  suggestions: finalSuggestions,
3639
- summary: orchestrationResult.summary
3670
+ summary: orchestrationResult.summary,
3671
+ levelOutcomes: { consolidation: 'success' }
3640
3672
  };
3641
3673
  } catch (error) {
3642
3674
  logger.error(`[Council] Cross-level consolidation failed: ${error.message}`);
@@ -3657,7 +3689,8 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3657
3689
  runId,
3658
3690
  suggestions: finalFallback,
3659
3691
  summary: `Council analysis complete (consolidation failed): ${finalFallback.length} suggestions`,
3660
- orchestrationFailed: true
3692
+ orchestrationFailed: true,
3693
+ levelOutcomes: { consolidation: 'failed' }
3661
3694
  };
3662
3695
  }
3663
3696
  }
@@ -20,6 +20,30 @@ const BIN_DIR = path.join(__dirname, '..', '..', 'bin');
20
20
  * Claude model definitions with tier mappings
21
21
  */
22
22
  const CLAUDE_MODELS = [
23
+ {
24
+ id: 'opus-4.7-xhigh',
25
+ cli_model: 'claude-opus-4-7',
26
+ env: { CLAUDE_CODE_EFFORT_LEVEL: 'xhigh' },
27
+ name: 'Opus 4.7 xhigh',
28
+ tier: 'thorough',
29
+ tagline: 'Latest Gen',
30
+ description: 'Opus 4.7 (latest) with extra-high effort',
31
+ badge: 'Latest',
32
+ badgeClass: 'badge-power'
33
+ },
34
+ {
35
+ id: 'opus',
36
+ aliases: ['opus-4.6-high'],
37
+ cli_model: 'claude-opus-4-6',
38
+ env: { CLAUDE_CODE_EFFORT_LEVEL: 'high' },
39
+ name: 'Opus 4.6 High',
40
+ tier: 'thorough',
41
+ tagline: 'Maximum Depth',
42
+ description: 'Opus 4.6 with high effort — deepest analysis',
43
+ badge: 'Most Thorough',
44
+ badgeClass: 'badge-power',
45
+ default: true
46
+ },
23
47
  {
24
48
  id: 'haiku',
25
49
  name: 'Haiku 4.6',
@@ -41,7 +65,7 @@ const CLAUDE_MODELS = [
41
65
  },
42
66
  {
43
67
  id: 'opus-4.6-low',
44
- cli_model: 'opus',
68
+ cli_model: 'claude-opus-4-6',
45
69
  env: { CLAUDE_CODE_EFFORT_LEVEL: 'low' },
46
70
  name: 'Opus 4.6 Low',
47
71
  tier: 'balanced',
@@ -52,7 +76,7 @@ const CLAUDE_MODELS = [
52
76
  },
53
77
  {
54
78
  id: 'opus-4.6-medium',
55
- cli_model: 'opus',
79
+ cli_model: 'claude-opus-4-6',
56
80
  env: { CLAUDE_CODE_EFFORT_LEVEL: 'medium' },
57
81
  name: 'Opus 4.6 Medium',
58
82
  tier: 'balanced',
@@ -61,21 +85,9 @@ const CLAUDE_MODELS = [
61
85
  badge: 'Thorough',
62
86
  badgeClass: 'badge-power'
63
87
  },
64
- {
65
- id: 'opus',
66
- aliases: ['opus-4.6-high'],
67
- env: { CLAUDE_CODE_EFFORT_LEVEL: 'high' },
68
- name: 'Opus 4.6 High',
69
- tier: 'thorough',
70
- tagline: 'Maximum Depth',
71
- description: 'Opus 4.6 with high effort — deepest analysis',
72
- badge: 'Most Thorough',
73
- badgeClass: 'badge-power',
74
- default: true
75
- },
76
88
  {
77
89
  id: 'opus-4.6-1m',
78
- cli_model: 'opus[1m]',
90
+ cli_model: 'claude-opus-4-6[1m]',
79
91
  name: 'Opus 4.6 1M',
80
92
  tier: 'balanced',
81
93
  tagline: 'Extended Context',
package/src/database.js CHANGED
@@ -21,7 +21,7 @@ function getDbPath() {
21
21
  /**
22
22
  * Current schema version - increment this when adding new migrations
23
23
  */
24
- const CURRENT_SCHEMA_VERSION = 43;
24
+ const CURRENT_SCHEMA_VERSION = 44;
25
25
 
26
26
  /**
27
27
  * Database schema SQL statements
@@ -184,6 +184,7 @@ const SCHEMA_SQL = {
184
184
  parent_run_id TEXT,
185
185
  config_type TEXT DEFAULT 'single',
186
186
  levels_config TEXT,
187
+ level_outcomes TEXT,
187
188
  scope_start TEXT,
188
189
  scope_end TEXT,
189
190
  FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE CASCADE
@@ -1866,6 +1867,25 @@ const MIGRATIONS = {
1866
1867
  };
1867
1868
  addColumnIfNotExists('repo_settings', 'load_skills', 'INTEGER');
1868
1869
  console.log('Migration to schema version 43 complete');
1870
+ },
1871
+
1872
+ 44: (db) => {
1873
+ console.log('Running migration to schema version 44: Add level_outcomes to analysis_runs...');
1874
+ const hasLevelOutcomes = columnExists(db, 'analysis_runs', 'level_outcomes');
1875
+ if (!hasLevelOutcomes) {
1876
+ try {
1877
+ db.prepare(`ALTER TABLE analysis_runs ADD COLUMN level_outcomes TEXT`).run();
1878
+ console.log(' Added level_outcomes column to analysis_runs');
1879
+ } catch (error) {
1880
+ if (!error.message.includes('duplicate column name')) {
1881
+ throw error;
1882
+ }
1883
+ console.log(' Column level_outcomes already exists (race condition)');
1884
+ }
1885
+ } else {
1886
+ console.log(' Column level_outcomes already exists');
1887
+ }
1888
+ console.log('Migration to schema version 44 complete');
1869
1889
  }
1870
1890
  };
1871
1891
 
@@ -4348,6 +4368,11 @@ class AnalysisRunRepository {
4348
4368
  params.push(updates.diff);
4349
4369
  }
4350
4370
 
4371
+ if (updates.levelOutcomes !== undefined) {
4372
+ setClauses.push('level_outcomes = ?');
4373
+ params.push(updates.levelOutcomes === null ? null : JSON.stringify(updates.levelOutcomes));
4374
+ }
4375
+
4351
4376
  if (setClauses.length === 0) {
4352
4377
  return false;
4353
4378
  }
@@ -4380,7 +4405,7 @@ class AnalysisRunRepository {
4380
4405
  const columns = [
4381
4406
  'id', 'review_id', 'provider', 'model', 'tier', 'custom_instructions', 'global_instructions', 'repo_instructions', 'request_instructions',
4382
4407
  'head_sha', 'summary', 'status', 'total_suggestions', 'files_analyzed', 'started_at', 'completed_at',
4383
- 'parent_run_id', 'config_type', 'levels_config'
4408
+ 'parent_run_id', 'config_type', 'levels_config', 'level_outcomes'
4384
4409
  ];
4385
4410
  if (includeDiff) {
4386
4411
  columns.splice(columns.indexOf('head_sha') + 1, 0, 'diff'); // Insert diff after head_sha
@@ -4408,7 +4433,7 @@ class AnalysisRunRepository {
4408
4433
  const columns = [
4409
4434
  'id', 'review_id', 'provider', 'model', 'tier', 'custom_instructions', 'global_instructions', 'repo_instructions', 'request_instructions',
4410
4435
  'head_sha', 'summary', 'status', 'total_suggestions', 'files_analyzed', 'started_at', 'completed_at',
4411
- 'parent_run_id', 'config_type', 'levels_config'
4436
+ 'parent_run_id', 'config_type', 'levels_config', 'level_outcomes'
4412
4437
  ];
4413
4438
  if (includeDiff) {
4414
4439
  columns.splice(columns.indexOf('head_sha') + 1, 0, 'diff'); // Insert diff after head_sha
@@ -4446,7 +4471,7 @@ class AnalysisRunRepository {
4446
4471
  const columns = [
4447
4472
  'id', 'review_id', 'provider', 'model', 'tier', 'custom_instructions', 'global_instructions', 'repo_instructions', 'request_instructions',
4448
4473
  'head_sha', 'summary', 'status', 'total_suggestions', 'files_analyzed', 'started_at', 'completed_at',
4449
- 'parent_run_id', 'config_type', 'levels_config'
4474
+ 'parent_run_id', 'config_type', 'levels_config', 'level_outcomes'
4450
4475
  ];
4451
4476
  if (includeDiff) {
4452
4477
  columns.splice(columns.indexOf('head_sha') + 1, 0, 'diff'); // Insert diff after head_sha
@@ -4473,7 +4498,7 @@ class AnalysisRunRepository {
4473
4498
  return query(this.db, `
4474
4499
  SELECT id, review_id, provider, model, tier, custom_instructions, global_instructions, repo_instructions, request_instructions,
4475
4500
  head_sha, summary, status, total_suggestions, files_analyzed, started_at, completed_at,
4476
- parent_run_id, config_type, levels_config
4501
+ parent_run_id, config_type, levels_config, level_outcomes
4477
4502
  FROM analysis_runs
4478
4503
  WHERE parent_run_id = ?
4479
4504
  ORDER BY started_at ASC
@@ -4971,5 +4996,6 @@ module.exports = {
4971
4996
  generateWorktreeId,
4972
4997
  migrateExistingWorktrees,
4973
4998
  // Exported for testing only
4974
- _MIGRATIONS: MIGRATIONS
4999
+ _MIGRATIONS: MIGRATIONS,
5000
+ MIGRATIONS
4975
5001
  };