@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.
- package/README.md +77 -4
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/review-requests/SKILL.md +4 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/skills/analyze/SKILL.md +4 -3
- package/public/css/pr.css +1962 -114
- package/public/js/CONVENTIONS.md +16 -0
- package/public/js/components/AIPanel.js +66 -0
- package/public/js/components/AnalysisConfigModal.js +2 -2
- package/public/js/components/ChatPanel.js +2955 -0
- package/public/js/components/CouncilProgressModal.js +12 -16
- package/public/js/components/KeyboardShortcuts.js +3 -0
- package/public/js/components/PanelGroup.js +723 -0
- package/public/js/components/PreviewModal.js +3 -8
- package/public/js/index.js +8 -0
- package/public/js/local.js +17 -615
- package/public/js/modules/analysis-history.js +19 -68
- package/public/js/modules/comment-manager.js +103 -20
- package/public/js/modules/diff-context.js +176 -0
- package/public/js/modules/diff-renderer.js +30 -0
- package/public/js/modules/file-comment-manager.js +126 -105
- package/public/js/modules/file-list-merger.js +64 -0
- package/public/js/modules/panel-resizer.js +25 -6
- package/public/js/modules/suggestion-manager.js +40 -125
- package/public/js/pr.js +1009 -159
- package/public/js/repo-settings.js +36 -6
- package/public/js/utils/category-emoji.js +44 -0
- package/public/js/utils/time.js +32 -0
- package/public/local.html +107 -70
- package/public/pr.html +107 -70
- package/public/repo-settings.html +32 -0
- package/src/ai/analyzer.js +5 -1
- package/src/ai/copilot-provider.js +39 -9
- package/src/ai/cursor-agent-provider.js +45 -11
- package/src/ai/gemini-provider.js +17 -4
- package/src/ai/prompts/config.js +7 -1
- package/src/ai/provider-availability.js +1 -1
- package/src/ai/provider.js +25 -37
- package/src/chat/CONVENTIONS.md +18 -0
- package/src/chat/pi-bridge.js +491 -0
- package/src/chat/prompt-builder.js +272 -0
- package/src/chat/session-manager.js +619 -0
- package/src/config.js +14 -0
- package/src/database.js +322 -15
- package/src/main.js +4 -17
- package/src/routes/analyses.js +721 -0
- package/src/routes/chat.js +655 -0
- package/src/routes/config.js +29 -8
- package/src/routes/context-files.js +274 -0
- package/src/routes/local.js +225 -1133
- package/src/routes/mcp.js +39 -30
- package/src/routes/pr.js +424 -58
- package/src/routes/reviews.js +1035 -0
- package/src/routes/shared.js +4 -29
- package/src/server.js +34 -12
- package/src/sse/review-events.js +46 -0
- package/src/utils/auto-context.js +88 -0
- package/src/utils/category-emoji.js +33 -0
- package/src/utils/diff-annotator.js +75 -1
- package/src/utils/diff-file-list.js +57 -0
- package/src/routes/analysis.js +0 -1600
- 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
|
-
//
|
|
1027
|
-
//
|
|
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
|
|
1052
|
-
//
|
|
1053
|
-
//
|
|
1054
|
-
|
|
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;
|