@in-the-loop-labs/pair-review 3.2.2 → 3.3.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 +7 -6
- package/package.json +5 -4
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/skills/analyze/references/orchestration-balanced.md +9 -1
- package/plugin-code-critic/skills/analyze/references/orchestration-fast.md +8 -1
- package/plugin-code-critic/skills/analyze/references/orchestration-thorough.md +8 -7
- package/public/css/repo-settings.css +347 -0
- package/public/index.html +46 -9
- package/public/js/components/AIPanel.js +79 -37
- package/public/js/components/DiffOptionsDropdown.js +84 -1
- package/public/js/index.js +31 -6
- package/public/js/modules/analysis-history.js +11 -7
- package/public/js/pr.js +22 -0
- package/public/js/repo-settings.js +334 -6
- package/public/repo-settings.html +29 -0
- package/src/ai/analyzer.js +28 -19
- package/src/ai/claude-cli.js +2 -0
- package/src/ai/claude-provider.js +4 -1
- package/src/ai/prompts/baseline/consolidation/balanced.js +6 -4
- package/src/ai/prompts/baseline/consolidation/fast.js +6 -2
- package/src/ai/prompts/baseline/consolidation/thorough.js +7 -6
- package/src/ai/prompts/baseline/orchestration/balanced.js +13 -1
- package/src/ai/prompts/baseline/orchestration/fast.js +12 -1
- package/src/ai/prompts/baseline/orchestration/thorough.js +8 -7
- package/src/ai/provider.js +7 -6
- package/src/chat/session-manager.js +6 -3
- package/src/config.js +230 -38
- package/src/database.js +766 -38
- package/src/git/worktree-pool-lifecycle.js +674 -0
- package/src/git/worktree-pool-usage.js +216 -0
- package/src/git/worktree.js +46 -13
- package/src/main.js +185 -26
- package/src/routes/analyses.js +48 -26
- package/src/routes/chat.js +27 -3
- package/src/routes/config.js +17 -5
- package/src/routes/executable-analysis.js +38 -19
- package/src/routes/local.js +19 -6
- package/src/routes/mcp.js +13 -2
- package/src/routes/pr.js +72 -29
- package/src/routes/setup.js +41 -4
- package/src/routes/stack-analysis.js +29 -10
- package/src/routes/worktrees.js +294 -9
- package/src/server.js +20 -3
- package/src/setup/pr-setup.js +161 -27
- package/src/ws/server.js +51 -1
|
@@ -234,7 +234,8 @@ async function runExecutableAnalysis(req, res, params, shared, callbacks) {
|
|
|
234
234
|
reviewId, review, selectedProvider, selectedModel,
|
|
235
235
|
repoInstructions, requestInstructions,
|
|
236
236
|
runId, analysisId, repository, reviewType, headSha,
|
|
237
|
-
extraInitialStatus
|
|
237
|
+
extraInitialStatus,
|
|
238
|
+
providerOverrides = {}
|
|
238
239
|
} = params;
|
|
239
240
|
|
|
240
241
|
const {
|
|
@@ -291,32 +292,48 @@ async function runExecutableAnalysis(req, res, params, shared, callbacks) {
|
|
|
291
292
|
broadcastProgress(analysisId, initialStatus);
|
|
292
293
|
broadcastReviewEvent(reviewId, { type: 'review:analysis_started', analysisId });
|
|
293
294
|
|
|
294
|
-
//
|
|
295
|
+
// Register analysis hold for pool worktree usage tracking.
|
|
296
|
+
// startAnalysis is inside the try so a synchronous exception in setup still
|
|
297
|
+
// cleans up the hold (the .finally() inside the async IIFE only covers errors after it starts).
|
|
298
|
+
const poolLifecycle = params.poolLifecycle;
|
|
299
|
+
let poolWorktreeId;
|
|
295
300
|
const analysisHookConfig = req.app.get('config') || {};
|
|
296
301
|
const hookPayloadFields = buildHookPayload(review, {});
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
302
|
+
try {
|
|
303
|
+
poolWorktreeId = await poolLifecycle?.startAnalysis(reviewId, analysisId);
|
|
304
|
+
|
|
305
|
+
// 3. Fire analysis.started hook
|
|
306
|
+
if (hasHooks('analysis.started', analysisHookConfig)) {
|
|
307
|
+
getCachedUser(analysisHookConfig).then(user => {
|
|
308
|
+
fireHooks('analysis.started', buildAnalysisStartedPayload({
|
|
309
|
+
reviewId, analysisId, provider: selectedProvider, model: selectedModel,
|
|
310
|
+
...hookPayloadFields,
|
|
311
|
+
user,
|
|
312
|
+
}), analysisHookConfig);
|
|
313
|
+
}).catch(() => {});
|
|
314
|
+
}
|
|
306
315
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
316
|
+
// 4. Respond immediately — analysis runs async
|
|
317
|
+
res.json({
|
|
318
|
+
analysisId,
|
|
319
|
+
runId,
|
|
320
|
+
status: 'running',
|
|
321
|
+
message: 'Executable provider analysis started'
|
|
322
|
+
});
|
|
323
|
+
} catch (setupError) {
|
|
324
|
+
// Synchronous setup failure — clean up the analysis hold immediately
|
|
325
|
+
reviewToAnalysisId.delete(reviewId);
|
|
326
|
+
if (poolWorktreeId) {
|
|
327
|
+
poolLifecycle?.endAnalysis(analysisId);
|
|
328
|
+
}
|
|
329
|
+
throw setupError;
|
|
330
|
+
}
|
|
314
331
|
|
|
315
332
|
// 5. Run the executable provider asynchronously
|
|
316
333
|
(async () => {
|
|
317
334
|
const tmpDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'pair-review-exec-'));
|
|
318
335
|
try {
|
|
319
|
-
const provider = createProvider(selectedProvider, selectedModel);
|
|
336
|
+
const provider = createProvider(selectedProvider, selectedModel, providerOverrides);
|
|
320
337
|
|
|
321
338
|
const executableContext = {
|
|
322
339
|
...buildContext(review, { selectedModel, requestInstructions }),
|
|
@@ -476,6 +493,8 @@ async function runExecutableAnalysis(req, res, params, shared, callbacks) {
|
|
|
476
493
|
// Do NOT delete activeAnalyses entry — leave it with terminal status so
|
|
477
494
|
// clients can poll for final results via HTTP (matches local.js/pr.js).
|
|
478
495
|
reviewToAnalysisId.delete(reviewId);
|
|
496
|
+
// Remove pool worktree analysis hold
|
|
497
|
+
poolLifecycle?.endAnalysis(analysisId);
|
|
479
498
|
|
|
480
499
|
// Clean up temp directory (keep in debug mode for inspection)
|
|
481
500
|
if (tmpDir) {
|
package/src/routes/local.js
CHANGED
|
@@ -24,7 +24,7 @@ const { broadcastReviewEvent } = require('../events/review-events');
|
|
|
24
24
|
const { fireHooks, hasHooks } = require('../hooks/hook-runner');
|
|
25
25
|
const { buildReviewStartedPayload, buildReviewLoadedPayload, buildAnalysisStartedPayload, buildAnalysisCompletedPayload, getCachedUser } = require('../hooks/payloads');
|
|
26
26
|
const { mergeInstructions } = require('../utils/instructions');
|
|
27
|
-
const { getGitHubToken } = require('../config');
|
|
27
|
+
const { getGitHubToken, resolveLoadSkills, buildCouncilProviderOverrides } = require('../config');
|
|
28
28
|
const { generateScopedDiff, computeScopedDigest, getBranchCommitCount, getFirstCommitSubject, detectAndBuildBranchInfo, findMergeBase, getCurrentBranch, getRepositoryName } = require('../local-review');
|
|
29
29
|
const { STOPS, isValidScope, normalizeScope, reviewScope, includesBranch, DEFAULT_SCOPE } = require('../local-scope');
|
|
30
30
|
const { getGeneratedFilePatterns } = require('../git/gitattributes');
|
|
@@ -1023,7 +1023,8 @@ router.get('/api/local/:reviewId/check-stale', async (req, res) => {
|
|
|
1023
1023
|
*/
|
|
1024
1024
|
async function handleExecutableAnalysis(req, res, {
|
|
1025
1025
|
reviewId, review, localPath, repository, selectedProvider, selectedModel,
|
|
1026
|
-
repoInstructions, requestInstructions, combinedInstructions, runId, analysisId, reviewRepo
|
|
1026
|
+
repoInstructions, requestInstructions, combinedInstructions, runId, analysisId, reviewRepo,
|
|
1027
|
+
providerOverrides
|
|
1027
1028
|
}) {
|
|
1028
1029
|
return runExecutableAnalysis(req, res, {
|
|
1029
1030
|
reviewId,
|
|
@@ -1036,7 +1037,8 @@ async function handleExecutableAnalysis(req, res, {
|
|
|
1036
1037
|
analysisId,
|
|
1037
1038
|
repository,
|
|
1038
1039
|
reviewType: review.review_type || 'local',
|
|
1039
|
-
headSha: review.local_head_sha
|
|
1040
|
+
headSha: review.local_head_sha,
|
|
1041
|
+
providerOverrides
|
|
1040
1042
|
}, {
|
|
1041
1043
|
activeAnalyses,
|
|
1042
1044
|
reviewToAnalysisId,
|
|
@@ -1171,6 +1173,11 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
|
|
|
1171
1173
|
const runId = uuidv4();
|
|
1172
1174
|
const analysisId = runId;
|
|
1173
1175
|
|
|
1176
|
+
// Resolve load_skills across all config tiers
|
|
1177
|
+
const providerLoadSkills = appConfig.providers?.[selectedProvider]?.load_skills;
|
|
1178
|
+
const loadSkills = resolveLoadSkills(appConfig, repository, repoSettings, providerLoadSkills);
|
|
1179
|
+
const providerOverrides = { load_skills: loadSkills };
|
|
1180
|
+
|
|
1174
1181
|
// Check if selected provider is an executable provider (external tool)
|
|
1175
1182
|
const ProviderClass = getProviderClass(selectedProvider);
|
|
1176
1183
|
if (ProviderClass?.isExecutable) {
|
|
@@ -1186,7 +1193,8 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
|
|
|
1186
1193
|
combinedInstructions,
|
|
1187
1194
|
runId,
|
|
1188
1195
|
analysisId,
|
|
1189
|
-
reviewRepo
|
|
1196
|
+
reviewRepo,
|
|
1197
|
+
providerOverrides
|
|
1190
1198
|
});
|
|
1191
1199
|
}
|
|
1192
1200
|
|
|
@@ -1258,7 +1266,7 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
|
|
|
1258
1266
|
}
|
|
1259
1267
|
|
|
1260
1268
|
// Create analyzer instance with provider and model
|
|
1261
|
-
const analyzer = new Analyzer(db, selectedModel, selectedProvider);
|
|
1269
|
+
const analyzer = new Analyzer(db, selectedModel, selectedProvider, providerOverrides);
|
|
1262
1270
|
|
|
1263
1271
|
// Build local review metadata for the analyzer
|
|
1264
1272
|
// The analyzer uses base_sha and head_sha for git diff commands
|
|
@@ -2051,7 +2059,6 @@ router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
|
|
|
2051
2059
|
scopeEnd: councilScopeEnd,
|
|
2052
2060
|
};
|
|
2053
2061
|
|
|
2054
|
-
const analyzer = new Analyzer(db, 'council', 'council');
|
|
2055
2062
|
// Use the scope-aware helper so the file list matches the generated diff
|
|
2056
2063
|
// (covers branch, staged, unstaged, and untracked stops as appropriate).
|
|
2057
2064
|
const changedFiles = await getChangedFiles(localPath, {
|
|
@@ -2085,6 +2092,10 @@ router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
|
|
|
2085
2092
|
// Import launchCouncilAnalysis from analyses.js
|
|
2086
2093
|
const analysesRouter = require('./analyses');
|
|
2087
2094
|
const localCouncilConfig = req.app.get('config') || {};
|
|
2095
|
+
|
|
2096
|
+
const { providerOverrides: councilProviderOverrides, providerOverridesMap: councilProviderOverridesMap } =
|
|
2097
|
+
buildCouncilProviderOverrides(localCouncilConfig, review.repository, repoSettings);
|
|
2098
|
+
|
|
2088
2099
|
const { analysisId, runId } = await analysesRouter.launchCouncilAnalysis(
|
|
2089
2100
|
db,
|
|
2090
2101
|
{
|
|
@@ -2099,6 +2110,8 @@ router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
|
|
|
2099
2110
|
config: localCouncilConfig,
|
|
2100
2111
|
excludePrevious,
|
|
2101
2112
|
serverPort: req.socket.localPort,
|
|
2113
|
+
providerOverrides: councilProviderOverrides,
|
|
2114
|
+
providerOverridesMap: councilProviderOverridesMap,
|
|
2102
2115
|
hookContext: {
|
|
2103
2116
|
mode: 'local',
|
|
2104
2117
|
localContext: { path: localPath, branch: review.local_head_branch, headSha: review.local_head_sha },
|
package/src/routes/mcp.js
CHANGED
|
@@ -23,6 +23,7 @@ const {
|
|
|
23
23
|
createProgressCallback
|
|
24
24
|
} = require('./shared');
|
|
25
25
|
const { safeParseJson } = require('../utils/safe-parse-json');
|
|
26
|
+
const { resolveLoadSkills } = require('../config');
|
|
26
27
|
|
|
27
28
|
// All valid tier values: canonical tiers + aliases (for Zod enum validation)
|
|
28
29
|
const ALL_TIER_VALUES = /** @type {[string, ...string[]]} */ ([...TIERS, ...Object.keys(TIER_ALIASES)]);
|
|
@@ -595,8 +596,13 @@ function createMCPServer(db, options = {}) {
|
|
|
595
596
|
broadcastProgress(analysisId, initialStatus);
|
|
596
597
|
broadcastReviewEvent(reviewId, { type: 'review:analysis_started', analysisId });
|
|
597
598
|
|
|
599
|
+
// Resolve load_skills across all config tiers
|
|
600
|
+
const providerLoadSkills = config.providers?.[provider]?.load_skills;
|
|
601
|
+
const loadSkills = resolveLoadSkills(config, repository, repoSettings, providerLoadSkills);
|
|
602
|
+
const providerOverrides = { load_skills: loadSkills };
|
|
603
|
+
|
|
598
604
|
// Create analyzer and launch asynchronously
|
|
599
|
-
const analyzer = new Analyzer(db, model, provider);
|
|
605
|
+
const analyzer = new Analyzer(db, model, provider, providerOverrides);
|
|
600
606
|
const localMetadata = {
|
|
601
607
|
id: reviewId,
|
|
602
608
|
repository: review.repository,
|
|
@@ -747,7 +753,12 @@ function createMCPServer(db, options = {}) {
|
|
|
747
753
|
broadcastProgress(analysisId, initialStatus);
|
|
748
754
|
broadcastReviewEvent(review.id, { type: 'review:analysis_started', analysisId });
|
|
749
755
|
|
|
750
|
-
|
|
756
|
+
// Resolve load_skills across all config tiers
|
|
757
|
+
const prProviderLoadSkills = config.providers?.[provider]?.load_skills;
|
|
758
|
+
const prLoadSkills = resolveLoadSkills(config, repository, repoSettings, prProviderLoadSkills);
|
|
759
|
+
const prProviderOverrides = { load_skills: prLoadSkills };
|
|
760
|
+
|
|
761
|
+
const analyzer = new Analyzer(db, model, provider, prProviderOverrides);
|
|
751
762
|
const progressCallback = createProgressCallback(analysisId);
|
|
752
763
|
const tier = resolveTier(args.tier);
|
|
753
764
|
|
package/src/routes/pr.js
CHANGED
|
@@ -25,7 +25,7 @@ const Analyzer = require('../ai/analyzer');
|
|
|
25
25
|
const { v4: uuidv4 } = require('uuid');
|
|
26
26
|
const fs = require('fs').promises;
|
|
27
27
|
const path = require('path');
|
|
28
|
-
const { getGitHubToken } = require('../config');
|
|
28
|
+
const { getGitHubToken, getWorktreeDisplayName, resolveLoadSkills, buildCouncilProviderOverrides } = require('../config');
|
|
29
29
|
const logger = require('../utils/logger');
|
|
30
30
|
const { buildDiffLineSet } = require('../utils/diff-annotator');
|
|
31
31
|
const { broadcastReviewEvent } = require('../events/review-events');
|
|
@@ -278,6 +278,8 @@ router.get('/api/pr/:owner/:repo/:number', async (req, res) => {
|
|
|
278
278
|
additions: extendedData.additions || 0,
|
|
279
279
|
deletions: extendedData.deletions || 0,
|
|
280
280
|
diff_content: extendedData.diff || '',
|
|
281
|
+
worktree_path: extendedData.worktree_path || null,
|
|
282
|
+
worktree_name: getWorktreeDisplayName(extendedData.worktree_path, req.app.get('config') || {}, repository),
|
|
281
283
|
html_url: extendedData.html_url || `https://github.com/${repoOwner}/${repoName}/pull/${prMetadata.pr_number}`,
|
|
282
284
|
pendingDraft: pendingDraft ? {
|
|
283
285
|
id: pendingDraft.id,
|
|
@@ -392,7 +394,8 @@ router.post('/api/pr/:owner/:repo/:number/refresh', async (req, res) => {
|
|
|
392
394
|
html_url: prData.html_url,
|
|
393
395
|
base_sha: prData.base_sha,
|
|
394
396
|
head_sha: prData.head_sha,
|
|
395
|
-
node_id: prData.node_id // GraphQL node ID for PR (required for GraphQL review submission)
|
|
397
|
+
node_id: prData.node_id, // GraphQL node ID for PR (required for GraphQL review submission)
|
|
398
|
+
worktree_path: worktreePath
|
|
396
399
|
};
|
|
397
400
|
|
|
398
401
|
// Update database with new data
|
|
@@ -485,7 +488,9 @@ router.post('/api/pr/:owner/:repo/:number/refresh', async (req, res) => {
|
|
|
485
488
|
html_url: parsedData.html_url || `https://github.com/${repoOwner}/${repoName}/pull/${prMetadata.pr_number}`,
|
|
486
489
|
head_sha: parsedData.head_sha,
|
|
487
490
|
base_sha: parsedData.base_sha,
|
|
488
|
-
node_id: parsedData.node_id
|
|
491
|
+
node_id: parsedData.node_id,
|
|
492
|
+
worktree_path: parsedData.worktree_path || null,
|
|
493
|
+
worktree_name: getWorktreeDisplayName(parsedData.worktree_path, config, repository)
|
|
489
494
|
}
|
|
490
495
|
};
|
|
491
496
|
|
|
@@ -1513,7 +1518,7 @@ router.post('/api/parse-pr-url', (req, res) => {
|
|
|
1513
1518
|
async function handleExecutablePRAnalysis(req, res, {
|
|
1514
1519
|
reviewId, review, prNumber, owner, repo, repository, worktreePath, prMetadata,
|
|
1515
1520
|
selectedProvider, selectedModel, repoInstructions, requestInstructions,
|
|
1516
|
-
combinedInstructions, runId, analysisId, reviewRepo
|
|
1521
|
+
combinedInstructions, runId, analysisId, reviewRepo, poolLifecycle, providerOverrides
|
|
1517
1522
|
}) {
|
|
1518
1523
|
const prContext = {
|
|
1519
1524
|
number: prNumber, owner, repo,
|
|
@@ -1533,7 +1538,9 @@ async function handleExecutablePRAnalysis(req, res, {
|
|
|
1533
1538
|
repository,
|
|
1534
1539
|
reviewType: 'pr',
|
|
1535
1540
|
headSha: prMetadata.head_sha,
|
|
1536
|
-
extraInitialStatus: { prNumber }
|
|
1541
|
+
extraInitialStatus: { prNumber },
|
|
1542
|
+
poolLifecycle,
|
|
1543
|
+
providerOverrides
|
|
1537
1544
|
}, {
|
|
1538
1545
|
activeAnalyses,
|
|
1539
1546
|
reviewToAnalysisId,
|
|
@@ -1645,7 +1652,7 @@ router.post('/api/pr/:owner/:repo/:number/analyses', async (req, res) => {
|
|
|
1645
1652
|
const appConfig = req.app.get('config') || {};
|
|
1646
1653
|
const globalInstructions = appConfig.globalInstructions || null;
|
|
1647
1654
|
|
|
1648
|
-
const { provider, model, repoInstructions, combinedInstructions } = await withTransaction(db, async () => {
|
|
1655
|
+
const { provider, model, repoInstructions, combinedInstructions, repoSettings: fetchedRepoSettings } = await withTransaction(db, async () => {
|
|
1649
1656
|
const repoSettingsRepo = new RepoSettingsRepository(db);
|
|
1650
1657
|
const fetchedRepoSettings = await repoSettingsRepo.getRepoSettings(repository);
|
|
1651
1658
|
|
|
@@ -1678,7 +1685,8 @@ router.post('/api/pr/:owner/:repo/:number/analyses', async (req, res) => {
|
|
|
1678
1685
|
provider: selectedProvider,
|
|
1679
1686
|
model: selectedModel,
|
|
1680
1687
|
repoInstructions: fetchedRepoInstructions,
|
|
1681
|
-
combinedInstructions: mergedInstructions
|
|
1688
|
+
combinedInstructions: mergedInstructions,
|
|
1689
|
+
repoSettings: fetchedRepoSettings
|
|
1682
1690
|
};
|
|
1683
1691
|
});
|
|
1684
1692
|
|
|
@@ -1687,6 +1695,11 @@ router.post('/api/pr/:owner/:repo/:number/analyses', async (req, res) => {
|
|
|
1687
1695
|
|
|
1688
1696
|
const { review } = await reviewRepo.getOrCreate({ prNumber, repository });
|
|
1689
1697
|
|
|
1698
|
+
// Resolve load_skills across all config tiers
|
|
1699
|
+
const providerLoadSkills = appConfig.providers?.[provider]?.load_skills;
|
|
1700
|
+
const loadSkills = resolveLoadSkills(appConfig, repository, fetchedRepoSettings, providerLoadSkills);
|
|
1701
|
+
const providerOverrides = { load_skills: loadSkills };
|
|
1702
|
+
|
|
1690
1703
|
// Check if selected provider is an executable provider (external tool)
|
|
1691
1704
|
const ProviderClass = getProviderClass(provider);
|
|
1692
1705
|
if (ProviderClass?.isExecutable) {
|
|
@@ -1706,7 +1719,9 @@ router.post('/api/pr/:owner/:repo/:number/analyses', async (req, res) => {
|
|
|
1706
1719
|
combinedInstructions,
|
|
1707
1720
|
runId,
|
|
1708
1721
|
analysisId,
|
|
1709
|
-
reviewRepo
|
|
1722
|
+
reviewRepo,
|
|
1723
|
+
poolLifecycle: req.app.get('poolLifecycle'),
|
|
1724
|
+
providerOverrides
|
|
1710
1725
|
});
|
|
1711
1726
|
}
|
|
1712
1727
|
|
|
@@ -1753,37 +1768,56 @@ router.post('/api/pr/:owner/:repo/:number/analyses', async (req, res) => {
|
|
|
1753
1768
|
|
|
1754
1769
|
broadcastProgress(analysisId, initialStatus);
|
|
1755
1770
|
broadcastReviewEvent(review.id, { type: 'review:analysis_started', analysisId });
|
|
1771
|
+
|
|
1772
|
+
// Register analysis hold for pool worktree usage tracking.
|
|
1773
|
+
// startAnalysis is inside the try so a synchronous exception in setup still
|
|
1774
|
+
// cleans up the hold (the .finally() on the promise chain only covers async errors).
|
|
1775
|
+
const poolLifecycle = req.app.get('poolLifecycle');
|
|
1776
|
+
let poolWorktreeId;
|
|
1756
1777
|
const analysisConfig = appConfig;
|
|
1757
1778
|
const analysisPrContext = {
|
|
1758
1779
|
number: prNumber, owner, repo,
|
|
1759
1780
|
author: prMetadata.author, baseBranch: prMetadata.base_branch, headBranch: prMetadata.head_branch,
|
|
1760
1781
|
baseSha: prMetadata.base_sha || null, headSha: prMetadata.head_sha || null,
|
|
1761
1782
|
};
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1783
|
+
let analysisPromise;
|
|
1784
|
+
try {
|
|
1785
|
+
poolWorktreeId = await poolLifecycle?.startAnalysis(review.id, analysisId);
|
|
1786
|
+
if (hasHooks('analysis.started', analysisConfig)) {
|
|
1787
|
+
getCachedUser(analysisConfig).then(user => {
|
|
1788
|
+
fireHooks('analysis.started', buildAnalysisStartedPayload({
|
|
1789
|
+
reviewId: review.id, analysisId, provider, model, mode: 'pr', prContext: analysisPrContext, user,
|
|
1790
|
+
}), analysisConfig);
|
|
1791
|
+
}).catch(() => {});
|
|
1792
|
+
}
|
|
1769
1793
|
|
|
1770
|
-
|
|
1794
|
+
const analyzer = new Analyzer(req.app.get('db'), model, provider, providerOverrides);
|
|
1795
|
+
|
|
1796
|
+
logger.section(`AI Analysis Request - PR #${prNumber}`);
|
|
1797
|
+
logger.log('API', `Repository: ${repository}`, 'magenta');
|
|
1798
|
+
logger.log('API', `Worktree: ${worktreePath}`, 'magenta');
|
|
1799
|
+
logger.log('API', `Analysis ID: ${analysisId}`, 'magenta');
|
|
1800
|
+
logger.log('API', `Review ID: ${review.id}`, 'magenta');
|
|
1801
|
+
logger.log('API', `Provider: ${provider}`, 'cyan');
|
|
1802
|
+
logger.log('API', `Model: ${model}`, 'cyan');
|
|
1803
|
+
logger.log('API', `Tier: ${tier}`, 'cyan');
|
|
1804
|
+
if (combinedInstructions) {
|
|
1805
|
+
logger.log('API', `Custom instructions: ${combinedInstructions.length} chars`, 'cyan');
|
|
1806
|
+
}
|
|
1771
1807
|
|
|
1772
|
-
|
|
1773
|
-
logger.log('API', `Repository: ${repository}`, 'magenta');
|
|
1774
|
-
logger.log('API', `Worktree: ${worktreePath}`, 'magenta');
|
|
1775
|
-
logger.log('API', `Analysis ID: ${analysisId}`, 'magenta');
|
|
1776
|
-
logger.log('API', `Review ID: ${review.id}`, 'magenta');
|
|
1777
|
-
logger.log('API', `Provider: ${provider}`, 'cyan');
|
|
1778
|
-
logger.log('API', `Model: ${model}`, 'cyan');
|
|
1779
|
-
logger.log('API', `Tier: ${tier}`, 'cyan');
|
|
1780
|
-
if (combinedInstructions) {
|
|
1781
|
-
logger.log('API', `Custom instructions: ${combinedInstructions.length} chars`, 'cyan');
|
|
1782
|
-
}
|
|
1808
|
+
const progressCallback = createProgressCallback(analysisId);
|
|
1783
1809
|
|
|
1784
|
-
|
|
1810
|
+
analysisPromise = analyzer.analyzeLevel1(review.id, worktreePath, prMetadata, progressCallback, { globalInstructions, repoInstructions, requestInstructions }, null, { analysisId, runId, skipRunCreation: true, tier, skipLevel3: requestSkipLevel3, enabledLevels: levelsConfig, excludePrevious, serverPort: req.socket.localPort });
|
|
1811
|
+
} catch (setupError) {
|
|
1812
|
+
// Synchronous setup failure — clean up the analysis hold immediately
|
|
1813
|
+
reviewToAnalysisId.delete(review.id);
|
|
1814
|
+
if (poolWorktreeId) {
|
|
1815
|
+
poolLifecycle?.endAnalysis(analysisId);
|
|
1816
|
+
}
|
|
1817
|
+
throw setupError;
|
|
1818
|
+
}
|
|
1785
1819
|
|
|
1786
|
-
|
|
1820
|
+
analysisPromise
|
|
1787
1821
|
.then(async result => {
|
|
1788
1822
|
logger.section('Analysis Results');
|
|
1789
1823
|
logger.success(`Analysis complete for PR #${prNumber}`);
|
|
@@ -1929,6 +1963,8 @@ router.post('/api/pr/:owner/:repo/:number/analyses', async (req, res) => {
|
|
|
1929
1963
|
.finally(() => {
|
|
1930
1964
|
// Clean up review to analysis ID mapping (unified map)
|
|
1931
1965
|
reviewToAnalysisId.delete(review.id);
|
|
1966
|
+
// Remove pool worktree analysis hold
|
|
1967
|
+
poolLifecycle?.endAnalysis(analysisId);
|
|
1932
1968
|
});
|
|
1933
1969
|
|
|
1934
1970
|
res.json({
|
|
@@ -2013,6 +2049,10 @@ router.post('/api/pr/:owner/:repo/:number/analyses/council', async (req, res) =>
|
|
|
2013
2049
|
}
|
|
2014
2050
|
|
|
2015
2051
|
const prCouncilConfig = req.app.get('config') || {};
|
|
2052
|
+
|
|
2053
|
+
const { providerOverrides: councilProviderOverrides, providerOverridesMap: councilProviderOverridesMap } =
|
|
2054
|
+
buildCouncilProviderOverrides(prCouncilConfig, repository, repoSettings);
|
|
2055
|
+
|
|
2016
2056
|
const { analysisId, runId } = await analysesRouter.launchCouncilAnalysis(
|
|
2017
2057
|
db,
|
|
2018
2058
|
{
|
|
@@ -2027,6 +2067,9 @@ router.post('/api/pr/:owner/:repo/:number/analyses/council', async (req, res) =>
|
|
|
2027
2067
|
config: prCouncilConfig,
|
|
2028
2068
|
excludePrevious,
|
|
2029
2069
|
serverPort: req.socket.localPort,
|
|
2070
|
+
poolLifecycle: req.app.get('poolLifecycle'),
|
|
2071
|
+
providerOverrides: councilProviderOverrides,
|
|
2072
|
+
providerOverridesMap: councilProviderOverridesMap,
|
|
2030
2073
|
hookContext: {
|
|
2031
2074
|
mode: 'pr',
|
|
2032
2075
|
prContext: {
|
package/src/routes/setup.js
CHANGED
|
@@ -16,7 +16,7 @@ const { activeSetups, broadcastSetupProgress } = require('./shared');
|
|
|
16
16
|
const { setupPRReview } = require('../setup/pr-setup');
|
|
17
17
|
const { setupLocalReview } = require('../setup/local-setup');
|
|
18
18
|
const { getGitHubToken, expandPath } = require('../config');
|
|
19
|
-
const { queryOne } = require('../database');
|
|
19
|
+
const { queryOne, ReviewRepository } = require('../database');
|
|
20
20
|
const { normalizeRepository } = require('../utils/paths');
|
|
21
21
|
const logger = require('../utils/logger');
|
|
22
22
|
|
|
@@ -81,7 +81,7 @@ router.post('/api/setup/pr/:owner/:repo/:number', async (req, res) => {
|
|
|
81
81
|
const repository = normalizeRepository(owner, repo);
|
|
82
82
|
const existingPR = await queryOne(
|
|
83
83
|
db,
|
|
84
|
-
'SELECT id FROM pr_metadata WHERE pr_number = ? AND repository = ? COLLATE NOCASE',
|
|
84
|
+
'SELECT id, pr_data FROM pr_metadata WHERE pr_number = ? AND repository = ? COLLATE NOCASE',
|
|
85
85
|
[prNumber, repository]
|
|
86
86
|
);
|
|
87
87
|
if (existingPR) {
|
|
@@ -91,9 +91,44 @@ router.post('/api/setup/pr/:owner/:repo/:number', async (req, res) => {
|
|
|
91
91
|
[prNumber, repository]
|
|
92
92
|
);
|
|
93
93
|
if (worktree) {
|
|
94
|
-
|
|
94
|
+
// If the worktree belongs to the pool, verify it is still actively
|
|
95
|
+
// owned (in_use). Pool slots retain their worktrees row after being
|
|
96
|
+
// released — markAvailable() clears ownership without deleting the
|
|
97
|
+
// record. A released slot may have been reassigned to a different PR,
|
|
98
|
+
// so we must fall through to re-run setup and reacquire a pool slot.
|
|
99
|
+
const poolLifecycle = req.app.get('poolLifecycle');
|
|
100
|
+
const poolEntry = poolLifecycle ? await poolLifecycle.poolRepo.getPoolEntry(worktree.id) : null;
|
|
101
|
+
if (poolEntry && poolEntry.status !== 'in_use') {
|
|
102
|
+
if (poolEntry.status === 'available' && poolEntry.current_pr_number === prNumber) {
|
|
103
|
+
// Still associated with this PR — reclaim without re-setup
|
|
104
|
+
logger.info(`Reclaiming pool worktree ${worktree.id} for ${repository} #${prNumber} (was ${poolEntry.status})`);
|
|
105
|
+
await poolLifecycle.poolRepo.markInUse(poolEntry.id, prNumber);
|
|
106
|
+
const reviewRepo = new ReviewRepository(db);
|
|
107
|
+
const { review } = await reviewRepo.getOrCreate({ prNumber, repository });
|
|
108
|
+
await poolLifecycle.poolRepo.setCurrentReviewId(poolEntry.id, review.id);
|
|
109
|
+
return res.json({ existing: true, reviewUrl: `/pr/${owner}/${repo}/${prNumber}` });
|
|
110
|
+
}
|
|
111
|
+
logger.info(`Pool worktree ${worktree.id} for ${repository} #${prNumber} is ${poolEntry.status}, re-running setup to reacquire`);
|
|
112
|
+
} else {
|
|
113
|
+
return res.json({ existing: true, reviewUrl: `/pr/${owner}/${repo}/${prNumber}` });
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
logger.info(`PR metadata exists but worktree missing for ${repository} #${prNumber}, re-running setup`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// If we have stored PR data with a head_sha, pass it to setupPRReview
|
|
121
|
+
// so it can attempt restore mode (skip GitHub fetch + diff regeneration).
|
|
122
|
+
let restoreMetadata = null;
|
|
123
|
+
if (existingPR && existingPR.pr_data) {
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(existingPR.pr_data);
|
|
126
|
+
if (parsed.head_sha) {
|
|
127
|
+
restoreMetadata = parsed;
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
logger.warn(`Could not parse stored pr_data for ${repository} #${prNumber}`);
|
|
95
131
|
}
|
|
96
|
-
logger.info(`PR metadata exists but worktree missing for ${repository} #${prNumber}, re-running setup`);
|
|
97
132
|
}
|
|
98
133
|
|
|
99
134
|
// Start the async setup
|
|
@@ -108,6 +143,8 @@ router.post('/api/setup/pr/:owner/:repo/:number', async (req, res) => {
|
|
|
108
143
|
prNumber,
|
|
109
144
|
githubToken,
|
|
110
145
|
config,
|
|
146
|
+
poolLifecycle: req.app.get('poolLifecycle'),
|
|
147
|
+
restoreMetadata,
|
|
111
148
|
onProgress: (progress) => {
|
|
112
149
|
sendSetupEvent(setupId, 'step', progress);
|
|
113
150
|
}
|
|
@@ -19,7 +19,7 @@ const { normalizeRepository } = require('../utils/paths');
|
|
|
19
19
|
const { mergeInstructions } = require('../utils/instructions');
|
|
20
20
|
const { GitWorktreeManager } = require('../git/worktree');
|
|
21
21
|
const { GitHubClient } = require('../github/client');
|
|
22
|
-
const { getGitHubToken } = require('../config');
|
|
22
|
+
const { getGitHubToken, resolveLoadSkills, buildCouncilProviderOverrides } = require('../config');
|
|
23
23
|
const { setupStackPR } = require('../setup/stack-setup');
|
|
24
24
|
const Analyzer = require('../ai/analyzer');
|
|
25
25
|
const { getProviderClass, createProvider } = require('../ai/provider');
|
|
@@ -265,7 +265,7 @@ async function executeStackAnalysis(params) {
|
|
|
265
265
|
broadcastStackProgress(stackAnalysisId, state);
|
|
266
266
|
|
|
267
267
|
const prInfo = { owner, repo, number: prNum };
|
|
268
|
-
const perPRWorktreePath = await worktreeManager.createWorktreeForPR(
|
|
268
|
+
const { path: perPRWorktreePath } = await worktreeManager.createWorktreeForPR(
|
|
269
269
|
prInfo, prData, repositoryPath
|
|
270
270
|
);
|
|
271
271
|
worktreePathMap.set(prNum, perPRWorktreePath);
|
|
@@ -374,29 +374,41 @@ async function analyzeStackPR(deps, db, config, {
|
|
|
374
374
|
let analysisResult;
|
|
375
375
|
|
|
376
376
|
if (configType === 'council' || configType === 'advanced' || isCouncil) {
|
|
377
|
+
const { providerOverrides: councilProviderOverrides, providerOverridesMap: councilProviderOverridesMap } =
|
|
378
|
+
buildCouncilProviderOverrides(config, repository, repoSettings);
|
|
379
|
+
|
|
377
380
|
analysisResult = await launchStackCouncilAnalysis(deps, db, config, {
|
|
378
381
|
reviewId, worktreePath, prMetadata, prNum, owner, repo, repository,
|
|
379
382
|
globalInstructions, repoInstructions, requestInstructions,
|
|
380
|
-
councilId, rawCouncilConfig, configType, onAnalysisIdReady
|
|
383
|
+
councilId, rawCouncilConfig, configType, onAnalysisIdReady,
|
|
384
|
+
providerOverrides: councilProviderOverrides,
|
|
385
|
+
providerOverridesMap: councilProviderOverridesMap
|
|
381
386
|
});
|
|
382
387
|
} else {
|
|
383
388
|
let selectedProvider = reqProvider || repoSettings?.default_provider || config.default_provider || config.provider || 'claude';
|
|
384
389
|
let selectedModel = reqModel || repoSettings?.default_model || config.default_model || config.model || 'opus';
|
|
385
390
|
|
|
391
|
+
// Resolve load_skills across all config tiers
|
|
392
|
+
const providerLoadSkills = config.providers?.[selectedProvider]?.load_skills;
|
|
393
|
+
const loadSkills = resolveLoadSkills(config, repository, repoSettings, providerLoadSkills);
|
|
394
|
+
const providerOverrides = { load_skills: loadSkills };
|
|
395
|
+
|
|
386
396
|
const ProviderClass = deps.getProviderClass(selectedProvider);
|
|
387
397
|
|
|
388
398
|
if (ProviderClass?.isExecutable) {
|
|
389
399
|
analysisResult = await launchStackExecutableAnalysis(deps, db, config, {
|
|
390
400
|
reviewId, worktreePath, prMetadata, prNum, owner, repo, repository,
|
|
391
401
|
selectedProvider, selectedModel,
|
|
392
|
-
repoInstructions, requestInstructions, onAnalysisIdReady
|
|
402
|
+
repoInstructions, requestInstructions, onAnalysisIdReady,
|
|
403
|
+
providerOverrides
|
|
393
404
|
});
|
|
394
405
|
} else {
|
|
395
406
|
analysisResult = await launchStackSingleAnalysis(deps, db, config, {
|
|
396
407
|
reviewId, worktreePath, prMetadata, prNum, owner, repo, repository,
|
|
397
408
|
selectedProvider, selectedModel,
|
|
398
409
|
globalInstructions, repoInstructions, requestInstructions,
|
|
399
|
-
reqTier, reqEnabledLevels, onAnalysisIdReady
|
|
410
|
+
reqTier, reqEnabledLevels, onAnalysisIdReady,
|
|
411
|
+
providerOverrides
|
|
400
412
|
});
|
|
401
413
|
}
|
|
402
414
|
}
|
|
@@ -415,7 +427,8 @@ async function launchStackSingleAnalysis(deps, db, config, {
|
|
|
415
427
|
reviewId, worktreePath, prMetadata, prNum, owner, repo, repository,
|
|
416
428
|
selectedProvider, selectedModel,
|
|
417
429
|
globalInstructions, repoInstructions, requestInstructions,
|
|
418
|
-
reqTier, reqEnabledLevels, onAnalysisIdReady
|
|
430
|
+
reqTier, reqEnabledLevels, onAnalysisIdReady,
|
|
431
|
+
providerOverrides = {}
|
|
419
432
|
}) {
|
|
420
433
|
const runId = uuidv4();
|
|
421
434
|
const analysisId = runId;
|
|
@@ -462,7 +475,7 @@ async function launchStackSingleAnalysis(deps, db, config, {
|
|
|
462
475
|
broadcastProgress(analysisId, initialStatus);
|
|
463
476
|
broadcastReviewEvent(reviewId, { type: 'review:analysis_started', analysisId });
|
|
464
477
|
|
|
465
|
-
const analyzer = new deps.Analyzer(db, selectedModel, selectedProvider);
|
|
478
|
+
const analyzer = new deps.Analyzer(db, selectedModel, selectedProvider, providerOverrides);
|
|
466
479
|
const progressCallback = createProgressCallback(analysisId);
|
|
467
480
|
|
|
468
481
|
logger.info(`Stack analysis: starting single-model analysis for PR #${prNum} (${selectedProvider}/${selectedModel})`);
|
|
@@ -538,7 +551,9 @@ async function launchStackSingleAnalysis(deps, db, config, {
|
|
|
538
551
|
async function launchStackCouncilAnalysis(deps, db, config, {
|
|
539
552
|
reviewId, worktreePath, prMetadata, prNum, owner, repo, repository,
|
|
540
553
|
globalInstructions, repoInstructions, requestInstructions,
|
|
541
|
-
councilId, rawCouncilConfig, configType, onAnalysisIdReady
|
|
554
|
+
councilId, rawCouncilConfig, configType, onAnalysisIdReady,
|
|
555
|
+
providerOverrides = {},
|
|
556
|
+
providerOverridesMap = null
|
|
542
557
|
}) {
|
|
543
558
|
let councilConfig;
|
|
544
559
|
let resolvedConfigType = configType;
|
|
@@ -580,6 +595,8 @@ async function launchStackCouncilAnalysis(deps, db, config, {
|
|
|
580
595
|
logLabel: `Stack PR #${prNum}`,
|
|
581
596
|
initialStatusExtra: { prNumber: prNum, reviewType: 'pr' },
|
|
582
597
|
config,
|
|
598
|
+
providerOverrides,
|
|
599
|
+
providerOverridesMap,
|
|
583
600
|
hookContext: {
|
|
584
601
|
mode: 'pr',
|
|
585
602
|
prContext: {
|
|
@@ -623,7 +640,8 @@ async function launchStackCouncilAnalysis(deps, db, config, {
|
|
|
623
640
|
async function launchStackExecutableAnalysis(deps, db, config, {
|
|
624
641
|
reviewId, worktreePath, prMetadata, prNum, owner, repo, repository,
|
|
625
642
|
selectedProvider, selectedModel,
|
|
626
|
-
repoInstructions, requestInstructions, onAnalysisIdReady
|
|
643
|
+
repoInstructions, requestInstructions, onAnalysisIdReady,
|
|
644
|
+
providerOverrides = {}
|
|
627
645
|
}) {
|
|
628
646
|
const runId = uuidv4();
|
|
629
647
|
const analysisId = runId;
|
|
@@ -671,7 +689,8 @@ async function launchStackExecutableAnalysis(deps, db, config, {
|
|
|
671
689
|
repository,
|
|
672
690
|
reviewType: 'pr',
|
|
673
691
|
headSha: prMetadata.head_sha,
|
|
674
|
-
extraInitialStatus: { prNumber: prNum }
|
|
692
|
+
extraInitialStatus: { prNumber: prNum },
|
|
693
|
+
providerOverrides
|
|
675
694
|
}, {
|
|
676
695
|
activeAnalyses,
|
|
677
696
|
reviewToAnalysisId,
|