@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 +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/pr.css +25 -2
- package/public/js/components/TimeoutSelect.js +4 -0
- package/public/js/local.js +4 -3
- package/public/js/modules/analysis-history.js +47 -22
- package/public/js/pr.js +65 -5
- package/src/ai/analyzer.js +48 -15
- package/src/ai/claude-provider.js +27 -15
- package/src/database.js +32 -6
- package/src/git/diff-flags.js +4 -2
- package/src/git/worktree-pool-lifecycle.js +12 -3
- package/src/git/worktree.js +172 -33
- package/src/main.js +2 -1
- package/src/routes/analyses.js +3 -1
- package/src/routes/pr.js +25 -27
- package/src/routes/reviews.js +19 -15
- package/src/setup/pr-setup.js +19 -6
- package/src/utils/diff-file-content.js +110 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "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
|
+
"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-
|
|
8025
|
+
.analysis-history-level.level-success {
|
|
8007
8026
|
color: var(--color-success, #22c55e);
|
|
8008
8027
|
}
|
|
8009
8028
|
|
|
8010
|
-
.analysis-history-level.level-
|
|
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
|
/**
|
package/public/js/local.js
CHANGED
|
@@ -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)
|
|
887
|
-
*
|
|
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
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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');
|
package/src/ai/analyzer.js
CHANGED
|
@@ -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 =
|
|
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
|
};
|