@in-the-loop-labs/pair-review 1.6.2 → 2.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.
- 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 +1930 -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 +2952 -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 +57 -19
- 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 +964 -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 +36 -7
- 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 +262 -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 +223 -0
- package/src/routes/local.js +225 -1133
- package/src/routes/mcp.js +39 -30
- package/src/routes/pr.js +410 -52
- 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-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,31 @@
|
|
|
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 { broadcastReviewEvent } = require('../sse/review-events');
|
|
25
29
|
const simpleGit = require('simple-git');
|
|
30
|
+
const {
|
|
31
|
+
activeAnalyses,
|
|
32
|
+
reviewToAnalysisId,
|
|
33
|
+
getModel,
|
|
34
|
+
determineCompletionInfo,
|
|
35
|
+
broadcastProgress,
|
|
36
|
+
createProgressCallback,
|
|
37
|
+
parseEnabledLevels
|
|
38
|
+
} = require('./shared');
|
|
39
|
+
const { validateCouncilConfig, normalizeCouncilConfig } = require('./councils');
|
|
40
|
+
const { TIERS, TIER_ALIASES, VALID_TIERS, resolveTier } = require('../ai/prompts/config');
|
|
41
|
+
const analysesRouter = require('./analyses');
|
|
26
42
|
|
|
27
43
|
const router = express.Router();
|
|
28
44
|
|
|
@@ -674,57 +690,6 @@ router.get('/api/pr/:owner/:repo/:number/diff', async (req, res) => {
|
|
|
674
690
|
}
|
|
675
691
|
});
|
|
676
692
|
|
|
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
693
|
/**
|
|
729
694
|
* Get original file content from worktree for context expansion
|
|
730
695
|
*/
|
|
@@ -1389,4 +1354,397 @@ router.post('/api/parse-pr-url', (req, res) => {
|
|
|
1389
1354
|
});
|
|
1390
1355
|
});
|
|
1391
1356
|
|
|
1357
|
+
// ==========================================================================
|
|
1358
|
+
// PR Analysis Routes
|
|
1359
|
+
// ==========================================================================
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Trigger AI analysis for a PR
|
|
1363
|
+
*/
|
|
1364
|
+
router.post('/api/pr/:owner/:repo/:number/analyses', async (req, res) => {
|
|
1365
|
+
try {
|
|
1366
|
+
const { owner, repo, number } = req.params;
|
|
1367
|
+
const prNumber = parseInt(number);
|
|
1368
|
+
|
|
1369
|
+
const { provider: requestProvider, model: requestModel, tier: requestTier, customInstructions: rawInstructions, skipLevel3: requestSkipLevel3, enabledLevels: requestEnabledLevels } = req.body || {};
|
|
1370
|
+
|
|
1371
|
+
const MAX_INSTRUCTIONS_LENGTH = 5000;
|
|
1372
|
+
let requestInstructions = rawInstructions?.trim() || null;
|
|
1373
|
+
if (requestInstructions && requestInstructions.length > MAX_INSTRUCTIONS_LENGTH) {
|
|
1374
|
+
return res.status(400).json({
|
|
1375
|
+
error: `Custom instructions exceed maximum length of ${MAX_INSTRUCTIONS_LENGTH} characters`
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
if (requestTier && !VALID_TIERS.includes(requestTier)) {
|
|
1380
|
+
return res.status(400).json({
|
|
1381
|
+
error: `Invalid tier: "${requestTier}". Valid tiers: ${VALID_TIERS.join(', ')}`
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
if (isNaN(prNumber) || prNumber <= 0) {
|
|
1386
|
+
return res.status(400).json({
|
|
1387
|
+
error: 'Invalid pull request number'
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const repository = normalizeRepository(owner, repo);
|
|
1392
|
+
|
|
1393
|
+
const db = req.app.get('db');
|
|
1394
|
+
const reviewRepo = new ReviewRepository(db);
|
|
1395
|
+
const prMetadataRepo = new PRMetadataRepository(db);
|
|
1396
|
+
const prMetadata = await prMetadataRepo.getByPR(prNumber, repository);
|
|
1397
|
+
|
|
1398
|
+
if (!prMetadata) {
|
|
1399
|
+
return res.status(404).json({
|
|
1400
|
+
error: `Pull request #${prNumber} not found. Please load the PR first.`
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
const worktreeManager = new GitWorktreeManager(db);
|
|
1405
|
+
const worktreePath = await worktreeManager.getWorktreePath({ owner, repo, number: prNumber });
|
|
1406
|
+
|
|
1407
|
+
if (!await worktreeManager.worktreeExists({ owner, repo, number: prNumber })) {
|
|
1408
|
+
return res.status(404).json({
|
|
1409
|
+
error: 'Worktree not found for this PR. Please reload the PR.'
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const { provider, model, repoInstructions, combinedInstructions } = await withTransaction(db, async () => {
|
|
1414
|
+
const repoSettingsRepo = new RepoSettingsRepository(db);
|
|
1415
|
+
const fetchedRepoSettings = await repoSettingsRepo.getRepoSettings(repository);
|
|
1416
|
+
|
|
1417
|
+
let selectedProvider;
|
|
1418
|
+
if (requestProvider) {
|
|
1419
|
+
selectedProvider = requestProvider;
|
|
1420
|
+
} else if (fetchedRepoSettings && fetchedRepoSettings.default_provider) {
|
|
1421
|
+
selectedProvider = fetchedRepoSettings.default_provider;
|
|
1422
|
+
} else {
|
|
1423
|
+
const config = req.app.get('config') || {};
|
|
1424
|
+
selectedProvider = config.default_provider || config.provider || 'claude';
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
let selectedModel;
|
|
1428
|
+
if (requestModel) {
|
|
1429
|
+
selectedModel = requestModel;
|
|
1430
|
+
} else if (fetchedRepoSettings && fetchedRepoSettings.default_model) {
|
|
1431
|
+
selectedModel = fetchedRepoSettings.default_model;
|
|
1432
|
+
} else {
|
|
1433
|
+
selectedModel = getModel(req);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const fetchedRepoInstructions = fetchedRepoSettings?.default_instructions || null;
|
|
1437
|
+
const mergedInstructions = mergeInstructions(fetchedRepoInstructions, requestInstructions);
|
|
1438
|
+
|
|
1439
|
+
if (requestInstructions) {
|
|
1440
|
+
await reviewRepo.upsertCustomInstructions(prNumber, repository, requestInstructions);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
return {
|
|
1444
|
+
provider: selectedProvider,
|
|
1445
|
+
model: selectedModel,
|
|
1446
|
+
repoInstructions: fetchedRepoInstructions,
|
|
1447
|
+
combinedInstructions: mergedInstructions
|
|
1448
|
+
};
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
const runId = uuidv4();
|
|
1452
|
+
const analysisId = runId;
|
|
1453
|
+
|
|
1454
|
+
const review = await reviewRepo.getOrCreate({ prNumber, repository });
|
|
1455
|
+
|
|
1456
|
+
const analysisRunRepo = new AnalysisRunRepository(db);
|
|
1457
|
+
const levelsConfig = parseEnabledLevels(requestEnabledLevels, requestSkipLevel3);
|
|
1458
|
+
const tier = requestTier ? resolveTier(requestTier) : 'balanced';
|
|
1459
|
+
await analysisRunRepo.create({
|
|
1460
|
+
id: runId,
|
|
1461
|
+
reviewId: review.id,
|
|
1462
|
+
provider,
|
|
1463
|
+
model,
|
|
1464
|
+
tier,
|
|
1465
|
+
repoInstructions,
|
|
1466
|
+
requestInstructions,
|
|
1467
|
+
headSha: prMetadata.head_sha || null,
|
|
1468
|
+
configType: 'single',
|
|
1469
|
+
levelsConfig
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
const initialStatus = {
|
|
1473
|
+
id: analysisId,
|
|
1474
|
+
runId,
|
|
1475
|
+
reviewId: review.id,
|
|
1476
|
+
prNumber,
|
|
1477
|
+
repository,
|
|
1478
|
+
reviewType: 'pr',
|
|
1479
|
+
status: 'running',
|
|
1480
|
+
startedAt: new Date().toISOString(),
|
|
1481
|
+
progress: 'Starting analysis...',
|
|
1482
|
+
levels: {
|
|
1483
|
+
1: levelsConfig[1] ? { status: 'running', progress: 'Starting...' } : { status: 'skipped', progress: 'Skipped' },
|
|
1484
|
+
2: levelsConfig[2] ? { status: 'running', progress: 'Starting...' } : { status: 'skipped', progress: 'Skipped' },
|
|
1485
|
+
3: levelsConfig[3] ? { status: 'running', progress: 'Starting...' } : { status: 'skipped', progress: 'Skipped' },
|
|
1486
|
+
4: { status: 'pending', progress: 'Pending' }
|
|
1487
|
+
},
|
|
1488
|
+
filesAnalyzed: 0,
|
|
1489
|
+
filesRemaining: 0
|
|
1490
|
+
};
|
|
1491
|
+
activeAnalyses.set(analysisId, initialStatus);
|
|
1492
|
+
|
|
1493
|
+
// Store review to analysis ID mapping (unified map using integer reviewId)
|
|
1494
|
+
reviewToAnalysisId.set(review.id, analysisId);
|
|
1495
|
+
|
|
1496
|
+
broadcastProgress(analysisId, initialStatus);
|
|
1497
|
+
broadcastReviewEvent(review.id, { type: 'review:analysis_started', analysisId });
|
|
1498
|
+
|
|
1499
|
+
const analyzer = new Analyzer(req.app.get('db'), model, provider);
|
|
1500
|
+
|
|
1501
|
+
logger.section(`AI Analysis Request - PR #${prNumber}`);
|
|
1502
|
+
logger.log('API', `Repository: ${repository}`, 'magenta');
|
|
1503
|
+
logger.log('API', `Worktree: ${worktreePath}`, 'magenta');
|
|
1504
|
+
logger.log('API', `Analysis ID: ${analysisId}`, 'magenta');
|
|
1505
|
+
logger.log('API', `Review ID: ${review.id}`, 'magenta');
|
|
1506
|
+
logger.log('API', `Provider: ${provider}`, 'cyan');
|
|
1507
|
+
logger.log('API', `Model: ${model}`, 'cyan');
|
|
1508
|
+
logger.log('API', `Tier: ${tier}`, 'cyan');
|
|
1509
|
+
if (combinedInstructions) {
|
|
1510
|
+
logger.log('API', `Custom instructions: ${combinedInstructions.length} chars`, 'cyan');
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
const progressCallback = createProgressCallback(analysisId);
|
|
1514
|
+
|
|
1515
|
+
analyzer.analyzeLevel1(review.id, worktreePath, prMetadata, progressCallback, { repoInstructions, requestInstructions }, null, { analysisId, runId, skipRunCreation: true, tier, skipLevel3: requestSkipLevel3, enabledLevels: levelsConfig })
|
|
1516
|
+
.then(async result => {
|
|
1517
|
+
logger.section('Analysis Results');
|
|
1518
|
+
logger.success(`Analysis complete for PR #${prNumber}`);
|
|
1519
|
+
logger.success(`Found ${result.suggestions.length} suggestions:`);
|
|
1520
|
+
|
|
1521
|
+
try {
|
|
1522
|
+
await prMetadataRepo.updateLastAiRunId(prMetadata.id, result.runId);
|
|
1523
|
+
logger.info(`Updated pr_metadata with last_ai_run_id: ${result.runId}`);
|
|
1524
|
+
} catch (updateError) {
|
|
1525
|
+
logger.warn(`Failed to update pr_metadata with last_ai_run_id: ${updateError.message}`);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
if (result.summary) {
|
|
1529
|
+
try {
|
|
1530
|
+
await reviewRepo.upsertSummary(prNumber, repository, result.summary);
|
|
1531
|
+
logger.info(`Saved analysis summary to review record`);
|
|
1532
|
+
logger.section('Analysis Summary');
|
|
1533
|
+
logger.info(result.summary);
|
|
1534
|
+
} catch (summaryError) {
|
|
1535
|
+
logger.warn(`Failed to save analysis summary: ${summaryError.message}`);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
result.suggestions.forEach(s => {
|
|
1539
|
+
const icon = s.type === 'bug' ? '\uD83D\uDC1B' :
|
|
1540
|
+
s.type === 'praise' ? '\u2B50' :
|
|
1541
|
+
s.type === 'improvement' ? '\uD83D\uDCA1' :
|
|
1542
|
+
s.type === 'security' ? '\uD83D\uDD12' :
|
|
1543
|
+
s.type === 'performance' ? '\u26A1' :
|
|
1544
|
+
s.type === 'design' ? '\uD83D\uDCD0' :
|
|
1545
|
+
s.type === 'suggestion' ? '\uD83D\uDCAC' :
|
|
1546
|
+
s.type === 'code-style' || s.type === 'style' ? '\uD83E\uDDF9' : '\uD83D\uDCDD';
|
|
1547
|
+
logger.log('Result', `${icon} ${s.type}: ${s.title} (${s.file}:${s.line_start})`, 'green');
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
const completionInfo = determineCompletionInfo(result);
|
|
1551
|
+
|
|
1552
|
+
const currentStatus = activeAnalyses.get(analysisId);
|
|
1553
|
+
if (!currentStatus) {
|
|
1554
|
+
logger.warn('Analysis already completed or removed:', analysisId);
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
if (currentStatus.status === 'cancelled') {
|
|
1559
|
+
logger.info(`Analysis ${analysisId} was cancelled, skipping completion update`);
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
for (let i = 1; i <= completionInfo.completedLevel; i++) {
|
|
1564
|
+
currentStatus.levels[i] = {
|
|
1565
|
+
status: 'completed',
|
|
1566
|
+
progress: `Level ${i} complete`
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
currentStatus.levels[4] = {
|
|
1571
|
+
status: 'completed',
|
|
1572
|
+
progress: 'Results finalized'
|
|
1573
|
+
};
|
|
1574
|
+
|
|
1575
|
+
const completedStatus = {
|
|
1576
|
+
...currentStatus,
|
|
1577
|
+
status: 'completed',
|
|
1578
|
+
level: completionInfo.completedLevel,
|
|
1579
|
+
completedLevel: completionInfo.completedLevel,
|
|
1580
|
+
completedAt: new Date().toISOString(),
|
|
1581
|
+
result,
|
|
1582
|
+
progress: completionInfo.progressMessage,
|
|
1583
|
+
suggestionsCount: completionInfo.totalSuggestions,
|
|
1584
|
+
filesAnalyzed: currentStatus?.filesAnalyzed || 0,
|
|
1585
|
+
filesRemaining: 0,
|
|
1586
|
+
currentFile: currentStatus?.totalFiles || 0,
|
|
1587
|
+
totalFiles: currentStatus?.totalFiles || 0
|
|
1588
|
+
};
|
|
1589
|
+
activeAnalyses.set(analysisId, completedStatus);
|
|
1590
|
+
|
|
1591
|
+
broadcastProgress(analysisId, completedStatus);
|
|
1592
|
+
broadcastReviewEvent(review.id, { type: 'review:analysis_completed' });
|
|
1593
|
+
})
|
|
1594
|
+
.catch(error => {
|
|
1595
|
+
const currentStatus = activeAnalyses.get(analysisId);
|
|
1596
|
+
if (!currentStatus) {
|
|
1597
|
+
logger.warn('Analysis status not found during error handling:', analysisId);
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
if (error.isCancellation) {
|
|
1602
|
+
logger.info(`Analysis cancelled for PR #${prNumber}`);
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
logger.error(`Analysis failed for PR #${prNumber}: ${error.message}`);
|
|
1607
|
+
|
|
1608
|
+
for (let i = 1; i <= 4; i++) {
|
|
1609
|
+
currentStatus.levels[i] = {
|
|
1610
|
+
status: 'failed',
|
|
1611
|
+
progress: 'Failed'
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
const failedStatus = {
|
|
1616
|
+
...currentStatus,
|
|
1617
|
+
status: 'failed',
|
|
1618
|
+
level: 1,
|
|
1619
|
+
completedAt: new Date().toISOString(),
|
|
1620
|
+
error: error.message,
|
|
1621
|
+
progress: 'Analysis failed'
|
|
1622
|
+
};
|
|
1623
|
+
activeAnalyses.set(analysisId, failedStatus);
|
|
1624
|
+
|
|
1625
|
+
broadcastProgress(analysisId, failedStatus);
|
|
1626
|
+
})
|
|
1627
|
+
.finally(() => {
|
|
1628
|
+
// Clean up review to analysis ID mapping (unified map)
|
|
1629
|
+
reviewToAnalysisId.delete(review.id);
|
|
1630
|
+
});
|
|
1631
|
+
|
|
1632
|
+
res.json({
|
|
1633
|
+
analysisId,
|
|
1634
|
+
runId,
|
|
1635
|
+
status: 'started',
|
|
1636
|
+
message: 'AI analysis started in background'
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
logger.error('Error starting AI analysis:', error);
|
|
1641
|
+
res.status(500).json({
|
|
1642
|
+
error: 'Failed to start AI analysis'
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
/**
|
|
1648
|
+
* Trigger council analysis for a PR
|
|
1649
|
+
*/
|
|
1650
|
+
router.post('/api/pr/:owner/:repo/:number/analyses/council', async (req, res) => {
|
|
1651
|
+
try {
|
|
1652
|
+
const { owner, repo, number } = req.params;
|
|
1653
|
+
const prNumber = parseInt(number);
|
|
1654
|
+
const { councilId, councilConfig: inlineConfig, customInstructions: rawInstructions, configType: requestConfigType } = req.body || {};
|
|
1655
|
+
|
|
1656
|
+
if (isNaN(prNumber) || prNumber <= 0) {
|
|
1657
|
+
return res.status(400).json({ error: 'Invalid pull request number' });
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
if (!councilId && !inlineConfig) {
|
|
1661
|
+
return res.status(400).json({ error: 'Either councilId or councilConfig is required' });
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
const repository = normalizeRepository(owner, repo);
|
|
1665
|
+
const db = req.app.get('db');
|
|
1666
|
+
|
|
1667
|
+
let councilConfig;
|
|
1668
|
+
let configType;
|
|
1669
|
+
if (councilId) {
|
|
1670
|
+
const councilRepo = new CouncilRepository(db);
|
|
1671
|
+
const council = await councilRepo.getById(councilId);
|
|
1672
|
+
if (!council) {
|
|
1673
|
+
return res.status(404).json({ error: 'Council not found' });
|
|
1674
|
+
}
|
|
1675
|
+
councilConfig = council.config;
|
|
1676
|
+
configType = requestConfigType || council.type || 'advanced';
|
|
1677
|
+
} else {
|
|
1678
|
+
councilConfig = inlineConfig;
|
|
1679
|
+
configType = requestConfigType || 'advanced';
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
councilConfig = normalizeCouncilConfig(councilConfig, configType);
|
|
1683
|
+
|
|
1684
|
+
const configError = validateCouncilConfig(councilConfig, configType);
|
|
1685
|
+
if (configError) {
|
|
1686
|
+
return res.status(400).json({ error: `Invalid council config: ${configError}` });
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
const prMetadataRepo = new PRMetadataRepository(db);
|
|
1690
|
+
const prMetadata = await prMetadataRepo.getByPR(prNumber, repository);
|
|
1691
|
+
if (!prMetadata) {
|
|
1692
|
+
return res.status(404).json({ error: `Pull request #${prNumber} not found. Please load the PR first.` });
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const worktreeManager = new GitWorktreeManager(db);
|
|
1696
|
+
const worktreePath = await worktreeManager.getWorktreePath({ owner, repo, number: prNumber });
|
|
1697
|
+
if (!await worktreeManager.worktreeExists({ owner, repo, number: prNumber })) {
|
|
1698
|
+
return res.status(404).json({ error: 'Worktree not found for this PR. Please reload the PR.' });
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
const reviewRepo = new ReviewRepository(db);
|
|
1702
|
+
const repoSettingsRepo = new RepoSettingsRepository(db);
|
|
1703
|
+
const repoSettings = await repoSettingsRepo.getRepoSettings(repository);
|
|
1704
|
+
const repoInstructions = repoSettings?.default_instructions || null;
|
|
1705
|
+
const requestInstructions = rawInstructions?.trim() || null;
|
|
1706
|
+
|
|
1707
|
+
const review = await reviewRepo.getOrCreate({ prNumber, repository });
|
|
1708
|
+
|
|
1709
|
+
if (requestInstructions) {
|
|
1710
|
+
await reviewRepo.upsertCustomInstructions(prNumber, repository, requestInstructions);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
const { analysisId, runId } = await analysesRouter.launchCouncilAnalysis(
|
|
1714
|
+
db,
|
|
1715
|
+
{
|
|
1716
|
+
reviewId: review.id,
|
|
1717
|
+
worktreePath,
|
|
1718
|
+
prMetadata,
|
|
1719
|
+
changedFiles: null,
|
|
1720
|
+
repository,
|
|
1721
|
+
headSha: prMetadata.head_sha,
|
|
1722
|
+
logLabel: `PR #${prNumber}`,
|
|
1723
|
+
initialStatusExtra: { prNumber, reviewType: 'pr' },
|
|
1724
|
+
extraBroadcastKeys: null,
|
|
1725
|
+
onSuccess: async (result) => {
|
|
1726
|
+
if (result.summary) {
|
|
1727
|
+
await reviewRepo.upsertSummary(prNumber, repository, result.summary);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
},
|
|
1731
|
+
councilConfig,
|
|
1732
|
+
councilId,
|
|
1733
|
+
{ repoInstructions, requestInstructions },
|
|
1734
|
+
configType
|
|
1735
|
+
);
|
|
1736
|
+
|
|
1737
|
+
res.json({
|
|
1738
|
+
analysisId,
|
|
1739
|
+
runId,
|
|
1740
|
+
status: 'started',
|
|
1741
|
+
message: 'Council analysis started in background',
|
|
1742
|
+
isCouncil: true
|
|
1743
|
+
});
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
logger.error('Error starting council analysis:', error);
|
|
1746
|
+
res.status(500).json({ error: 'Failed to start council analysis' });
|
|
1747
|
+
}
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1392
1750
|
module.exports = router;
|