@in-the-loop-labs/pair-review 1.6.2 → 2.0.1

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 (63) hide show
  1. package/README.md +77 -4
  2. package/package.json +1 -1
  3. package/plugin/.claude-plugin/plugin.json +1 -1
  4. package/plugin/skills/review-requests/SKILL.md +4 -1
  5. package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
  6. package/plugin-code-critic/skills/analyze/SKILL.md +4 -3
  7. package/public/css/pr.css +1962 -114
  8. package/public/js/CONVENTIONS.md +16 -0
  9. package/public/js/components/AIPanel.js +66 -0
  10. package/public/js/components/AnalysisConfigModal.js +2 -2
  11. package/public/js/components/ChatPanel.js +2955 -0
  12. package/public/js/components/CouncilProgressModal.js +12 -16
  13. package/public/js/components/KeyboardShortcuts.js +3 -0
  14. package/public/js/components/PanelGroup.js +723 -0
  15. package/public/js/components/PreviewModal.js +3 -8
  16. package/public/js/index.js +8 -0
  17. package/public/js/local.js +17 -615
  18. package/public/js/modules/analysis-history.js +19 -68
  19. package/public/js/modules/comment-manager.js +103 -20
  20. package/public/js/modules/diff-context.js +176 -0
  21. package/public/js/modules/diff-renderer.js +30 -0
  22. package/public/js/modules/file-comment-manager.js +126 -105
  23. package/public/js/modules/file-list-merger.js +64 -0
  24. package/public/js/modules/panel-resizer.js +25 -6
  25. package/public/js/modules/suggestion-manager.js +40 -125
  26. package/public/js/pr.js +1009 -159
  27. package/public/js/repo-settings.js +36 -6
  28. package/public/js/utils/category-emoji.js +44 -0
  29. package/public/js/utils/time.js +32 -0
  30. package/public/local.html +107 -70
  31. package/public/pr.html +107 -70
  32. package/public/repo-settings.html +32 -0
  33. package/src/ai/analyzer.js +5 -1
  34. package/src/ai/copilot-provider.js +39 -9
  35. package/src/ai/cursor-agent-provider.js +45 -11
  36. package/src/ai/gemini-provider.js +17 -4
  37. package/src/ai/prompts/config.js +7 -1
  38. package/src/ai/provider-availability.js +1 -1
  39. package/src/ai/provider.js +25 -37
  40. package/src/chat/CONVENTIONS.md +18 -0
  41. package/src/chat/pi-bridge.js +491 -0
  42. package/src/chat/prompt-builder.js +272 -0
  43. package/src/chat/session-manager.js +619 -0
  44. package/src/config.js +14 -0
  45. package/src/database.js +322 -15
  46. package/src/main.js +4 -17
  47. package/src/routes/analyses.js +721 -0
  48. package/src/routes/chat.js +655 -0
  49. package/src/routes/config.js +29 -8
  50. package/src/routes/context-files.js +274 -0
  51. package/src/routes/local.js +225 -1133
  52. package/src/routes/mcp.js +39 -30
  53. package/src/routes/pr.js +424 -58
  54. package/src/routes/reviews.js +1035 -0
  55. package/src/routes/shared.js +4 -29
  56. package/src/server.js +34 -12
  57. package/src/sse/review-events.js +46 -0
  58. package/src/utils/auto-context.js +88 -0
  59. package/src/utils/category-emoji.js +33 -0
  60. package/src/utils/diff-annotator.js +75 -1
  61. package/src/utils/diff-file-list.js +57 -0
  62. package/src/routes/analysis.js +0 -1600
  63. package/src/routes/comments.js +0 -534
package/src/routes/pr.js CHANGED
@@ -14,15 +14,32 @@
14
14
  */
15
15
 
16
16
  const express = require('express');
17
- const { query, queryOne, run, WorktreeRepository, ReviewRepository, GitHubReviewRepository } = require('../database');
17
+ const { query, queryOne, run, withTransaction, WorktreeRepository, ReviewRepository, GitHubReviewRepository, RepoSettingsRepository, AnalysisRunRepository, PRMetadataRepository, CouncilRepository } = require('../database');
18
18
  const { GitWorktreeManager } = require('../git/worktree');
