@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.
Files changed (79) 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 +83 -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 +83 -8
  21. package/public/js/components/AnalysisConfigModal.js +155 -5
  22. package/public/js/components/ChatPanel.js +22 -5
  23. package/public/js/components/CouncilProgressModal.js +239 -22
  24. package/public/js/components/TimeoutSelect.js +2 -0
  25. package/public/js/components/VoiceCentricConfigTab.js +179 -12
  26. package/public/js/index.js +119 -1
  27. package/public/js/local.js +141 -47
  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 +534 -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 +89 -4
  59. package/src/chat/prompt-builder.js +17 -1
  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 +133 -52
  64. package/src/local-review.js +15 -9
  65. package/src/main.js +3 -2
  66. package/src/routes/analyses.js +34 -8
  67. package/src/routes/chat.js +15 -8
  68. package/src/routes/config.js +3 -120
  69. package/src/routes/councils.js +15 -6
  70. package/src/routes/executable-analysis.js +494 -0
  71. package/src/routes/local.js +160 -26
  72. package/src/routes/mcp.js +9 -4
  73. package/src/routes/pr.js +166 -29
  74. package/src/routes/reviews.js +31 -5
  75. package/src/routes/shared.js +72 -5
  76. package/src/routes/worktrees.js +4 -2
  77. package/src/utils/comment-formatter.js +28 -11
  78. package/src/utils/instructions.js +22 -8
  79. package/src/utils/logger.js +20 -10
@@ -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
- const { getDefaultBranch } = require('../git/base-branch');
83
- const defaultBranch = localPath ? getDefaultBranch(localPath) : null;
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, regenerate the diff with whitespace changes hidden (transient view, not cached)
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, review.local_base_branch, { hideWhitespace: true });
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 whitespace-filtered diff for review #${reviewId}: ${wsError.message}`);
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
- const config = req.app.get('config') || {};
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: req.app.get('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,