@in-the-loop-labs/pair-review 2.6.3 → 3.0.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 (150) hide show
  1. package/.pi/extensions/task/index.ts +1 -1
  2. package/.pi/skills/review-roulette/SKILL.md +1 -1
  3. package/LICENSE +201 -674
  4. package/README.md +2 -2
  5. package/bin/pair-review.js +1 -1
  6. package/package.json +2 -2
  7. package/plugin/.claude-plugin/plugin.json +2 -2
  8. package/plugin-code-critic/.claude-plugin/plugin.json +2 -2
  9. package/plugin-code-critic/skills/analyze/scripts/git-diff-lines +1 -1
  10. package/public/css/ai-summary-modal.css +1 -1
  11. package/public/css/pr.css +194 -0
  12. package/public/index.html +168 -3
  13. package/public/js/components/AIPanel.js +17 -3
  14. package/public/js/components/AISummaryModal.js +1 -1
  15. package/public/js/components/AdvancedConfigTab.js +1 -1
  16. package/public/js/components/AnalysisConfigModal.js +1 -1
  17. package/public/js/components/ChatPanel.js +42 -7
  18. package/public/js/components/ConfirmDialog.js +22 -3
  19. package/public/js/components/CouncilProgressModal.js +14 -1
  20. package/public/js/components/DiffOptionsDropdown.js +411 -24
  21. package/public/js/components/EmojiPicker.js +1 -1
  22. package/public/js/components/KeyboardShortcuts.js +1 -1
  23. package/public/js/components/PanelGroup.js +1 -1
  24. package/public/js/components/PreviewModal.js +1 -1
  25. package/public/js/components/ReviewModal.js +1 -1
  26. package/public/js/components/SplitButton.js +1 -1
  27. package/public/js/components/StatusIndicator.js +1 -1
  28. package/public/js/components/SuggestionNavigator.js +13 -6
  29. package/public/js/components/TabTitle.js +96 -0
  30. package/public/js/components/TextInputDialog.js +1 -1
  31. package/public/js/components/TimeoutSelect.js +1 -1
  32. package/public/js/components/Toast.js +7 -1
  33. package/public/js/components/VoiceCentricConfigTab.js +1 -1
  34. package/public/js/index.js +649 -44
  35. package/public/js/local.js +570 -77
  36. package/public/js/modules/analysis-history.js +4 -3
  37. package/public/js/modules/comment-manager.js +6 -1
  38. package/public/js/modules/comment-minimizer.js +304 -0
  39. package/public/js/modules/diff-context.js +1 -1
  40. package/public/js/modules/diff-renderer.js +1 -1
  41. package/public/js/modules/file-comment-manager.js +1 -1
  42. package/public/js/modules/file-list-merger.js +1 -1
  43. package/public/js/modules/gap-coordinates.js +1 -1
  44. package/public/js/modules/hunk-parser.js +1 -1
  45. package/public/js/modules/line-tracker.js +1 -1
  46. package/public/js/modules/panel-resizer.js +1 -1
  47. package/public/js/modules/storage-cleanup.js +1 -1
  48. package/public/js/modules/suggestion-manager.js +1 -1
  49. package/public/js/pr.js +83 -7
  50. package/public/js/repo-settings.js +1 -1
  51. package/public/js/utils/category-emoji.js +1 -1
  52. package/public/js/utils/file-order.js +1 -1
  53. package/public/js/utils/markdown.js +1 -1
  54. package/public/js/utils/suggestion-ui.js +1 -1
  55. package/public/js/utils/tier-icons.js +1 -1
  56. package/public/js/utils/time.js +1 -1
  57. package/public/js/ws-client.js +1 -1
  58. package/public/local.html +14 -0
  59. package/public/pr.html +3 -0
  60. package/public/setup.html +1 -1
  61. package/src/ai/analyzer.js +18 -12
  62. package/src/ai/claude-cli.js +1 -1
  63. package/src/ai/claude-provider.js +1 -1
  64. package/src/ai/codex-provider.js +1 -1
  65. package/src/ai/copilot-provider.js +1 -1
  66. package/src/ai/cursor-agent-provider.js +1 -1
  67. package/src/ai/gemini-provider.js +1 -1
  68. package/src/ai/index.js +1 -1
  69. package/src/ai/opencode-provider.js +1 -1
  70. package/src/ai/pi-provider.js +1 -1
  71. package/src/ai/prompts/baseline/consolidation/balanced.js +1 -1
  72. package/src/ai/prompts/baseline/consolidation/fast.js +1 -1
  73. package/src/ai/prompts/baseline/consolidation/thorough.js +1 -1
  74. package/src/ai/prompts/baseline/level1/balanced.js +1 -1
  75. package/src/ai/prompts/baseline/level1/fast.js +1 -1
  76. package/src/ai/prompts/baseline/level1/thorough.js +1 -1
  77. package/src/ai/prompts/baseline/level2/balanced.js +1 -1
  78. package/src/ai/prompts/baseline/level2/fast.js +1 -1
  79. package/src/ai/prompts/baseline/level2/thorough.js +1 -1
  80. package/src/ai/prompts/baseline/level3/balanced.js +1 -1
  81. package/src/ai/prompts/baseline/level3/fast.js +1 -1
  82. package/src/ai/prompts/baseline/level3/thorough.js +1 -1
  83. package/src/ai/prompts/baseline/orchestration/balanced.js +1 -1
  84. package/src/ai/prompts/baseline/orchestration/fast.js +1 -1
  85. package/src/ai/prompts/baseline/orchestration/thorough.js +1 -1
  86. package/src/ai/prompts/config.js +1 -1
  87. package/src/ai/prompts/index.js +1 -1
  88. package/src/ai/prompts/line-number-guidance.js +1 -1
  89. package/src/ai/prompts/render-for-skill.js +1 -1
  90. package/src/ai/prompts/shared/diff-instructions.js +1 -1
  91. package/src/ai/prompts/shared/output-schema.js +1 -1
  92. package/src/ai/prompts/shared/valid-files.js +1 -1
  93. package/src/ai/prompts/sparse-checkout-guidance.js +1 -1
  94. package/src/ai/provider-availability.js +1 -1
  95. package/src/ai/provider.js +1 -1
  96. package/src/ai/stream-parser.js +1 -1
  97. package/src/chat/acp-bridge.js +1 -1
  98. package/src/chat/api-reference.js +1 -1
  99. package/src/chat/chat-providers.js +1 -1
  100. package/src/chat/claude-code-bridge.js +1 -1
  101. package/src/chat/codex-bridge.js +1 -1
  102. package/src/chat/pi-bridge.js +1 -1
  103. package/src/chat/prompt-builder.js +1 -1
  104. package/src/chat/session-manager.js +1 -1
  105. package/src/config.js +3 -1
  106. package/src/database.js +591 -40
  107. package/src/events/review-events.js +1 -1
  108. package/src/git/base-branch.js +173 -0
  109. package/src/git/gitattributes.js +1 -1
  110. package/src/git/sha-abbrev.js +35 -0
  111. package/src/git/worktree.js +1 -1
  112. package/src/github/client.js +33 -2
  113. package/src/github/parser.js +1 -1
  114. package/src/hooks/hook-runner.js +100 -0
  115. package/src/hooks/payloads.js +212 -0
  116. package/src/local-review.js +469 -130
  117. package/src/local-scope.js +58 -0
  118. package/src/main.js +56 -5
  119. package/src/mcp-stdio.js +1 -1
  120. package/src/protocol-handler.js +1 -1
  121. package/src/routes/analyses.js +74 -11
  122. package/src/routes/chat.js +34 -1
  123. package/src/routes/config.js +2 -1
  124. package/src/routes/context-files.js +1 -1
  125. package/src/routes/councils.js +1 -1
  126. package/src/routes/github-collections.js +1 -1
  127. package/src/routes/local.js +735 -69
  128. package/src/routes/mcp.js +21 -11
  129. package/src/routes/pr.js +91 -13
  130. package/src/routes/reviews.js +1 -1
  131. package/src/routes/setup.js +2 -1
  132. package/src/routes/shared.js +1 -1
  133. package/src/routes/worktrees.js +213 -149
  134. package/src/server.js +31 -1
  135. package/src/setup/local-setup.js +47 -6
  136. package/src/setup/pr-setup.js +29 -6
  137. package/src/utils/auto-context.js +1 -1
  138. package/src/utils/category-emoji.js +1 -1
  139. package/src/utils/comment-formatter.js +1 -1
  140. package/src/utils/diff-annotator.js +1 -1
  141. package/src/utils/diff-file-list.js +1 -1
  142. package/src/utils/instructions.js +1 -1
  143. package/src/utils/json-extractor.js +1 -1
  144. package/src/utils/line-validation.js +1 -1
  145. package/src/utils/logger.js +1 -1
  146. package/src/utils/paths.js +1 -1
  147. package/src/utils/safe-parse-json.js +1 -1
  148. package/src/utils/stats-calculator.js +1 -1
  149. package/src/ws/index.js +1 -1
  150. package/src/ws/server.js +1 -1