19
19
  const { GitHubClient } = require('../github/client');
20
20
  const { getGeneratedFilePatterns } = require('../git/gitattributes');
21
21
  const { normalizeRepository } = require('../utils/paths');
22
+ const { mergeInstructions } = require('../utils/instructions');
23
+ const Analyzer = require('../ai/analyzer');
24
+ const { v4: uuidv4 } = require('uuid');
22
25
  const fs = require('fs').promises;
23
26
  const path = require('path');
24
27
  const logger = require('../utils/logger');
28
+ const { buildDiffLineSet } = require('../utils/diff-annotator');
29
+ const { broadcastReviewEvent } = require('../sse/review-events');
25
30
  const simpleGit = require('simple-git');
31
+ const {
32
+ activeAnalyses,
33
+ reviewToAnalysisId,
34
+ getModel,
35
+ determineCompletionInfo,
36
+ broadcastProgress,
37
+ createProgressCallback,
38
+ parseEnabledLevels
39
+ } = require('./shared');
40
+ const { validateCouncilConfig, normalizeCouncilConfig } = require('./councils');
41
+ const { TIERS, TIER_ALIASES, VALID_TIERS, resolveTier } = require('../ai/prompts/config');
42
+ const analysesRouter = require('./analyses');
26
43
 
27
44
  const router = express.Router();
28
45
 
@@ -674,57 +691,6 @@ router.get('/api/pr/:owner/:repo/:number/diff', async (req, res) => {
674
691
  }
675
692
  });
676
693
 
677
- /**
678
- * Get PR comments
679
- */
680
- router.get('/api/pr/:owner/:repo/:number/comments', async (req, res) => {
681
- try {
682
- const { owner, repo, number } = req.params;
683
- const prNumber = parseInt(number);
684
-
685
- if (isNaN(prNumber) || prNumber <= 0) {
686
- return res.status(400).json({
687
- error: 'Invalid pull request number'
688
- });
689
- }
690
-
691
- const repository = normalizeRepository(owner, repo);
692
-
693
- // Get review ID first
694
- const review = await queryOne(req.app.get('db'), `
695
- SELECT id FROM reviews
696
- WHERE pr_number = ? AND repository = ? COLLATE NOCASE
697
- `, [prNumber, repository]);
698
-
699
- if (!review) {
700
- return res.json({ comments: [] });
701
- }
702
-
703
- // Get comments for this review
704
- const comments = await query(req.app.get('db'), `
705
- SELECT
706
- id,
707
- file_path,
708
- line_number,
709
- comment_text,
710
- comment_type,
711
- status,
712
- created_at
713
- FROM comments
714
- WHERE review_id = ?
715
- ORDER BY file_path, line_number, created_at
716
- `, [review.id]);
717
-
718
- res.json({ comments });
719
-
720
- } catch (error) {
721
- console.error('Error fetching PR comments:', error);
722
- res.status(500).json({
723
- error: 'Internal server error while fetching comments'
724
- });
725
- }
726
- });
727
-
728
694
  /**
729
695
  * Get original file content from worktree for context expansion
730
696
  */
