@in-the-loop-labs/pair-review 3.0.6 → 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 +113 -29
  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 +152 -15
  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
@@ -32,8 +32,10 @@ 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
34
  const { getProviderClass, createProvider } = require('../ai/provider');
35
- const { getDefaultBranch } = require('../git/base-branch');
35
+ const { readFileSync } = require('fs');
36
+ const { getDefaultBranch, tryGraphiteState, readGraphitePRInfo, enrichStackWithPRInfo } = require('../git/base-branch');
36
37
  const { CommentRepository } = require('../database');
38
+ const { runExecutableAnalysis, getChangedFiles } = require('./executable-analysis');
37
39
  const {
38
40
  activeAnalyses,
39
41
  localReviewDiffs,
@@ -43,7 +45,8 @@ const {
43
45
  broadcastProgress,
44
46
  CancellationError,
45
47
  createProgressCallback,
46
- parseEnabledLevels
48
+ parseEnabledLevels,
49
+ registerProcess: registerProcessForCancellation
47
50
  } = require('./shared');
48
51
 
49
52
  const router = express.Router();
@@ -69,6 +72,26 @@ function deleteLocalReviewDiff(reviewId) {
69
72
  localReviewDiffs.delete(toIntKey(reviewId));
70
73
  }
71
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
+
72
95
  /**
73
96
  * Check whether branch scope should be selectable in the scope range selector.
74
97
  * Returns true when the current branch is a non-default, non-detached branch,
@@ -599,6 +622,24 @@ router.get('/api/local/:reviewId', async (req, res) => {
599
622
 
600
623
  // Compute SHA abbreviation length from the repo's git config
601
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
+
602
643
  const metadataElapsed = Date.now() - tEndpoint;
603
644
  if (metadataElapsed > 200) {
604
645
  logger.debug(`[perf] metadata#${reviewId} took ${metadataElapsed}ms (threshold: 200ms)`);
@@ -619,6 +660,7 @@ router.get('/api/local/:reviewId', async (req, res) => {
619
660
  baseBranch,
620
661
  branchInfo,
621
662
  branchAvailable,
663
+ stackData,
622
664
  shaAbbrevLength,
623
665
  createdAt: review.created_at,
624
666
  updatedAt: review.updated_at
@@ -742,18 +784,20 @@ router.get('/api/local/:reviewId/diff', async (req, res) => {
742
784
  });
743
785
  }
744
786
 
745
- // 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)
746
788
  const hideWhitespace = req.query.w === '1';
789
+ const baseBranchOverride = req.query.base;
747
790
  const scopeStart = review.local_scope_start || DEFAULT_SCOPE.start;
748
791
  const scopeEnd = review.local_scope_end || DEFAULT_SCOPE.end;
792
+ const baseBranch = baseBranchOverride || review.local_base_branch;
749
793
  let diffData;
750
794
 
751
- if (hideWhitespace && review.local_path) {
795
+ if ((hideWhitespace || baseBranchOverride) && review.local_path) {
752
796
  try {
753
- 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 });
754
798
  diffData = { diff: wsResult.diff, stats: wsResult.stats };
755
799
  } catch (wsError) {
756
- 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}`);
757
801
  // Fall through to cached diff below
758
802
  }
759
803
  }
@@ -970,6 +1014,63 @@ router.get('/api/local/:reviewId/check-stale', async (req, res) => {
970
1014
  }
971
1015
  });
972
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
+
973
1074
  /**
974
1075
  * Start Level 1 AI analysis for local review
975
1076
  */
@@ -984,7 +1085,7 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
984
1085
  }
985
1086
 
986
1087
  // Extract optional provider, model, tier, customInstructions and skipLevel3 from request body
987
- 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 || {};
988
1089
 
989
1090
  // Trim and validate custom instructions
990
1091
  const MAX_INSTRUCTIONS_LENGTH = 5000;
@@ -1015,10 +1116,15 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
1015
1116
  const localPath = review.local_path;
1016
1117
  const repository = review.repository;
1017
1118
 
1119
+ // Guard: reject if scope resolves to zero changed files
1120
+ if (await rejectIfEmptyScope(res, review, localPath)) return;
1121
+
1018
1122
  // Fetch repo settings for default instructions
1019
1123
  const repoSettingsRepo = new RepoSettingsRepository(db);
1020
1124
  const repoSettings = repository ? await repoSettingsRepo.getRepoSettings(repository) : null;
1021
1125
 
1126
+ const appConfig = req.app.get('config') || {};
1127
+
1022
1128
  // Determine provider: request body > repo settings > config > default ('claude')
1023
1129
  let selectedProvider;
1024
1130
  if (requestProvider) {
@@ -1026,8 +1132,7 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
1026
1132
  } else if (repoSettings && repoSettings.default_provider) {
1027
1133
  selectedProvider = repoSettings.default_provider;
1028
1134
  } else {
1029
- const config = req.app.get('config') || {};
1030
- selectedProvider = config.default_provider || config.provider || 'claude';
1135
+ selectedProvider = appConfig.default_provider || appConfig.provider || 'claude';
1031
1136
  }