package/src/routes/mcp.js CHANGED
@@ -1,4 +1,4 @@
1
- // SPDX-License-Identifier: GPL-3.0-or-later
1
+ // Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
2
2
  const express = require('express');
3
3
  const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
4
4
  const { StreamableHTTPServerTransport } = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
@@ -11,6 +11,7 @@ const { getTierForModel } = require('../ai/provider');
11
11
  const { TIERS, TIER_ALIASES, resolveTier } = require('../ai/prompts/config');
12
12
  const { GitWorktreeManager } = require('../git/worktree');
13
13
  const path = require('path');
14
+ const { getCurrentBranch } = require('../local-review');
14
15
  const { normalizeRepository } = require('../utils/paths');
15
16
  const logger = require('../utils/logger');
16
17
  const { broadcastReviewEvent } = require('../events/review-events');
@@ -118,7 +119,11 @@ async function resolveReview(args, db) {
118
119
  const reviewRepo = new ReviewRepository(db);
119
120
 
120
121
  if (args.path && args.headSha) {
121
- const review = await reviewRepo.getLocalReview(args.path, args.headSha);
122
+ let headBranch = args.branch;
123
+ if (!headBranch) {
124
+ try { headBranch = await getCurrentBranch(args.path); } catch (_) { /* non-fatal */ }
125
+ }
126
+ const review = await reviewRepo.findLocalReview(args.path, args.headSha, headBranch);
122
127
  if (!review) {
123
128
  return { review: null, error: `No local review found for path "${args.path}" with HEAD SHA "${args.headSha}"` };
124
129
  }
@@ -507,22 +512,27 @@ function createMCPServer(db, options = {}) {
507
512
  const localPath = args.path;
508
513
  const localHeadSha = args.headSha;
509
514
 
515
+ // Resolve current branch for session identity
516
+ let localHeadBranch;
517
+ try { localHeadBranch = await getCurrentBranch(localPath); } catch (_) { /* non-fatal */ }
518
+
510
519
  // Look up or create local review record
511
- // Try to get repository name from existing review, else use directory basename
520
+ let reviewId;
512
521
  let repository;
513
- const existingReview = await reviewRepo.getLocalReview(localPath, localHeadSha);
522
+ const existingReview = await reviewRepo.findLocalReview(localPath, localHeadSha, localHeadBranch);
514
523
  if (existingReview) {
524
+ reviewId = existingReview.id;
515
525
  repository = existingReview.repository;
516
526
  } else {
517
527
  repository = path.basename(localPath);
528
+ reviewId = await reviewRepo.upsertLocalReview({
529
+ localPath,
530
+ localHeadSha,
531
+ repository,
532
+ localHeadBranch
533
+ });
518
534
  }
519
535
 
520
- const reviewId = await reviewRepo.upsertLocalReview({
521
- localPath,
522
- localHeadSha,
523
- repository
524
- });
525
-
526
536
  // Concurrent analysis guard: check if one is already running
527
537
  const existingAnalysisId = reviewToAnalysisId.get(reviewId);
528
538
  if (existingAnalysisId && activeAnalyses.get(existingAnalysisId)?.status === 'running') {
@@ -687,7 +697,7 @@ function createMCPServer(db, options = {}) {
687
697
  const worktreePath = await worktreeManager.getWorktreePath({ owner, repo, number: prNumber });
688
698
 
689
699
  // Get or create review record
690
- const review = await reviewRepo.getOrCreate({ prNumber, repository });
700
+ const { review } = await reviewRepo.getOrCreate({ prNumber, repository });
691
701
 
692
702
  // Resolve provider and model
693
703
  const repoSettings = await repoSettingsRepo.getRepoSettings(repository);
package/src/routes/pr.js CHANGED
@@ -1,4 +1,4 @@
1
- // SPDX-License-Identifier: GPL-3.0-or-later
1
+ // Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
2
2
  /**
3
3
  * Core PR Routes
4
4
  *
@@ -18,6 +18,7 @@ const { query, queryOne, run, withTransaction, WorktreeRepository, ReviewReposit
18
18
  const { GitWorktreeManager } = require('../git/worktree');
19
19
  const { GitHubClient } = require('../github/client');
20
20
  const { getGeneratedFilePatterns } = require('../git/gitattributes');
21
+ const { getShaAbbrevLength, DEFAULT_SHA_ABBREV_LENGTH } = require('../git/sha-abbrev');
21
22
  const { normalizeRepository, resolveRenamedFile, resolveRenamedFileOld } = require('../utils/paths');
22
23
  const { mergeInstructions } = require('../utils/instructions');
23
24
  const Analyzer = require('../ai/analyzer');
@@ -28,6 +29,8 @@ const { getGitHubToken } = require('../config');
28
29
  const logger = require('../utils/logger');
29
30
  const { buildDiffLineSet } = require('../utils/diff-annotator');
30
31
  const { broadcastReviewEvent } = require('../events/review-events');
32
+ const { fireHooks, hasHooks } = require('../hooks/hook-runner');
33
+ const { buildReviewStartedPayload, buildReviewLoadedPayload, buildAnalysisStartedPayload, buildAnalysisCompletedPayload, getCachedUser } = require('../hooks/payloads');
31
34
  const simpleGit = require('simple-git');
32
35
  const {
33
36
  activeAnalyses,
@@ -179,11 +182,12 @@ router.get('/api/pr/:owner/:repo/:number', async (req, res) => {
179
182
  });
180
183
  }
181
184
 
182
- // Get review record if it exists (don't create on GET - REST compliance)
183
- // The review.id is used for comments to avoid ID collision with local mode
185
+ // Eagerly create the review record on first visit so all subsequent
186
+ // events (analysis, comments, hooks) always have a real reviewId.
187
+ // To distinguish new vs returning, snapshot the current record first.
184
188
  const db = req.app.get('db');
185
189
  const reviewRepo = new ReviewRepository(db);
186
- const review = await reviewRepo.getReviewByPR(prNumber, repository);
190
+ const { review, created: isNewReview } = await reviewRepo.getOrCreate({ prNumber, repository });
187
191
 
188
192
  // Parse extended PR data
189
193
  let extendedData = {};
@@ -196,10 +200,9 @@ router.get('/api/pr/:owner/:repo/:number', async (req, res) => {
196
200
  // Parse owner and repo from repository field
197
201
  const [repoOwner, repoName] = repository.split('/');
198
202
 
199
- // Check for pending GitHub draft if we have a review record
200
- // This avoids unnecessary GitHub API calls for PRs the user hasn't started reviewing
203
+ // Check for pending GitHub draft
201
204
  let pendingDraft = null;
202
- if (review) {
205
+ {
203
206
  const config = req.app.get('config');
204
207
  const githubToken = getGitHubToken(config || {}) || req.app.get('githubToken');
205
208
 
@@ -220,13 +223,17 @@ router.get('/api/pr/:owner/:repo/:number', async (req, res) => {
220
223
  }
221
224
  }
222
225
 
226
+ // Compute SHA abbreviation length from the worktree's git config
227
+ const shaAbbrevLength = extendedData.worktree_path
228
+ ? getShaAbbrevLength(extendedData.worktree_path)
229
+ : DEFAULT_SHA_ABBREV_LENGTH;
230
+
223
231
  // Prepare response
224
232
  // Use review.id instead of prMetadata.id to avoid ID collision with local mode
225
- // When no review exists yet, id will be null
226
233
  const response = {
227
234
  success: true,
228
235
  data: {
229
- id: review ? review.id : null,
236
+ id: review.id,
230
237
  owner: repoOwner,
231
238
  repo: repoName,
232
239
  number: prMetadata.pr_number,
@@ -238,6 +245,7 @@ router.get('/api/pr/:owner/:repo/:number', async (req, res) => {
238
245
  head_branch: prMetadata.head_branch,
239
246
  head_sha: extendedData.head_sha || null, // Head commit SHA for GitHub API comments
240
247
  node_id: extendedData.node_id || null, // GraphQL node ID for review submission
248
+ shaAbbrevLength,
241
249
  created_at: prMetadata.created_at,
242
250
  updated_at: prMetadata.updated_at,
243
251
  file_changes: extendedData.changed_files ? extendedData.changed_files.length : 0,
@@ -259,6 +267,22 @@ router.get('/api/pr/:owner/:repo/:number', async (req, res) => {
259
267
 
260
268
  res.json(response);
261
269
 
270
+ // Fire review hook (after response, non-blocking)
271
+ const config = req.app.get('config') || {};
272
+ const prContext = {
273
+ number: prNumber, owner: repoOwner, repo: repoName,
274
+ author: prMetadata.author, baseBranch: prMetadata.base_branch, headBranch: prMetadata.head_branch,
275
+ baseSha: extendedData.base_sha || null, headSha: extendedData.head_sha || null,
276
+ };
277
+ const hookEvent = isNewReview ? 'review.started' : 'review.loaded';
278
+ if (hasHooks(hookEvent, config)) {
279
+ getCachedUser(config).then(user => {
280
+ const builder = isNewReview ? buildReviewStartedPayload : buildReviewLoadedPayload;
281
+ const payload = builder({ reviewId: review.id, mode: 'pr', prContext, user });
282
+ fireHooks(hookEvent, payload, config);
283
+ }).catch(err => { logger.warn(`Review hook failed: ${err.message}`); });
284
+ }
285
+
262
286
  } catch (error) {
263
287
  console.error('Error fetching PR data:', error);
264
288
  res.status(500).json({
@@ -358,7 +382,7 @@ router.post('/api/pr/:owner/:repo/:number/refresh', async (req, res) => {
358
382
  // Get or create a review record for this PR
359
383
  // The review.id is used for comments to avoid ID collision with local mode
360
384
  const reviewRepo = new ReviewRepository(db);
361
- const review = await reviewRepo.getOrCreate({ prNumber, repository });
385
+ const { review } = await reviewRepo.getOrCreate({ prNumber, repository });
362
386
 
363
387
  // Fetch and return updated PR data (reuse the same structure as GET endpoint)
364
388
  const prMetadata = await queryOne(db, `
@@ -1013,7 +1037,7 @@ router.post('/api/pr/:owner/:repo/:number/submit-review', async (req, res) => {
1013
1037
  // Get or create a review record for this PR
1014
1038
  // Comments are associated with review.id, not prMetadata.id
1015
1039
  const reviewRepo = new ReviewRepository(db);
1016
- const review = await reviewRepo.getOrCreate({ prNumber, repository });
1040
+ const { review } = await reviewRepo.getOrCreate({ prNumber, repository });
1017
1041
 
1018
1042
  // Get all active user comments for this PR using review.id
1019
1043
  const comments = await query(db, `
@@ -1518,7 +1542,7 @@ router.post('/api/pr/:owner/:repo/:number/analyses', async (req, res) => {
1518
1542
  const runId = uuidv4();
1519
1543
  const analysisId = runId;
1520
1544
 
1521
- const review = await reviewRepo.getOrCreate({ prNumber, repository });
1545
+ const { review } = await reviewRepo.getOrCreate({ prNumber, repository });
1522
1546
 
1523
1547
  const analysisRunRepo = new AnalysisRunRepository(db);
1524
1548
  const levelsConfig = parseEnabledLevels(requestEnabledLevels, requestSkipLevel3);
@@ -1562,6 +1586,19 @@ router.post('/api/pr/:owner/:repo/:number/analyses', async (req, res) => {
1562
1586
 
1563
1587
  broadcastProgress(analysisId, initialStatus);
1564
1588
  broadcastReviewEvent(review.id, { type: 'review:analysis_started', analysisId });
1589
+ const analysisConfig = req.app.get('config') || {};
1590
+ const analysisPrContext = {
1591
+ number: prNumber, owner, repo,
1592
+ author: prMetadata.author, baseBranch: prMetadata.base_branch, headBranch: prMetadata.head_branch,
1593
+ baseSha: prMetadata.base_sha || null, headSha: prMetadata.head_sha || null,
1594
+ };
1595
+ if (hasHooks('analysis.started', analysisConfig)) {
1596
+ getCachedUser(analysisConfig).then(user => {
1597
+ fireHooks('analysis.started', buildAnalysisStartedPayload({
1598
+ reviewId: review.id, analysisId, provider, model, mode: 'pr', prContext: analysisPrContext, user,
1599
+ }), analysisConfig);
1600
+ }).catch(() => {});
1601
+ }
1565
1602
 
1566
1603
  const analyzer = new Analyzer(req.app.get('db'), model, provider);
1567
1604
 
@@ -1657,6 +1694,18 @@ router.post('/api/pr/:owner/:repo/:number/analyses', async (req, res) => {
1657
1694
 
1658
1695
  broadcastProgress(analysisId, completedStatus);
1659
1696
  broadcastReviewEvent(review.id, { type: 'review:analysis_completed' });
1697
+
1698
+ // Fire analysis.completed hook
1699
+ const hookConfig = req.app.get('config') || {};
1700
+ if (hasHooks('analysis.completed', hookConfig)) {
1701
+ getCachedUser(hookConfig).then(user => {
1702
+ fireHooks('analysis.completed', buildAnalysisCompletedPayload({
1703
+ reviewId: review.id, analysisId, provider, model, status: 'success',
1704
+ totalSuggestions: completionInfo.totalSuggestions,
1705
+ mode: 'pr', prContext: analysisPrContext, user,
1706
+ }), hookConfig);
1707
+ }).catch(() => {});
1708
+ }
1660
1709
  })
1661
1710
  .catch(error => {
1662
1711
  const currentStatus = activeAnalyses.get(analysisId);
@@ -1667,6 +1716,15 @@ router.post('/api/pr/:owner/:repo/:number/analyses', async (req, res) => {
1667
1716
 
1668
1717
  if (error.isCancellation) {
1669
1718
  logger.info(`Analysis cancelled for PR #${prNumber}`);
1719
+ if (hasHooks('analysis.completed', analysisConfig)) {
1720
+ getCachedUser(analysisConfig).then(user => {
1721
+ fireHooks('analysis.completed', buildAnalysisCompletedPayload({
1722
+ reviewId: review.id, analysisId, provider, model,
1723
+ status: 'cancelled', totalSuggestions: 0,
1724
+ mode: 'pr', prContext: analysisPrContext, user,
1725
+ }), analysisConfig);
1726
+ }).catch(() => {});
1727
+ }
1670
1728
  return;
1671
1729
  }
1672
1730
 
@@ -1690,6 +1748,16 @@ router.post('/api/pr/:owner/:repo/:number/analyses', async (req, res) => {
1690
1748
  activeAnalyses.set(analysisId, failedStatus);
1691
1749
 
1692
1750
  broadcastProgress(analysisId, failedStatus);
1751
+
1752
+ if (hasHooks('analysis.completed', analysisConfig)) {
1753
+ getCachedUser(analysisConfig).then(user => {
1754
+ fireHooks('analysis.completed', buildAnalysisCompletedPayload({
1755
+ reviewId: review.id, analysisId, provider, model,
1756
+ status: 'failed', totalSuggestions: 0,
1757
+ mode: 'pr', prContext: analysisPrContext, user,
1758
+ }), analysisConfig);
1759
+ }).catch(() => {});
1760
+ }
1693
1761
  })
1694
1762
  .finally(() => {
1695
1763
  // Clean up review to analysis ID mapping (unified map)
@@ -1771,7 +1839,7 @@ router.post('/api/pr/:owner/:repo/:number/analyses/council', async (req, res) =>
1771
1839
  const repoInstructions = repoSettings?.default_instructions || null;
1772
1840
  const requestInstructions = rawInstructions?.trim() || null;
1773
1841
 
1774
- const review = await reviewRepo.getOrCreate({ prNumber, repository });
1842
+ const { review } = await reviewRepo.getOrCreate({ prNumber, repository });
1775
1843
 
1776
1844
  if (requestInstructions) {
1777
1845
  await reviewRepo.upsertCustomInstructions(prNumber, repository, requestInstructions);
@@ -1788,6 +1856,16 @@ router.post('/api/pr/:owner/:repo/:number/analyses/council', async (req, res) =>
1788
1856
  headSha: prMetadata.head_sha,
1789
1857
  logLabel: `PR #${prNumber}`,
1790
1858
  initialStatusExtra: { prNumber, reviewType: 'pr' },
1859
+ config: req.app.get('config') || {},
1860
+ hookContext: {
1861
+ mode: 'pr',
1862
+ prContext: {
1863
+ number: prNumber, owner, repo,
1864
+ author: prMetadata.author, baseBranch: prMetadata.base_branch,
1865
+ headBranch: prMetadata.head_branch,
1866
+ baseSha: prMetadata.base_sha || null, headSha: prMetadata.head_sha || null,
1867
+ },
1868
+ },
1791
1869
  onSuccess: async (result) => {
1792
1870
  if (result.summary) {
1793
1871
  await reviewRepo.upsertSummary(prNumber, repository, result.summary);
@@ -1,4 +1,4 @@
1
- // SPDX-License-Identifier: GPL-3.0-or-later
1
+ // Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
2
2
  /**
3
3
  * Unified Review Comment Routes
4
4
  *
@@ -1,4 +1,4 @@
1
- // SPDX-License-Identifier: GPL-3.0-or-later
1
+ // Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
2
2
  /**
3
3
  * Setup Routes
4
4
  *
@@ -165,6 +165,7 @@ router.post('/api/setup/local', async (req, res) => {
165
165
  const result = await setupLocalReview({
166
166
  db,
167
167
  targetPath,
168
+ config: req.app.get('config') || {},
168
169
  onProgress: (progress) => {
169
170
  sendSetupEvent(setupId, 'step', progress);
170
171
  }
@@ -1,4 +1,4 @@
1
- // SPDX-License-Identifier: GPL-3.0-or-later
1
+ // Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
2
2
  /**
3
3
  * Shared utilities for route handlers
4
4
  *