@@ -1023,8 +989,8 @@ router.post('/api/pr/:owner/:repo/:number/submit-review', async (req, res) => {
1023
989
  // GraphQL supports both line-level comments (within diff hunks) and file-level comments
1024
990
  // (for expanded context lines outside diff hunks via subjectType: FILE).
1025
991
  //
1026
- // Comments on expanded context lines (diff_position IS NULL) are formatted as file-level
1027
- // comments with a "(Ref Line X)" prefix in the body.
992
+ // We check whether the comment's target line actually appears in a diff hunk
993
+ // rather than relying on diff_position (which may not be set by all sources).
1028
994
  const prNodeId = prData.node_id;
1029
995
  if (!prNodeId) {
1030
996
  return res.status(400).json({
@@ -1032,6 +998,8 @@ router.post('/api/pr/:owner/:repo/:number/submit-review', async (req, res) => {
1032
998
  });
1033
999
  }
1034
1000
 
1001
+ const diffLineSet = buildDiffLineSet(diffContent);
1002
+
1035
1003
  const graphqlComments = comments.map(comment => {
1036
1004
  const side = comment.side || 'RIGHT';
1037
1005
  const isRange = comment.line_end && comment.line_end !== comment.line_start;
@@ -1048,10 +1016,15 @@ router.post('/api/pr/:owner/:repo/:number/submit-review', async (req, res) => {
1048
1016
  };
1049
1017
  }
1050
1018
 
1051
- // Detect expanded context comments (no diff_position)
1052
- // These are submitted as file-level comments since GitHub API rejects
1053
- // line-level comments on lines outside diff hunks.
1054
- const isExpandedContext = comment.diff_position === null || comment.diff_position === undefined;
1019
+ // Detect expanded context comments by checking whether the target line
1020
+ // actually appears in a diff hunk. This is more reliable than checking
1021
+ // diff_position, which may be absent for comments created by the chat agent.
1022
+ // For range comments, both endpoints must be inside the diff; if the start
1023
+ // line falls outside a hunk but the end is inside, submitting with start_line
1024
+ // would produce a position GitHub cannot render.
1025
+ const isExpandedContext = isRange
1026
+ ? !diffLineSet.isLineInDiff(comment.file, comment.line_start, side) || !diffLineSet.isLineInDiff(comment.file, comment.line_end, side)
1027
+ : !diffLineSet.isLineInDiff(comment.file, comment.line_start, side);
1055
1028
 
1056
1029
  if (isExpandedContext) {
1057
1030
  // File-level comment with line reference prefix
@@ -1389,4 +1362,397 @@ router.post('/api/parse-pr-url', (req, res) => {
1389
1362
  });
1390
1363
  });
1391
1364
 
1365
+ // ==========================================================================
1366
+ // PR Analysis Routes
1367
+ // ==========================================================================
1368
+
1369
+ /**
1370
+ * Trigger AI analysis for a PR
1371
+ */
1372
+ router.post('/api/pr/:owner/:repo/:number/analyses', async (req, res) => {
1373
+ try {
1374
+ const { owner, repo, number } = req.params;
1375
+ const prNumber = parseInt(number);
1376
+
1377
+ const { provider: requestProvider, model: requestModel, tier: requestTier, customInstructions: rawInstructions, skipLevel3: requestSkipLevel3, enabledLevels: requestEnabledLevels } = req.body || {};
1378
+
1379
+ const MAX_INSTRUCTIONS_LENGTH = 5000;
1380
+ let requestInstructions = rawInstructions?.trim() || null;
1381
+ if (requestInstructions && requestInstructions.length > MAX_INSTRUCTIONS_LENGTH) {
1382
+ return res.status(400).json({
1383
+ error: `Custom instructions exceed maximum length of ${MAX_INSTRUCTIONS_LENGTH} characters`
1384
+ });
1385
+ }
1386
+
1387
+ if (requestTier && !VALID_TIERS.includes(requestTier)) {
1388
+ return res.status(400).json({
1389
+ error: `Invalid tier: "${requestTier}". Valid tiers: ${VALID_TIERS.join(', ')}`
1390
+ });
1391
+ }
1392
+
1393
+ if (isNaN(prNumber) || prNumber <= 0) {
1394
+ return res.status(400).json({
1395
+ error: 'Invalid pull request number'
1396
+ });
1397
+ }
1398
+
1399
+ const repository = normalizeRepository(owner, repo);
1400
+
1401
+ const db = req.app.get('db');
1402
+ const reviewRepo = new ReviewRepository(db);
1403
+ const prMetadataRepo = new PRMetadataRepository(db);
1404
+ const prMetadata = await prMetadataRepo.getByPR(prNumber, repository);
1405
+
1406
+ if (!prMetadata) {
1407
+ return res.status(404).json({
1408
+ error: `Pull request #${prNumber} not found. Please load the PR first.`
1409
+ });
1410
+ }
1411
+
1412
+ const worktreeManager = new GitWorktreeManager(db);
1413
+ const worktreePath = await worktreeManager.getWorktreePath({ owner, repo, number: prNumber });
1414
+
1415
+ if (!await worktreeManager.worktreeExists({ owner, repo, number: prNumber })) {
1416
+ return res.status(404).json({
1417
+ error: 'Worktree not found for this PR. Please reload the PR.'
1418
+ });
1419
+ }
1420
+
1421
+ const { provider, model, repoInstructions, combinedInstructions } = await withTransaction(db, async () => {
1422
+ const repoSettingsRepo = new RepoSettingsRepository(db);
1423
+ const fetchedRepoSettings = await repoSettingsRepo.getRepoSettings(repository);
1424
+
1425
+ let selectedProvider;
1426
+ if (requestProvider) {
1427
+ selectedProvider = requestProvider;
1428
+ } else if (fetchedRepoSettings && fetchedRepoSettings.default_provider) {
1429
+ selectedProvider = fetchedRepoSettings.default_provider;
1430
+ } else {
1431
+ const config = req.app.get('config') || {};
1432
+ selectedProvider = config.default_provider || config.provider || 'claude';
1433
+ }
1434
+
1435
+ let selectedModel;
1436
+ if (requestModel) {
1437
+ selectedModel = requestModel;
1438
+ } else if (fetchedRepoSettings && fetchedRepoSettings.default_model) {
1439
+ selectedModel = fetchedRepoSettings.default_model;
1440
+ } else {
1441
+ selectedModel = getModel(req);
1442
+ }
1443
+
1444
+ const fetchedRepoInstructions = fetchedRepoSettings?.default_instructions || null;
1445
+ const mergedInstructions = mergeInstructions(fetchedRepoInstructions, requestInstructions);
1446
+
1447
+ if (requestInstructions) {
1448
+ await reviewRepo.upsertCustomInstructions(prNumber, repository, requestInstructions);
1449
+ }
1450
+
1451
+ return {
1452
+ provider: selectedProvider,
1453
+ model: selectedModel,
1454
+ repoInstructions: fetchedRepoInstructions,
1455
+ combinedInstructions: mergedInstructions
1456
+ };
1457
+ });
1458
+
1459
+ const runId = uuidv4();
1460
+ const analysisId = runId;
1461
+
1462
+ const review = await reviewRepo.getOrCreate({ prNumber, repository });
1463
+
1464
+ const analysisRunRepo = new AnalysisRunRepository(db);
1465
+ const levelsConfig = parseEnabledLevels(requestEnabledLevels, requestSkipLevel3);
1466
+ const tier = requestTier ? resolveTier(requestTier) : 'balanced';
1467
+ await analysisRunRepo.create({
1468
+ id: runId,
1469
+ reviewId: review.id,
1470
+ provider,
1471
+ model,
1472
+ tier,
1473
+ repoInstructions,
1474
+ requestInstructions,
1475
+ headSha: prMetadata.head_sha || null,
1476
+ configType: 'single',
1477
+ levelsConfig
1478
+ });
1479
+
1480
+ const initialStatus = {
1481
+ id: analysisId,
1482
+ runId,
1483
+ reviewId: review.id,
1484
+ prNumber,
1485
+ repository,
1486
+ reviewType: 'pr',
1487
+ status: 'running',
1488
+ startedAt: new Date().toISOString(),
1489
+ progress: 'Starting analysis...',
1490
+ levels: {
1491
+ 1: levelsConfig[1] ? { status: 'running', progress: 'Starting...' } : { status: 'skipped', progress: 'Skipped' },
1492
+ 2: levelsConfig[2] ? { status: 'running', progress: 'Starting...' } : { status: 'skipped', progress: 'Skipped' },
1493
+ 3: levelsConfig[3] ? { status: 'running', progress: 'Starting...' } : { status: 'skipped', progress: 'Skipped' },
1494
+ 4: { status: 'pending', progress: 'Pending' }
1495
+ },
1496
+ filesAnalyzed: 0,
1497
+ filesRemaining: 0
1498
+ };
1499
+ activeAnalyses.set(analysisId, initialStatus);
1500
+
1501
+ // Store review to analysis ID mapping (unified map using integer reviewId)
1502
+ reviewToAnalysisId.set(review.id, analysisId);
1503
+
1504
+ broadcastProgress(analysisId, initialStatus);
1505
+ broadcastReviewEvent(review.id, { type: 'review:analysis_started', analysisId });
1506
+
1507
+ const analyzer = new Analyzer(req.app.get('db'), model, provider);
1508
+
1509
+ logger.section(`AI Analysis Request - PR #${prNumber}`);
1510
+ logger.log('API', `Repository: ${repository}`, 'magenta');
1511
+ logger.log('API', `Worktree: ${worktreePath}`, 'magenta');
1512
+ logger.log('API', `Analysis ID: ${analysisId}`, 'magenta');
1513
+ logger.log('API', `Review ID: ${review.id}`, 'magenta');
1514
+ logger.log('API', `Provider: ${provider}`, 'cyan');
1515
+ logger.log('API', `Model: ${model}`, 'cyan');
1516
+ logger.log('API', `Tier: ${tier}`, 'cyan');
1517
+ if (combinedInstructions) {
1518
+ logger.log('API', `Custom instructions: ${combinedInstructions.length} chars`, 'cyan');
1519
+ }
1520
+
1521
+ const progressCallback = createProgressCallback(analysisId);
1522
+
1523
+ analyzer.analyzeLevel1(review.id, worktreePath, prMetadata, progressCallback, { repoInstructions, requestInstructions }, null, { analysisId, runId, skipRunCreation: true, tier, skipLevel3: requestSkipLevel3, enabledLevels: levelsConfig })
1524
+ .then(async result => {
1525
+ logger.section('Analysis Results');
1526
+ logger.success(`Analysis complete for PR #${prNumber}`);
1527
+ logger.success(`Found ${result.suggestions.length} suggestions:`);
1528
+
1529
+ try {
1530
+ await prMetadataRepo.updateLastAiRunId(prMetadata.id, result.runId);
1531
+ logger.info(`Updated pr_metadata with last_ai_run_id: ${result.runId}`);
1532
+ } catch (updateError) {
1533
+ logger.warn(`Failed to update pr_metadata with last_ai_run_id: ${updateError.message}`);
1534
+ }
1535
+
1536
+ if (result.summary) {
1537
+ try {
1538
+ await reviewRepo.upsertSummary(prNumber, repository, result.summary);
1539
+ logger.info(`Saved analysis summary to review record`);
1540
+ logger.section('Analysis Summary');
1541
+ logger.info(result.summary);
1542
+ } catch (summaryError) {
1543
+ logger.warn(`Failed to save analysis summary: ${summaryError.message}`);
1544
+ }
1545
+ }
1546
+ result.suggestions.forEach(s => {
1547
+ const icon = s.type === 'bug' ? '\uD83D\uDC1B' :
1548
+ s.type === 'praise' ? '\u2B50' :
1549
+ s.type === 'improvement' ? '\uD83D\uDCA1' :
1550
+ s.type === 'security' ? '\uD83D\uDD12' :
1551
+ s.type === 'performance' ? '\u26A1' :
1552
+ s.type === 'design' ? '\uD83D\uDCD0' :
1553
+ s.type === 'suggestion' ? '\uD83D\uDCAC' :
1554
+ s.type === 'code-style' || s.type === 'style' ? '\uD83E\uDDF9' : '\uD83D\uDCDD';
1555
+ logger.log('Result', `${icon} ${s.type}: ${s.title} (${s.file}:${s.line_start})`, 'green');
1556
+ });
1557
+
1558
+ const completionInfo = determineCompletionInfo(result);
1559
+
1560
+ const currentStatus = activeAnalyses.get(analysisId);
1561
+ if (!currentStatus) {
1562
+ logger.warn('Analysis already completed or removed:', analysisId);
1563
+ return;
1564
+ }
1565
+
1566
+ if (currentStatus.status === 'cancelled') {
1567
+ logger.info(`Analysis ${analysisId} was cancelled, skipping completion update`);
1568
+ return;
1569
+ }
1570
+
1571
+ for (let i = 1; i <= completionInfo.completedLevel; i++) {
1572
+ currentStatus.levels[i] = {
1573
+ status: 'completed',
1574
+ progress: `Level ${i} complete`
1575
+ };
1576
+ }
1577
+
1578
+ currentStatus.levels[4] = {
1579
+ status: 'completed',
1580
+ progress: 'Results finalized'
1581
+ };
1582
+
1583
+ const completedStatus = {
1584
+ ...currentStatus,
1585
+ status: 'completed',
1586
+ level: completionInfo.completedLevel,
1587
+ completedLevel: completionInfo.completedLevel,
1588
+ completedAt: new Date().toISOString(),
1589
+ result,
1590
+ progress: completionInfo.progressMessage,
1591
+ suggestionsCount: completionInfo.totalSuggestions,
1592
+ filesAnalyzed: currentStatus?.filesAnalyzed || 0,
1593
+ filesRemaining: 0,
1594
+ currentFile: currentStatus?.totalFiles || 0,
1595
+ totalFiles: currentStatus?.totalFiles || 0
1596
+ };
1597
+ activeAnalyses.set(analysisId, completedStatus);
1598
+
1599
+ broadcastProgress(analysisId, completedStatus);
1600
+ broadcastReviewEvent(review.id, { type: 'review:analysis_completed' });
1601
+ })
1602
+ .catch(error => {
1603
+ const currentStatus = activeAnalyses.get(analysisId);
1604
+ if (!currentStatus) {
1605
+ logger.warn('Analysis status not found during error handling:', analysisId);
1606
+ return;
1607
+ }
1608
+
1609
+ if (error.isCancellation) {
1610
+ logger.info(`Analysis cancelled for PR #${prNumber}`);
1611
+ return;
1612
+ }
1613
+
1614
+ logger.error(`Analysis failed for PR #${prNumber}: ${error.message}`);
1615
+
1616
+ for (let i = 1; i <= 4; i++) {
1617
+ currentStatus.levels[i] = {
1618
+ status: 'failed',
1619
+ progress: 'Failed'
1620
+ };
1621
+ }
1622
+
1623
+ const failedStatus = {
1624
+ ...currentStatus,
1625
+ status: 'failed',
1626
+ level: 1,
1627
+ completedAt: new Date().toISOString(),
1628
+ error: error.message,
1629
+ progress: 'Analysis failed'
1630
+ };
1631
+ activeAnalyses.set(analysisId, failedStatus);
1632
+
1633
+ broadcastProgress(analysisId, failedStatus);
1634
+ })
1635
+ .finally(() => {
1636
+ // Clean up review to analysis ID mapping (unified map)
1637
+ reviewToAnalysisId.delete(review.id);
1638
+ });
1639
+
1640
+ res.json({
1641
+ analysisId,
1642
+ runId,
1643
+ status: 'started',
1644
+ message: 'AI analysis started in background'
1645
+ });
1646
+
1647
+ } catch (error) {
1648
+ logger.error('Error starting AI analysis:', error);
1649
+ res.status(500).json({
1650
+ error: 'Failed to start AI analysis'
1651
+ });
1652
+ }
1653
+ });
1654
+
1655
+ /**
1656
+ * Trigger council analysis for a PR
1657
+ */
1658
+ router.post('/api/pr/:owner/:repo/:number/analyses/council', async (req, res) => {
1659
+ try {
1660
+ const { owner, repo, number } = req.params;
1661
+ const prNumber = parseInt(number);
1662
+ const { councilId, councilConfig: inlineConfig, customInstructions: rawInstructions, configType: requestConfigType } = req.body || {};
1663
+
1664
+ if (isNaN(prNumber) || prNumber <= 0) {
1665
+ return res.status(400).json({ error: 'Invalid pull request number' });
1666
+ }
1667
+
1668
+ if (!councilId && !inlineConfig) {
1669
+ return res.status(400).json({ error: 'Either councilId or councilConfig is required' });
1670
+ }
1671
+
1672
+ const repository = normalizeRepository(owner, repo);
1673
+ const db = req.app.get('db');
1674
+
1675
+ let councilConfig;
1676
+ let configType;
1677
+ if (councilId) {
1678
+ const councilRepo = new CouncilRepository(db);
1679
+ const council = await councilRepo.getById(councilId);
1680
+ if (!council) {
1681
+ return res.status(404).json({ error: 'Council not found' });
1682
+ }
1683
+ councilConfig = council.config;
1684
+ configType = requestConfigType || council.type || 'advanced';
1685
+ } else {
1686
+ councilConfig = inlineConfig;
1687
+ configType = requestConfigType || 'advanced';
1688
+ }
1689
+
1690
+ councilConfig = normalizeCouncilConfig(councilConfig, configType);
1691
+
1692
+ const configError = validateCouncilConfig(councilConfig, configType);
1693
+ if (configError) {
1694
+ return res.status(400).json({ error: `Invalid council config: ${configError}` });
1695
+ }
1696
+
1697
+ const prMetadataRepo = new PRMetadataRepository(db);
1698
+ const prMetadata = await prMetadataRepo.getByPR(prNumber, repository);
1699
+ if (!prMetadata) {
1700
+ return res.status(404).json({ error: `Pull request #${prNumber} not found. Please load the PR first.` });
1701
+ }
1702
+
1703
+ const worktreeManager = new GitWorktreeManager(db);
1704
+ const worktreePath = await worktreeManager.getWorktreePath({ owner, repo, number: prNumber });
1705
+ if (!await worktreeManager.worktreeExists({ owner, repo, number: prNumber })) {
1706
+ return res.status(404).json({ error: 'Worktree not found for this PR. Please reload the PR.' });
1707
+ }
1708
+
1709
+ const reviewRepo = new ReviewRepository(db);
1710
+ const repoSettingsRepo = new RepoSettingsRepository(db);
1711
+ const repoSettings = await repoSettingsRepo.getRepoSettings(repository);
1712
+ const repoInstructions = repoSettings?.default_instructions || null;
1713
+ const requestInstructions = rawInstructions?.trim() || null;
1714
+
1715
+ const review = await reviewRepo.getOrCreate({ prNumber, repository });
1716
+
1717
+ if (requestInstructions) {
1718
+ await reviewRepo.upsertCustomInstructions(prNumber, repository, requestInstructions);
1719
+ }
1720
+
1721
+ const { analysisId, runId } = await analysesRouter.launchCouncilAnalysis(
1722
+ db,
1723
+ {
1724
+ reviewId: review.id,
1725
+ worktreePath,
1726
+ prMetadata,
1727
+ changedFiles: null,
1728
+ repository,
1729
+ headSha: prMetadata.head_sha,
1730
+ logLabel: `PR #${prNumber}`,
1731
+ initialStatusExtra: { prNumber, reviewType: 'pr' },
1732
+ extraBroadcastKeys: null,
1733
+ onSuccess: async (result) => {
1734
+ if (result.summary) {
1735
+ await reviewRepo.upsertSummary(prNumber, repository, result.summary);
1736
+ }
1737
+ }
1738
+ },
1739
+ councilConfig,
1740
+ councilId,
1741
+ { repoInstructions, requestInstructions },
1742
+ configType
1743
+ );
1744
+
1745
+ res.json({
1746
+ analysisId,
1747
+ runId,
1748
+ status: 'started',
1749
+ message: 'Council analysis started in background',
1750
+ isCouncil: true
1751
+ });
1752
+ } catch (error) {
1753
+ logger.error('Error starting council analysis:', error);
1754
+ res.status(500).json({ error: 'Failed to start council analysis' });
1755
+ }
1756
+ });
1757
+
1392
1758
  module.exports = router;