1032
1137
 
1033
1138
  // Determine model: request body > repo settings > config/CLI > default
@@ -1042,8 +1147,10 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
1042
1147
 
1043
1148
  // Get repo instructions from settings
1044
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;
1045
1152
  // Merge for logging purposes (analyzer will also merge internally)
1046
- const combinedInstructions = mergeInstructions(repoInstructions, requestInstructions);
1153
+ const combinedInstructions = mergeInstructions({ globalInstructions, repoInstructions, requestInstructions });
1047
1154
 
1048
1155
  // Save custom instructions to the review record
1049
1156
  // Only update when requestInstructions has a value - updateReview would accept
@@ -1058,6 +1165,25 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
1058
1165
  const runId = uuidv4();
1059
1166
  const analysisId = runId;
1060
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
+
1061
1187
  // Extract scope early — needed for both analysis run creation and diff generation
1062
1188
  const scopeStart = review.local_scope_start || DEFAULT_SCOPE.start;
1063
1189
  const scopeEnd = review.local_scope_end || DEFAULT_SCOPE.end;
@@ -1073,6 +1199,7 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
1073
1199
  provider: selectedProvider,
1074
1200
  model: selectedModel,
1075
1201
  tier,
1202
+ globalInstructions,
1076
1203
  repoInstructions,
1077
1204
  requestInstructions,
1078
1205
  headSha: review.local_head_sha || null,
@@ -1178,7 +1305,7 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
1178
1305
  const progressCallback = createProgressCallback(analysisId);
1179
1306
 
1180
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)
1181
- 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 })
1182
1309
  .then(async result => {
1183
1310
  logger.section('Local Analysis Results');
1184
1311
  logger.success(`Analysis complete for local review #${reviewId}`);
@@ -1848,7 +1975,7 @@ router.post('/api/local/:reviewId/review-settings', async (req, res) => {
1848
1975
  router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
1849
1976
  try {
1850
1977
  const reviewId = parseInt(req.params.reviewId, 10);
1851
- const { councilId, councilConfig: inlineConfig, customInstructions: rawInstructions, configType: requestConfigType } = req.body || {};
1978
+ const { councilId, councilConfig: inlineConfig, customInstructions: rawInstructions, configType: requestConfigType, excludePrevious } = req.body || {};
1852
1979
 
1853
1980
  if (isNaN(reviewId) || reviewId <= 0) {
1854
1981
  return res.status(400).json({ error: 'Invalid review ID' });
@@ -1891,6 +2018,9 @@ router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
1891
2018
 
1892
2019
  const localPath = review.local_path;
1893
2020
 
2021
+ // Guard: reject if scope resolves to zero changed files
2022
+ if (await rejectIfEmptyScope(res, review, localPath)) return;
2023
+
1894
2024
  const councilScopeStart = review.local_scope_start || DEFAULT_SCOPE.start;
1895
2025
  const councilScopeEnd = review.local_scope_end || DEFAULT_SCOPE.end;
1896
2026
  const councilHasBranch = includesBranch(councilScopeStart);
@@ -1911,7 +2041,11 @@ router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
1911
2041
  title: review.name || (councilHasBranch ? `Branch changes: ${review.local_base_branch}..HEAD` : 'Local changes'),
1912
2042
  description: '',
1913
2043
  base_sha: analysisBaseSha,
1914
- 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,
1915
2049
  };
1916
2050
 
1917
2051
  const analyzer = new Analyzer(db, 'council', 'council');
@@ -1944,6 +2078,7 @@ router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
1944
2078
 
1945
2079
  // Import launchCouncilAnalysis from analyses.js
1946
2080
  const analysesRouter = require('./analyses');
2081
+ const localCouncilConfig = req.app.get('config') || {};
1947
2082
  const { analysisId, runId } = await analysesRouter.launchCouncilAnalysis(
1948
2083
  db,
1949
2084
  {
@@ -1955,7 +2090,9 @@ router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
1955
2090
  headSha: review.local_head_sha,
1956
2091
  logLabel: `local review #${reviewId}`,
1957
2092
  initialStatusExtra: { reviewId, reviewType: 'local' },
1958
- config: req.app.get('config') || {},
2093
+ config: localCouncilConfig,
2094
+ excludePrevious,
2095
+ serverPort: req.socket.localPort,
1959
2096
  hookContext: {
1960
2097
  mode: 'local',
1961
2098
  localContext: { path: localPath, branch: review.local_head_branch, headSha: review.local_head_sha },
@@ -1964,7 +2101,7 @@ router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
1964
2101
  },
1965
2102
  councilConfig,
1966
2103
  councilId,
1967
- { repoInstructions, requestInstructions },
2104
+ { globalInstructions: localCouncilConfig.globalInstructions || null, repoInstructions, requestInstructions },
1968
2105
  configType
1969
2106
  );
1970
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,