@in-the-loop-labs/pair-review 3.0.5 → 3.1.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.
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/skills/analyze/references/level1-balanced.md +8 -0
- package/plugin-code-critic/skills/analyze/references/level1-fast.md +7 -0
- package/plugin-code-critic/skills/analyze/references/level1-thorough.md +8 -0
- package/plugin-code-critic/skills/analyze/references/level2-balanced.md +9 -0
- package/plugin-code-critic/skills/analyze/references/level2-fast.md +8 -0
- package/plugin-code-critic/skills/analyze/references/level2-thorough.md +9 -0
- package/plugin-code-critic/skills/analyze/references/level3-balanced.md +9 -0
- package/plugin-code-critic/skills/analyze/references/level3-fast.md +8 -0
- package/plugin-code-critic/skills/analyze/references/level3-thorough.md +9 -0
- package/plugin-code-critic/skills/analyze/references/orchestration-balanced.md +9 -0
- package/plugin-code-critic/skills/analyze/references/orchestration-fast.md +5 -0
- package/plugin-code-critic/skills/analyze/references/orchestration-thorough.md +9 -0
- package/public/css/analysis-config.css +83 -0
- package/public/css/pr.css +191 -4
- package/public/index.html +20 -0
- package/public/js/components/AIPanel.js +1 -1
- package/public/js/components/AdvancedConfigTab.js +83 -8
- package/public/js/components/AnalysisConfigModal.js +155 -5
- package/public/js/components/ChatPanel.js +22 -5
- package/public/js/components/CouncilProgressModal.js +239 -22
- package/public/js/components/TimeoutSelect.js +2 -0
- package/public/js/components/VoiceCentricConfigTab.js +179 -12
- package/public/js/index.js +119 -1
- package/public/js/local.js +141 -47
- package/public/js/modules/suggestion-manager.js +2 -1
- package/public/js/pr.js +71 -12
- package/public/js/repo-settings.js +2 -2
- package/public/local.html +32 -11
- package/public/pr.html +2 -0
- package/src/ai/analyzer.js +371 -111
- package/src/ai/claude-provider.js +2 -0
- package/src/ai/codex-provider.js +1 -1
- package/src/ai/copilot-provider.js +2 -0
- package/src/ai/executable-provider.js +534 -0
- package/src/ai/gemini-provider.js +2 -0
- package/src/ai/index.js +9 -1
- package/src/ai/pi-provider.js +10 -8
- package/src/ai/prompts/baseline/consolidation/balanced.js +54 -2
- package/src/ai/prompts/baseline/consolidation/fast.js +31 -1
- package/src/ai/prompts/baseline/consolidation/thorough.js +46 -3
- package/src/ai/prompts/baseline/level1/balanced.js +12 -0
- package/src/ai/prompts/baseline/level1/fast.js +11 -0
- package/src/ai/prompts/baseline/level1/thorough.js +12 -0
- package/src/ai/prompts/baseline/level2/balanced.js +13 -0
- package/src/ai/prompts/baseline/level2/fast.js +12 -0
- package/src/ai/prompts/baseline/level2/thorough.js +13 -0
- package/src/ai/prompts/baseline/level3/balanced.js +13 -0
- package/src/ai/prompts/baseline/level3/fast.js +12 -0
- package/src/ai/prompts/baseline/level3/thorough.js +13 -0
- package/src/ai/prompts/baseline/orchestration/balanced.js +15 -0
- package/src/ai/prompts/baseline/orchestration/fast.js +11 -0
- package/src/ai/prompts/baseline/orchestration/thorough.js +15 -0
- package/src/ai/prompts/render-for-skill.js +3 -0
- package/src/ai/prompts/shared/output-schema.js +8 -0
- package/src/ai/provider.js +89 -4
- package/src/chat/prompt-builder.js +17 -1
- package/src/chat/session-manager.js +32 -28
- package/src/config.js +15 -2
- package/src/database.js +59 -15
- package/src/git/base-branch.js +133 -52
- package/src/local-review.js +15 -9
- package/src/main.js +3 -2
- package/src/routes/analyses.js +34 -8
- package/src/routes/chat.js +15 -8
- package/src/routes/config.js +3 -120
- package/src/routes/councils.js +15 -6
- package/src/routes/executable-analysis.js +494 -0
- package/src/routes/local.js +160 -26
- package/src/routes/mcp.js +9 -4
- package/src/routes/pr.js +166 -29
- package/src/routes/reviews.js +31 -5
- package/src/routes/shared.js +72 -5
- package/src/routes/worktrees.js +4 -2
- package/src/utils/comment-formatter.js +28 -11
- package/src/utils/instructions.js +22 -8
- package/src/utils/logger.js +20 -10
package/src/routes/local.js
CHANGED
|
@@ -31,6 +31,11 @@ const { getGeneratedFilePatterns } = require('../git/gitattributes');
|
|
|
31
31
|
const { getShaAbbrevLength } = require('../git/sha-abbrev');
|
|
32
32
|
const { validateCouncilConfig, normalizeCouncilConfig } = require('./councils');
|
|
33
33
|
const { TIERS, TIER_ALIASES, VALID_TIERS, resolveTier } = require('../ai/prompts/config');
|
|
34
|
+
const { getProviderClass, createProvider } = require('../ai/provider');
|
|
35
|
+
const { readFileSync } = require('fs');
|
|
36
|
+
const { getDefaultBranch, tryGraphiteState, readGraphitePRInfo, enrichStackWithPRInfo } = require('../git/base-branch');
|
|
37
|
+
const { CommentRepository } = require('../database');
|
|
38
|
+
const { runExecutableAnalysis, getChangedFiles } = require('./executable-analysis');
|
|
34
39
|
const {
|
|
35
40
|
activeAnalyses,
|
|
36
41
|
localReviewDiffs,
|
|
@@ -40,7 +45,8 @@ const {
|
|
|
40
45
|
broadcastProgress,
|
|
41
46
|
CancellationError,
|
|
42
47
|
createProgressCallback,
|
|
43
|
-
parseEnabledLevels
|
|
48
|
+
parseEnabledLevels,
|
|
49
|
+
registerProcess: registerProcessForCancellation
|
|
44
50
|
} = require('./shared');
|
|
45
51
|
|
|
46
52
|
const router = express.Router();
|
|
@@ -66,6 +72,26 @@ function deleteLocalReviewDiff(reviewId) {
|
|
|
66
72
|
localReviewDiffs.delete(toIntKey(reviewId));
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Guard: reject the request if the review's scope resolves to zero changed files.
|
|
77
|
+
* Returns true if the guard fired (response already sent), false otherwise.
|
|
78
|
+
*/
|
|
79
|
+
async function rejectIfEmptyScope(res, review, localPath) {
|
|
80
|
+
const scopeContext = {
|
|
81
|
+
scopeStart: review.local_scope_start || DEFAULT_SCOPE.start,
|
|
82
|
+
scopeEnd: review.local_scope_end || DEFAULT_SCOPE.end,
|
|
83
|
+
baseBranch: review.local_base_branch || null,
|
|
84
|
+
};
|
|
85
|
+
const changedFiles = await getChangedFiles(localPath, scopeContext);
|
|
86
|
+
if (changedFiles.length === 0) {
|
|
87
|
+
res.status(409).json({
|
|
88
|
+
error: 'No changes found in the selected scope. Check that your scope includes files with modifications, or adjust the scope range.'
|
|
89
|
+
});
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
69
95
|
/**
|
|
70
96
|
* Check whether branch scope should be selectable in the scope range selector.
|
|
71
97
|
* Returns true when the current branch is a non-default, non-detached branch,
|
|
@@ -79,9 +105,8 @@ function isBranchAvailable(branchName, scopeStart, localPath) {
|
|
|
79
105
|
if (includesBranch(scopeStart)) return true;
|
|
80
106
|
if (!branchName || branchName === 'HEAD' || branchName === 'unknown') return false;
|
|
81
107
|
|
|
82
|
-
|
|
83
|
-
const defaultBranch =
|
|
84
|
-
// If detection fails, fall back to checking main/master
|
|
108
|
+
// Detect the default branch using only local refs (no network).
|
|
109
|
+
const defaultBranch = getDefaultBranch(localPath);
|
|
85
110
|
if (defaultBranch) {
|
|
86
111
|
return branchName !== defaultBranch;
|
|
87
112
|
}
|
|
@@ -561,19 +586,14 @@ router.get('/api/local/:reviewId', async (req, res) => {
|
|
|
561
586
|
const baseBranch = review.local_base_branch || null;
|
|
562
587
|
|
|
563
588
|
// When scope does NOT include branch, check for branch detection info
|
|
564
|
-
// Frontend uses this to suggest expanding scope to include branch
|
|
589
|
+
// Frontend uses this to suggest expanding scope to include branch.
|
|
590
|
+
// Only use already-cached results here — never block the response on
|
|
591
|
+
// GitHub API calls. Background detection (after res.json) will populate
|
|
592
|
+
// the cache for subsequent requests.
|
|
565
593
|
let branchInfo = null;
|
|
566
594
|
const cachedDiff = getLocalReviewDiff(reviewId);
|
|
567
595
|
if (!includesBranch(scopeStart) && cachedDiff?.branchInfo) {
|
|
568
596
|
branchInfo = cachedDiff.branchInfo;
|
|
569
|
-
} else if (!includesBranch(scopeStart) && !cachedDiff && review.local_path) {
|
|
570
|
-
// No cache (web UI started session) — run detection on-demand
|
|
571
|
-
const config = req.app.get('config') || {};
|
|
572
|
-
branchInfo = await detectAndBuildBranchInfo(review.local_path, branchName, {
|
|
573
|
-
repository: repositoryName,
|
|
574
|
-
githubToken: getGitHubToken(config),
|
|
575
|
-
enableGraphite: config.enable_graphite === true
|
|
576
|
-
});
|
|
577
597
|
}
|
|
578
598
|
|
|
579
599
|
// Check repo settings for auto_branch_review preference
|
|
@@ -602,6 +622,24 @@ router.get('/api/local/:reviewId', async (req, res) => {
|
|
|
602
622
|
|
|
603
623
|
// Compute SHA abbreviation length from the repo's git config
|
|
604
624
|
const shaAbbrevLength = getShaAbbrevLength(review.local_path);
|
|
625
|
+
|
|
626
|
+
// Detect Graphite stack if enabled
|
|
627
|
+
let stackData = null;
|
|
628
|
+
const localConfig = req.app.get('config') || {};
|
|
629
|
+
if (localConfig.enable_graphite === true && review.local_path && branchName && branchName !== 'unknown' && branchName !== 'HEAD') {
|
|
630
|
+
try {
|
|
631
|
+
const graphiteResult = tryGraphiteState(review.local_path, branchName, { execSync, readFileSync });
|
|
632
|
+
if (graphiteResult?.stack) {
|
|
633
|
+
const prInfo = readGraphitePRInfo(review.local_path, { execSync, readFileSync });
|
|
634
|
+
stackData = prInfo?.prInfos
|
|
635
|
+
? enrichStackWithPRInfo(graphiteResult.stack, prInfo.prInfos)
|
|
636
|
+
: graphiteResult.stack;
|
|
637
|
+
}
|
|
638
|
+
} catch {
|
|
639
|
+
// Non-fatal — stack detection is an enhancement
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
605
643
|
const metadataElapsed = Date.now() - tEndpoint;
|
|
606
644
|
if (metadataElapsed > 200) {
|
|
607
645
|
logger.debug(`[perf] metadata#${reviewId} took ${metadataElapsed}ms (threshold: 200ms)`);
|
|
@@ -622,6 +660,7 @@ router.get('/api/local/:reviewId', async (req, res) => {
|
|
|
622
660
|
baseBranch,
|
|
623
661
|
branchInfo,
|
|
624
662
|
branchAvailable,
|
|
663
|
+
stackData,
|
|
625
664
|
shaAbbrevLength,
|
|
626
665
|
createdAt: review.created_at,
|
|
627
666
|
updatedAt: review.updated_at
|
|
@@ -745,18 +784,20 @@ router.get('/api/local/:reviewId/diff', async (req, res) => {
|
|
|
745
784
|
});
|
|
746
785
|
}
|
|
747
786
|
|
|
748
|
-
// When ?w=1
|
|
787
|
+
// When ?w=1 or ?base=<branch>, regenerate the diff (transient view, not cached)
|
|
749
788
|
const hideWhitespace = req.query.w === '1';
|
|
789
|
+
const baseBranchOverride = req.query.base;
|
|
750
790
|
const scopeStart = review.local_scope_start || DEFAULT_SCOPE.start;
|
|
751
791
|
const scopeEnd = review.local_scope_end || DEFAULT_SCOPE.end;
|
|
792
|
+
const baseBranch = baseBranchOverride || review.local_base_branch;
|
|
752
793
|
let diffData;
|
|
753
794
|
|
|
754
|
-
if (hideWhitespace && review.local_path) {
|
|
795
|
+
if ((hideWhitespace || baseBranchOverride) && review.local_path) {
|
|
755
796
|
try {
|
|
756
|
-
const wsResult = await generateScopedDiff(review.local_path, scopeStart, scopeEnd,
|
|
797
|
+
const wsResult = await generateScopedDiff(review.local_path, scopeStart, scopeEnd, baseBranch, { hideWhitespace });
|
|
757
798
|
diffData = { diff: wsResult.diff, stats: wsResult.stats };
|
|
758
799
|
} catch (wsError) {
|
|
759
|
-
logger.warn(`Could not generate
|
|
800
|
+
logger.warn(`Could not generate diff for review #${reviewId}: ${wsError.message}`);
|
|
760
801
|
// Fall through to cached diff below
|
|
761
802
|
}
|
|
762
803
|
}
|
|
@@ -973,6 +1014,63 @@ router.get('/api/local/:reviewId/check-stale', async (req, res) => {
|
|
|
973
1014
|
}
|
|
974
1015
|
});
|
|
975
1016
|
|
|
1017
|
+
/**
|
|
1018
|
+
* Handle analysis for executable providers (external CLI tools).
|
|
1019
|
+
* Spawns the external CLI, maps its output to suggestions, and stores results.
|
|
1020
|
+
*/
|
|
1021
|
+
async function handleExecutableAnalysis(req, res, {
|
|
1022
|
+
reviewId, review, localPath, repository, selectedProvider, selectedModel,
|
|
1023
|
+
repoInstructions, requestInstructions, combinedInstructions, runId, analysisId, reviewRepo
|
|
1024
|
+
}) {
|
|
1025
|
+
return runExecutableAnalysis(req, res, {
|
|
1026
|
+
reviewId,
|
|
1027
|
+
review,
|
|
1028
|
+
selectedProvider,
|
|
1029
|
+
selectedModel,
|
|
1030
|
+
repoInstructions,
|
|
1031
|
+
requestInstructions,
|
|
1032
|
+
runId,
|
|
1033
|
+
analysisId,
|
|
1034
|
+
repository,
|
|
1035
|
+
reviewType: review.review_type || 'local',
|
|
1036
|
+
headSha: review.local_head_sha
|
|
1037
|
+
}, {
|
|
1038
|
+
activeAnalyses,
|
|
1039
|
+
reviewToAnalysisId,
|
|
1040
|
+
broadcastProgress,
|
|
1041
|
+
broadcastReviewEvent,
|
|
1042
|
+
registerProcessForCancellation
|
|
1043
|
+
}, {
|
|
1044
|
+
logLabel: `Review #${reviewId}`,
|
|
1045
|
+
buildContext: (r, { selectedModel: model, requestInstructions: customInstructions }) => ({
|
|
1046
|
+
title: null,
|
|
1047
|
+
description: null,
|
|
1048
|
+
cwd: localPath,
|
|
1049
|
+
model,
|
|
1050
|
+
baseSha: null,
|
|
1051
|
+
headSha: r.local_head_sha || null,
|
|
1052
|
+
baseBranch: r.local_base_branch || null,
|
|
1053
|
+
headBranch: r.local_head_branch || null,
|
|
1054
|
+
scopeStart: r.local_scope_start || DEFAULT_SCOPE.start,
|
|
1055
|
+
scopeEnd: r.local_scope_end || DEFAULT_SCOPE.end,
|
|
1056
|
+
customInstructions: customInstructions || null
|
|
1057
|
+
}),
|
|
1058
|
+
buildHookPayload: () => ({
|
|
1059
|
+
mode: review.review_type || 'local',
|
|
1060
|
+
localContext: { path: localPath, branch: review.local_head_branch, headSha: review.local_head_sha }
|
|
1061
|
+
}),
|
|
1062
|
+
onSuccess: async (_db, _runId, { summary }) => {
|
|
1063
|
+
if (summary) {
|
|
1064
|
+
try {
|
|
1065
|
+
await reviewRepo.updateSummary(reviewId, summary);
|
|
1066
|
+
} catch (e) {
|
|
1067
|
+
logger.warn(`Failed to save summary: ${e.message}`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
|
|
976
1074
|
/**
|
|
977
1075
|
* Start Level 1 AI analysis for local review
|
|
978
1076
|
*/
|
|
@@ -987,7 +1085,7 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
|
|
|
987
1085
|
}
|
|
988
1086
|
|
|
989
1087
|
// Extract optional provider, model, tier, customInstructions and skipLevel3 from request body
|
|
990
|
-
const { provider: requestProvider, model: requestModel, tier: requestTier, customInstructions: rawInstructions, skipLevel3: requestSkipLevel3, enabledLevels: requestEnabledLevels } = req.body || {};
|
|
1088
|
+
const { provider: requestProvider, model: requestModel, tier: requestTier, customInstructions: rawInstructions, skipLevel3: requestSkipLevel3, enabledLevels: requestEnabledLevels, excludePrevious } = req.body || {};
|
|
991
1089
|
|
|
992
1090
|
// Trim and validate custom instructions
|
|
993
1091
|
const MAX_INSTRUCTIONS_LENGTH = 5000;
|
|
@@ -1018,10 +1116,15 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
|
|
|
1018
1116
|
const localPath = review.local_path;
|
|
1019
1117
|
const repository = review.repository;
|
|
1020
1118
|
|
|
1119
|
+
// Guard: reject if scope resolves to zero changed files
|
|
1120
|
+
if (await rejectIfEmptyScope(res, review, localPath)) return;
|
|
1121
|
+
|
|
1021
1122
|
// Fetch repo settings for default instructions
|
|
1022
1123
|
const repoSettingsRepo = new RepoSettingsRepository(db);
|
|
1023
1124
|
const repoSettings = repository ? await repoSettingsRepo.getRepoSettings(repository) : null;
|
|
1024
1125
|
|
|
1126
|
+
const appConfig = req.app.get('config') || {};
|
|
1127
|
+
|
|
1025
1128
|
// Determine provider: request body > repo settings > config > default ('claude')
|
|
1026
1129
|
let selectedProvider;
|
|
1027
1130
|
if (requestProvider) {
|
|
@@ -1029,8 +1132,7 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
|
|
|
1029
1132
|
} else if (repoSettings && repoSettings.default_provider) {
|
|
1030
1133
|
selectedProvider = repoSettings.default_provider;
|
|
1031
1134
|
} else {
|
|
1032
|
-
|
|
1033
|
-
selectedProvider = config.default_provider || config.provider || 'claude';
|
|
1135
|
+
selectedProvider = appConfig.default_provider || appConfig.provider || 'claude';
|
|
1034
1136
|
}
|
|
1035
1137
|
|
|
1036
1138
|
// Determine model: request body > repo settings > config/CLI > default
|
|
@@ -1045,8 +1147,10 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
|
|
|
1045
1147
|
|
|
1046
1148
|
// Get repo instructions from settings
|
|
1047
1149
|
const repoInstructions = repoSettings?.default_instructions || null;
|
|
1150
|
+
// Get global instructions from config (loaded at startup from ~/.pair-review/global-instructions.md)
|
|
1151
|
+
const globalInstructions = appConfig.globalInstructions || null;
|
|
1048
1152
|
// Merge for logging purposes (analyzer will also merge internally)
|
|
1049
|
-
const combinedInstructions = mergeInstructions(repoInstructions, requestInstructions);
|
|
1153
|
+
const combinedInstructions = mergeInstructions({ globalInstructions, repoInstructions, requestInstructions });
|
|
1050
1154
|
|
|
1051
1155
|
// Save custom instructions to the review record
|
|
1052
1156
|
// Only update when requestInstructions has a value - updateReview would accept
|
|
@@ -1061,6 +1165,25 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
|
|
|
1061
1165
|
const runId = uuidv4();
|
|
1062
1166
|
const analysisId = runId;
|
|
1063
1167
|
|
|
1168
|
+
// Check if selected provider is an executable provider (external tool)
|
|
1169
|
+
const ProviderClass = getProviderClass(selectedProvider);
|
|
1170
|
+
if (ProviderClass?.isExecutable) {
|
|
1171
|
+
return handleExecutableAnalysis(req, res, {
|
|
1172
|
+
reviewId,
|
|
1173
|
+
review,
|
|
1174
|
+
localPath,
|
|
1175
|
+
repository,
|
|
1176
|
+
selectedProvider,
|
|
1177
|
+
selectedModel,
|
|
1178
|
+
repoInstructions,
|
|
1179
|
+
requestInstructions,
|
|
1180
|
+
combinedInstructions,
|
|
1181
|
+
runId,
|
|
1182
|
+
analysisId,
|
|
1183
|
+
reviewRepo
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1064
1187
|
// Extract scope early — needed for both analysis run creation and diff generation
|
|
1065
1188
|
const scopeStart = review.local_scope_start || DEFAULT_SCOPE.start;
|
|
1066
1189
|
const scopeEnd = review.local_scope_end || DEFAULT_SCOPE.end;
|
|
@@ -1076,6 +1199,7 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
|
|
|
1076
1199
|
provider: selectedProvider,
|
|
1077
1200
|
model: selectedModel,
|
|
1078
1201
|
tier,
|
|
1202
|
+
globalInstructions,
|
|
1079
1203
|
repoInstructions,
|
|
1080
1204
|
requestInstructions,
|
|
1081
1205
|
headSha: review.local_head_sha || null,
|
|
@@ -1181,7 +1305,7 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
|
|
|
1181
1305
|
const progressCallback = createProgressCallback(analysisId);
|
|
1182
1306
|
|
|
1183
1307
|
// Start analysis asynchronously (skipRunCreation since we created the record above; also passes changedFiles for local mode path validation, tier for prompt selection, and skipLevel3 flag)
|
|
1184
|
-
analyzer.analyzeLevel1(reviewId, localPath, localMetadata, progressCallback, { repoInstructions, requestInstructions }, changedFiles, { analysisId, runId, skipRunCreation: true, tier, skipLevel3: requestSkipLevel3, enabledLevels: levelsConfig })
|
|
1308
|
+
analyzer.analyzeLevel1(reviewId, localPath, localMetadata, progressCallback, { globalInstructions, repoInstructions, requestInstructions }, changedFiles, { analysisId, runId, skipRunCreation: true, tier, skipLevel3: requestSkipLevel3, enabledLevels: levelsConfig, excludePrevious, serverPort: req.socket.localPort })
|
|
1185
1309
|
.then(async result => {
|
|
1186
1310
|
logger.section('Local Analysis Results');
|
|
1187
1311
|
logger.success(`Analysis complete for local review #${reviewId}`);
|
|
@@ -1851,7 +1975,7 @@ router.post('/api/local/:reviewId/review-settings', async (req, res) => {
|
|
|
1851
1975
|
router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
|
|
1852
1976
|
try {
|
|
1853
1977
|
const reviewId = parseInt(req.params.reviewId, 10);
|
|
1854
|
-
const { councilId, councilConfig: inlineConfig, customInstructions: rawInstructions, configType: requestConfigType } = req.body || {};
|
|
1978
|
+
const { councilId, councilConfig: inlineConfig, customInstructions: rawInstructions, configType: requestConfigType, excludePrevious } = req.body || {};
|
|
1855
1979
|
|
|
1856
1980
|
if (isNaN(reviewId) || reviewId <= 0) {
|
|
1857
1981
|
return res.status(400).json({ error: 'Invalid review ID' });
|
|
@@ -1894,6 +2018,9 @@ router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
|
|
|
1894
2018
|
|
|
1895
2019
|
const localPath = review.local_path;
|
|
1896
2020
|
|
|
2021
|
+
// Guard: reject if scope resolves to zero changed files
|
|
2022
|
+
if (await rejectIfEmptyScope(res, review, localPath)) return;
|
|
2023
|
+
|
|
1897
2024
|
const councilScopeStart = review.local_scope_start || DEFAULT_SCOPE.start;
|
|
1898
2025
|
const councilScopeEnd = review.local_scope_end || DEFAULT_SCOPE.end;
|
|
1899
2026
|
const councilHasBranch = includesBranch(councilScopeStart);
|
|
@@ -1914,7 +2041,11 @@ router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
|
|
|
1914
2041
|
title: review.name || (councilHasBranch ? `Branch changes: ${review.local_base_branch}..HEAD` : 'Local changes'),
|
|
1915
2042
|
description: '',
|
|
1916
2043
|
base_sha: analysisBaseSha,
|
|
1917
|
-
head_sha: review.local_head_sha
|
|
2044
|
+
head_sha: review.local_head_sha,
|
|
2045
|
+
base_branch: review.local_base_branch || null,
|
|
2046
|
+
head_branch: review.local_head_branch || null,
|
|
2047
|
+
scopeStart: councilScopeStart,
|
|
2048
|
+
scopeEnd: councilScopeEnd,
|
|
1918
2049
|
};
|
|
1919
2050
|
|
|
1920
2051
|
const analyzer = new Analyzer(db, 'council', 'council');
|
|
@@ -1947,6 +2078,7 @@ router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
|
|
|
1947
2078
|
|
|
1948
2079
|
// Import launchCouncilAnalysis from analyses.js
|
|
1949
2080
|
const analysesRouter = require('./analyses');
|
|
2081
|
+
const localCouncilConfig = req.app.get('config') || {};
|
|
1950
2082
|
const { analysisId, runId } = await analysesRouter.launchCouncilAnalysis(
|
|
1951
2083
|
db,
|
|
1952
2084
|
{
|
|
@@ -1958,7 +2090,9 @@ router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
|
|
|
1958
2090
|
headSha: review.local_head_sha,
|
|
1959
2091
|
logLabel: `local review #${reviewId}`,
|
|
1960
2092
|
initialStatusExtra: { reviewId, reviewType: 'local' },
|
|
1961
|
-
config:
|
|
2093
|
+
config: localCouncilConfig,
|
|
2094
|
+
excludePrevious,
|
|
2095
|
+
serverPort: req.socket.localPort,
|
|
1962
2096
|
hookContext: {
|
|
1963
2097
|
mode: 'local',
|
|
1964
2098
|
localContext: { path: localPath, branch: review.local_head_branch, headSha: review.local_head_sha },
|
|
@@ -1967,7 +2101,7 @@ router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
|
|
|
1967
2101
|
},
|
|
1968
2102
|
councilConfig,
|
|
1969
2103
|
councilId,
|
|
1970
|
-
{ repoInstructions, requestInstructions },
|
|
2104
|
+
{ globalInstructions: localCouncilConfig.globalInstructions || null, repoInstructions, requestInstructions },
|
|
1971
2105
|
configType
|
|
1972
2106
|
);
|
|
1973
2107
|
|
package/src/routes/mcp.js
CHANGED
|
@@ -411,7 +411,7 @@ function createMCPServer(db, options = {}) {
|
|
|
411
411
|
SELECT
|
|
412
412
|
id, ai_run_id, ai_level, ai_confidence,
|
|
413
413
|
file, line_start, line_end, type, title, body,
|
|
414
|
-
reasoning, status, is_file_level, created_at
|
|
414
|
+
reasoning, status, is_file_level, severity, created_at
|
|
415
415
|
FROM comments
|
|
416
416
|
WHERE ${conditions.join('\n AND ')}
|
|
417
417
|
ORDER BY file, line_start
|
|
@@ -433,6 +433,7 @@ function createMCPServer(db, options = {}) {
|
|
|
433
433
|
body: s.body,
|
|
434
434
|
type: s.type,
|
|
435
435
|
ai_confidence: s.ai_confidence,
|
|
436
|
+
severity: s.severity,
|
|
436
437
|
status: s.status,
|
|
437
438
|
reasoning: safeParseJson(s.reasoning),
|
|
438
439
|
}))
|
|
@@ -457,6 +458,10 @@ function createMCPServer(db, options = {}) {
|
|
|
457
458
|
.describe('Whether to skip Level 3 (codebase context) analysis'),
|
|
458
459
|
tier: z.enum(ALL_TIER_VALUES).default('balanced')
|
|
459
460
|
.describe('Analysis tier: fast (surface), balanced (standard), or thorough (deep)'),
|
|
461
|
+
excludePrevious: z.object({
|
|
462
|
+
github: z.boolean().optional().describe('Exclude GitHub PR inline review comments'),
|
|
463
|
+
feedback: z.boolean().optional().describe('Exclude existing pair-review suggestions and comments')
|
|
464
|
+
}).optional().describe('Exclude previously identified issues from results'),
|
|
460
465
|
},
|
|
461
466
|
async (args) => {
|
|
462
467
|
// Track analysisId and reviewId for cleanup in catch block (must be outside try scope)
|
|
@@ -630,7 +635,7 @@ function createMCPServer(db, options = {}) {
|
|
|
630
635
|
});
|
|
631
636
|
|
|
632
637
|
// Launch analysis asynchronously (skipRunCreation since we created the record above)
|
|
633
|
-
analyzer.analyzeLevel1(reviewId, localPath, localMetadata, progressCallback, { repoInstructions, requestInstructions }, changedFiles, { analysisId, runId, skipRunCreation: true, tier, skipLevel3: args.skipLevel3 })
|
|
638
|
+
analyzer.analyzeLevel1(reviewId, localPath, localMetadata, progressCallback, { repoInstructions, requestInstructions }, changedFiles, { analysisId, runId, skipRunCreation: true, tier, skipLevel3: args.skipLevel3, excludePrevious: args.excludePrevious, serverPort: (options.port || config.port || 7247) })
|
|
634
639
|
.then(result => handleAnalysisCompletion(analysisId, runId, result, async (r) => {
|
|
635
640
|
if (r.summary) {
|
|
636
641
|
try { await reviewRepo.updateSummary(reviewId, r.summary); } catch (_) { /* ignore */ }
|
|
@@ -763,7 +768,7 @@ function createMCPServer(db, options = {}) {
|
|
|
763
768
|
});
|
|
764
769
|
|
|
765
770
|
// Launch analysis asynchronously (skipRunCreation since we created the record above)
|
|
766
|
-
analyzer.analyzeLevel1(review.id, worktreePath, prMetadata, progressCallback, { repoInstructions, requestInstructions }, null, { analysisId, runId, skipRunCreation: true, tier, skipLevel3: args.skipLevel3 })
|
|
771
|
+
analyzer.analyzeLevel1(review.id, worktreePath, prMetadata, progressCallback, { repoInstructions, requestInstructions }, null, { analysisId, runId, skipRunCreation: true, tier, skipLevel3: args.skipLevel3, excludePrevious: args.excludePrevious, serverPort: (options.port || config.port || 7247) })
|
|
767
772
|
.then(result => handleAnalysisCompletion(analysisId, runId, result, async (r) => {
|
|
768
773
|
try { await prMetadataRepo.updateLastAiRunId(prMetadata.id, r.runId); } catch (_) { /* ignore */ }
|
|
769
774
|
if (r.summary) {
|
|
@@ -837,7 +842,7 @@ router.post('/mcp', async (req, res) => {
|
|
|
837
842
|
try {
|
|
838
843
|
const db = req.app.get('db');
|
|
839
844
|
const config = req.app.get('config') || {};
|
|
840
|
-
const server = createMCPServer(db, { config });
|
|
845
|
+
const server = createMCPServer(db, { config, port: req.socket.localPort });
|
|
841
846
|
|
|
842
847
|
const transport = new StreamableHTTPServerTransport({
|
|
843
848
|
sessionIdGenerator: undefined,
|