@in-the-loop-labs/pair-review 2.6.3 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/extensions/task/index.ts +1 -1
- package/.pi/skills/review-roulette/SKILL.md +1 -1
- package/LICENSE +201 -674
- package/README.md +2 -2
- package/bin/pair-review.js +1 -1
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin-code-critic/.claude-plugin/plugin.json +2 -2
- package/plugin-code-critic/skills/analyze/scripts/git-diff-lines +1 -1
- package/public/css/ai-summary-modal.css +1 -1
- package/public/css/pr.css +194 -0
- package/public/index.html +168 -3
- package/public/js/components/AIPanel.js +17 -3
- package/public/js/components/AISummaryModal.js +1 -1
- package/public/js/components/AdvancedConfigTab.js +1 -1
- package/public/js/components/AnalysisConfigModal.js +1 -1
- package/public/js/components/ChatPanel.js +42 -7
- package/public/js/components/ConfirmDialog.js +22 -3
- package/public/js/components/CouncilProgressModal.js +14 -1
- package/public/js/components/DiffOptionsDropdown.js +411 -24
- package/public/js/components/EmojiPicker.js +1 -1
- package/public/js/components/KeyboardShortcuts.js +1 -1
- package/public/js/components/PanelGroup.js +1 -1
- package/public/js/components/PreviewModal.js +1 -1
- package/public/js/components/ReviewModal.js +1 -1
- package/public/js/components/SplitButton.js +1 -1
- package/public/js/components/StatusIndicator.js +1 -1
- package/public/js/components/SuggestionNavigator.js +13 -6
- package/public/js/components/TabTitle.js +96 -0
- package/public/js/components/TextInputDialog.js +1 -1
- package/public/js/components/TimeoutSelect.js +1 -1
- package/public/js/components/Toast.js +7 -1
- package/public/js/components/VoiceCentricConfigTab.js +1 -1
- package/public/js/index.js +649 -44
- package/public/js/local.js +570 -77
- package/public/js/modules/analysis-history.js +4 -3
- package/public/js/modules/comment-manager.js +6 -1
- package/public/js/modules/comment-minimizer.js +304 -0
- package/public/js/modules/diff-context.js +1 -1
- package/public/js/modules/diff-renderer.js +1 -1
- package/public/js/modules/file-comment-manager.js +1 -1
- package/public/js/modules/file-list-merger.js +1 -1
- package/public/js/modules/gap-coordinates.js +1 -1
- package/public/js/modules/hunk-parser.js +1 -1
- package/public/js/modules/line-tracker.js +1 -1
- package/public/js/modules/panel-resizer.js +1 -1
- package/public/js/modules/storage-cleanup.js +1 -1
- package/public/js/modules/suggestion-manager.js +1 -1
- package/public/js/pr.js +83 -7
- package/public/js/repo-settings.js +1 -1
- package/public/js/utils/category-emoji.js +1 -1
- package/public/js/utils/file-order.js +1 -1
- package/public/js/utils/markdown.js +1 -1
- package/public/js/utils/suggestion-ui.js +1 -1
- package/public/js/utils/tier-icons.js +1 -1
- package/public/js/utils/time.js +1 -1
- package/public/js/ws-client.js +1 -1
- package/public/local.html +14 -0
- package/public/pr.html +3 -0
- package/public/setup.html +1 -1
- package/src/ai/analyzer.js +18 -12
- package/src/ai/claude-cli.js +1 -1
- package/src/ai/claude-provider.js +1 -1
- package/src/ai/codex-provider.js +1 -1
- package/src/ai/copilot-provider.js +1 -1
- package/src/ai/cursor-agent-provider.js +1 -1
- package/src/ai/gemini-provider.js +1 -1
- package/src/ai/index.js +1 -1
- package/src/ai/opencode-provider.js +1 -1
- package/src/ai/pi-provider.js +1 -1
- package/src/ai/prompts/baseline/consolidation/balanced.js +1 -1
- package/src/ai/prompts/baseline/consolidation/fast.js +1 -1
- package/src/ai/prompts/baseline/consolidation/thorough.js +1 -1
- package/src/ai/prompts/baseline/level1/balanced.js +1 -1
- package/src/ai/prompts/baseline/level1/fast.js +1 -1
- package/src/ai/prompts/baseline/level1/thorough.js +1 -1
- package/src/ai/prompts/baseline/level2/balanced.js +1 -1
- package/src/ai/prompts/baseline/level2/fast.js +1 -1
- package/src/ai/prompts/baseline/level2/thorough.js +1 -1
- package/src/ai/prompts/baseline/level3/balanced.js +1 -1
- package/src/ai/prompts/baseline/level3/fast.js +1 -1
- package/src/ai/prompts/baseline/level3/thorough.js +1 -1
- package/src/ai/prompts/baseline/orchestration/balanced.js +1 -1
- package/src/ai/prompts/baseline/orchestration/fast.js +1 -1
- package/src/ai/prompts/baseline/orchestration/thorough.js +1 -1
- package/src/ai/prompts/config.js +1 -1
- package/src/ai/prompts/index.js +1 -1
- package/src/ai/prompts/line-number-guidance.js +1 -1
- package/src/ai/prompts/render-for-skill.js +1 -1
- package/src/ai/prompts/shared/diff-instructions.js +1 -1
- package/src/ai/prompts/shared/output-schema.js +1 -1
- package/src/ai/prompts/shared/valid-files.js +1 -1
- package/src/ai/prompts/sparse-checkout-guidance.js +1 -1
- package/src/ai/provider-availability.js +1 -1
- package/src/ai/provider.js +1 -1
- package/src/ai/stream-parser.js +1 -1
- package/src/chat/acp-bridge.js +1 -1
- package/src/chat/api-reference.js +1 -1
- package/src/chat/chat-providers.js +1 -1
- package/src/chat/claude-code-bridge.js +1 -1
- package/src/chat/codex-bridge.js +1 -1
- package/src/chat/pi-bridge.js +1 -1
- package/src/chat/prompt-builder.js +1 -1
- package/src/chat/session-manager.js +1 -1
- package/src/config.js +3 -1
- package/src/database.js +591 -40
- package/src/events/review-events.js +1 -1
- package/src/git/base-branch.js +173 -0
- package/src/git/gitattributes.js +1 -1
- package/src/git/sha-abbrev.js +35 -0
- package/src/git/worktree.js +1 -1
- package/src/github/client.js +33 -2
- package/src/github/parser.js +1 -1
- package/src/hooks/hook-runner.js +100 -0
- package/src/hooks/payloads.js +212 -0
- package/src/local-review.js +469 -130
- package/src/local-scope.js +58 -0
- package/src/main.js +56 -5
- package/src/mcp-stdio.js +1 -1
- package/src/protocol-handler.js +1 -1
- package/src/routes/analyses.js +74 -11
- package/src/routes/chat.js +34 -1
- package/src/routes/config.js +2 -1
- package/src/routes/context-files.js +1 -1
- package/src/routes/councils.js +1 -1
- package/src/routes/github-collections.js +1 -1
- package/src/routes/local.js +735 -69
- package/src/routes/mcp.js +21 -11
- package/src/routes/pr.js +91 -13
- package/src/routes/reviews.js +1 -1
- package/src/routes/setup.js +2 -1
- package/src/routes/shared.js +1 -1
- package/src/routes/worktrees.js +213 -149
- package/src/server.js +31 -1
- package/src/setup/local-setup.js +47 -6
- package/src/setup/pr-setup.js +29 -6
- package/src/utils/auto-context.js +1 -1
- package/src/utils/category-emoji.js +1 -1
- package/src/utils/comment-formatter.js +1 -1
- package/src/utils/diff-annotator.js +1 -1
- package/src/utils/diff-file-list.js +1 -1
- package/src/utils/instructions.js +1 -1
- package/src/utils/json-extractor.js +1 -1
- package/src/utils/line-validation.js +1 -1
- package/src/utils/logger.js +1 -1
- package/src/utils/paths.js +1 -1
- package/src/utils/safe-parse-json.js +1 -1
- package/src/utils/stats-calculator.js +1 -1
- package/src/ws/index.js +1 -1
- package/src/ws/server.js +1 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
const STOPS = ['branch', 'staged', 'unstaged', 'untracked'];
|
|
4
|
+
|
|
5
|
+
const DEFAULT_SCOPE = { start: 'unstaged', end: 'untracked' };
|
|
6
|
+
|
|
7
|
+
function isValidScope(start, end) {
|
|
8
|
+
const si = STOPS.indexOf(start);
|
|
9
|
+
const ei = STOPS.indexOf(end);
|
|
10
|
+
return si !== -1 && ei !== -1 && si <= ei;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function scopeIncludes(start, end, stop) {
|
|
14
|
+
if (!isValidScope(start, end)) return false;
|
|
15
|
+
const si = STOPS.indexOf(start);
|
|
16
|
+
const ei = STOPS.indexOf(end);
|
|
17
|
+
const ti = STOPS.indexOf(stop);
|
|
18
|
+
return ti !== -1 && ti >= si && ti <= ei;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function includesBranch(start) {
|
|
22
|
+
return start === 'branch';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fromLegacyMode(localMode) {
|
|
26
|
+
if (localMode === 'uncommitted') {
|
|
27
|
+
return { start: 'unstaged', end: 'untracked' };
|
|
28
|
+
}
|
|
29
|
+
if (localMode === 'branch') {
|
|
30
|
+
return { start: 'branch', end: 'branch' };
|
|
31
|
+
}
|
|
32
|
+
return { start: DEFAULT_SCOPE.start, end: DEFAULT_SCOPE.end };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function scopeLabel(start, end) {
|
|
36
|
+
if (!isValidScope(start, end)) return '';
|
|
37
|
+
const label = s => s.charAt(0).toUpperCase() + s.slice(1);
|
|
38
|
+
if (start === end) return label(start);
|
|
39
|
+
return `${label(start)}\u2013${label(end)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const LocalScope = {
|
|
43
|
+
STOPS,
|
|
44
|
+
DEFAULT_SCOPE,
|
|
45
|
+
isValidScope,
|
|
46
|
+
scopeIncludes,
|
|
47
|
+
includesBranch,
|
|
48
|
+
fromLegacyMode,
|
|
49
|
+
scopeLabel,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (typeof window !== 'undefined') {
|
|
53
|
+
window.LocalScope = LocalScope;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
57
|
+
module.exports = LocalScope;
|
|
58
|
+
}
|
package/src/main.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// SPDX-License-Identifier:
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const { loadConfig, getConfigDir, getGitHubToken, showWelcomeMessage, resolveDbName, resolveMonorepoOptions } = require('./config');
|
|
4
4
|
const { initializeDatabase, run, queryOne, query, migrateExistingWorktrees, WorktreeRepository, ReviewRepository, RepoSettingsRepository, GitHubReviewRepository } = require('./database');
|
|
@@ -10,6 +10,7 @@ const Analyzer = require('./ai/analyzer');
|
|
|
10
10
|
const { applyConfigOverrides } = require('./ai');
|
|
11
11
|
const { handleLocalReview, findMainGitRoot } = require('./local-review');
|
|
12
12
|
const { storePRData, registerRepositoryLocation, findRepositoryPath } = require('./setup/pr-setup');
|
|
13
|
+
const { fireReviewStartedHook } = require('./hooks/payloads');
|
|
13
14
|
const { normalizeRepository, resolveRenamedFile, resolveRenamedFileOld } = require('./utils/paths');
|
|
14
15
|
const logger = require('./utils/logger');
|
|
15
16
|
const simpleGit = require('simple-git');
|
|
@@ -182,6 +183,41 @@ function cleanupStaleWorktreesAsync(config) {
|
|
|
182
183
|
});
|
|
183
184
|
}
|
|
184
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Asynchronously cleanup stale reviews (runs in background, doesn't block)
|
|
188
|
+
* @param {Object} config - Application configuration
|
|
189
|
+
*/
|
|
190
|
+
function cleanupStaleReviewsAsync(config) {
|
|
191
|
+
setImmediate(async () => {
|
|
192
|
+
try {
|
|
193
|
+
const retentionDays = config.review_retention_days || 21;
|
|
194
|
+
const cutoffDate = new Date();
|
|
195
|
+
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
|
|
196
|
+
const cutoffISO = cutoffDate.toISOString();
|
|
197
|
+
|
|
198
|
+
const reviewRepo = new ReviewRepository(db);
|
|
199
|
+
const staleReviews = await reviewRepo.findStale(cutoffISO);
|
|
200
|
+
|
|
201
|
+
if (staleReviews.length === 0) return;
|
|
202
|
+
|
|
203
|
+
logger.info(`[pair-review] Cleaning up ${staleReviews.length} reviews older than ${retentionDays} days`);
|
|
204
|
+
|
|
205
|
+
for (const review of staleReviews) {
|
|
206
|
+
try {
|
|
207
|
+
await reviewRepo.deleteWithRelatedData(review.id, {
|
|
208
|
+
prNumber: review.pr_number,
|
|
209
|
+
repository: review.repository
|
|
210
|
+
});
|
|
211
|
+
} catch (err) {
|
|
212
|
+
logger.error(`[pair-review] Failed to cleanup review ${review.id}: ${err.message}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
logger.error('[pair-review] Background review cleanup error:', error.message);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
185
221
|
// Known flags that are valid (for validation)
|
|
186
222
|
const KNOWN_FLAGS = new Set([
|
|
187
223
|
'--ai',
|
|
@@ -436,6 +472,11 @@ AI PROVIDERS:
|
|
|
436
472
|
// Resolve localPath, defaulting to cwd if not provided
|
|
437
473
|
const targetPath = flags.localPath || process.cwd();
|
|
438
474
|
await handleLocalReview(targetPath, flags);
|
|
475
|
+
|
|
476
|
+
// Async cleanup of stale worktrees and reviews (don't block startup)
|
|
477
|
+
cleanupStaleWorktreesAsync(config);
|
|
478
|
+
cleanupStaleReviewsAsync(config);
|
|
479
|
+
|
|
439
480
|
return; // Exit after local review
|
|
440
481
|
}
|
|
441
482
|
|
|
@@ -531,8 +572,9 @@ async function handlePullRequest(args, config, db, flags = {}) {
|
|
|
531
572
|
// Start server and open browser to setup page
|
|
532
573
|
const port = await startServer(db);
|
|
533
574
|
|
|
534
|
-
// Async cleanup of stale worktrees (don't block startup)
|
|
575
|
+
// Async cleanup of stale worktrees and reviews (don't block startup)
|
|
535
576
|
cleanupStaleWorktreesAsync(config);
|
|
577
|
+
cleanupStaleReviewsAsync(config);
|
|
536
578
|
|
|
537
579
|
let url = `http://localhost:${port}/pr/${prInfo.owner}/${prInfo.repo}/${prInfo.number}`;
|
|
538
580
|
if (flags.ai) {
|
|
@@ -555,8 +597,9 @@ async function handlePullRequest(args, config, db, flags = {}) {
|
|
|
555
597
|
async function startServerOnly(config) {
|
|
556
598
|
const port = await startServer(db);
|
|
557
599
|
|
|
558
|
-
// Async cleanup of stale worktrees (don't block startup)
|
|
600
|
+
// Async cleanup of stale worktrees and reviews (don't block startup)
|
|
559
601
|
cleanupStaleWorktreesAsync(config);
|
|
602
|
+
cleanupStaleReviewsAsync(config);
|
|
560
603
|
|
|
561
604
|
// Open browser to landing page
|
|
562
605
|
const url = `http://localhost:${port}/`;
|
|
@@ -744,10 +787,18 @@ async function performHeadlessReview(args, config, db, flags, options) {
|
|
|
744
787
|
|
|
745
788
|
// Store PR data in database
|
|
746
789
|
console.log('Storing pull request data...');
|
|
747
|
-
await storePRData(db, prInfo, prData, diff, changedFiles, worktreePath, {
|
|
790
|
+
const { isNewReview, reviewId: storedReviewId } = await storePRData(db, prInfo, prData, diff, changedFiles, worktreePath, {
|
|
748
791
|
skipWorktreeRecord: !!flags.useCheckout
|
|
749
792
|
});
|
|
750
793
|
|
|
794
|
+
// Fire review.started hook for new reviews (non-blocking)
|
|
795
|
+
if (isNewReview) {
|
|
796
|
+
fireReviewStartedHook({
|
|
797
|
+
reviewId: storedReviewId, prNumber: prInfo.number,
|
|
798
|
+
owner: prInfo.owner, repo: prInfo.repo, prData, config,
|
|
799
|
+
}).catch(err => { logger.warn(`Review hook failed: ${err.message}`); });
|
|
800
|
+
}
|
|
801
|
+
|
|
751
802
|
// Get PR metadata ID for AI analysis
|
|
752
803
|
const prMetadata = await queryOne(db, `
|
|
753
804
|
SELECT id, pr_data FROM pr_metadata
|
|
@@ -764,7 +815,7 @@ async function performHeadlessReview(args, config, db, flags, options) {
|
|
|
764
815
|
// The review.id is passed to the analyzer so comments use review.id, not prMetadata.id
|
|
765
816
|
// This avoids ID collision with local mode where comments also use reviews.id
|
|
766
817
|
const reviewRepo = new ReviewRepository(db);
|
|
767
|
-
const review = await reviewRepo.getOrCreate({ prNumber: prInfo.number, repository });
|
|
818
|
+
const { review } = await reviewRepo.getOrCreate({ prNumber: prInfo.number, repository });
|
|
768
819
|
|
|
769
820
|
// Fetch repo settings to get default instructions
|
|
770
821
|
const repoSettingsRepo = new RepoSettingsRepository(db);
|
package/src/mcp-stdio.js
CHANGED
package/src/protocol-handler.js
CHANGED
package/src/routes/analyses.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// SPDX-License-Identifier:
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
/**
|
|
3
3
|
* AI Analysis Routes (shared, ID-based endpoints)
|
|
4
4
|
*
|
|
@@ -21,6 +21,8 @@ const { getTierForModel } = require('../ai/provider');
|
|
|
21
21
|
const { v4: uuidv4 } = require('uuid');
|
|
22
22
|
const logger = require('../utils/logger');
|
|
23
23
|
const { broadcastReviewEvent } = require('../events/review-events');
|
|
24
|
+
const { fireHooks, hasHooks } = require('../hooks/hook-runner');
|
|
25
|
+
const { buildAnalysisStartedPayload, buildAnalysisCompletedPayload, getCachedUser } = require('../hooks/payloads');
|
|
24
26
|
const path = require('path');
|
|
25
27
|
const { normalizeRepository } = require('../utils/paths');
|
|
26
28
|
const {
|
|
@@ -31,7 +33,7 @@ const {
|
|
|
31
33
|
killProcesses,
|
|
32
34
|
createProgressCallback
|
|
33
35
|
} = require('./shared');
|
|
34
|
-
const { generateLocalDiff, computeLocalDiffDigest } = require('../local-review');
|
|
36
|
+
const { generateLocalDiff, computeLocalDiffDigest, getCurrentBranch } = require('../local-review');
|
|
35
37
|
const { validateCouncilConfig, normalizeCouncilConfig } = require('./councils');
|
|
36
38
|
const { TIERS, TIER_ALIASES, VALID_TIERS, resolveTier } = require('../ai/prompts/config');
|
|
37
39
|
|
|
@@ -209,13 +211,21 @@ router.post('/api/analyses/results', async (req, res) => {
|
|
|
209
211
|
// --- Resolve review ---
|
|
210
212
|
let reviewId;
|
|
211
213
|
if (hasLocal) {
|
|
212
|
-
// Local mode:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
214
|
+
// Local mode: find existing session or create a new one
|
|
215
|
+
let localHeadBranch;
|
|
216
|
+
try { localHeadBranch = await getCurrentBranch(localPath); } catch (_) { /* non-fatal */ }
|
|
217
|
+
const existingReview = await reviewRepo.findLocalReview(localPath, headSha, localHeadBranch);
|
|
218
|
+
if (existingReview) {
|
|
219
|
+
reviewId = existingReview.id;
|
|
220
|
+
} else {
|
|
221
|
+
const repository = path.basename(localPath) || 'local';
|
|
222
|
+
reviewId = await reviewRepo.upsertLocalReview({
|
|
223
|
+
localPath,
|
|
224
|
+
localHeadSha: headSha,
|
|
225
|
+
repository,
|
|
226
|
+
localHeadBranch
|
|
227
|
+
});
|
|
228
|
+
}
|
|
219
229
|
|
|
220
230
|
// Generate and store diff so the web UI can display it
|
|
221
231
|
try {
|
|
@@ -235,7 +245,7 @@ router.post('/api/analyses/results', async (req, res) => {
|
|
|
235
245
|
return res.status(400).json({ error: 'Invalid pull request number' });
|
|
236
246
|
}
|
|
237
247
|
const repository = normalizeRepository(repoParts[0], repoParts[1]);
|
|
238
|
-
const review = await reviewRepo.getOrCreate({
|
|
248
|
+
const { review } = await reviewRepo.getOrCreate({
|
|
239
249
|
prNumber: parsedPR,
|
|
240
250
|
repository
|
|
241
251
|
});
|
|
@@ -395,6 +405,10 @@ router.post('/api/analyses/:id/cancel', async (req, res) => {
|
|
|
395
405
|
// Broadcast cancelled status to WebSocket clients
|
|
396
406
|
broadcastProgress(id, cancelledStatus);
|
|
397
407
|
|
|
408
|
+
// Hook firing removed — the .catch(isCancellation) handlers in
|
|
409
|
+
// pr.js, local.js, and launchCouncilAnalysis already fire
|
|
410
|
+
// analysis.completed with full context when the process exits.
|
|
411
|
+
|
|
398
412
|
// Clean up review to analysis ID mapping
|
|
399
413
|
if (analysis.reviewId) {
|
|
400
414
|
reviewToAnalysisId.delete(analysis.reviewId);
|
|
@@ -453,7 +467,9 @@ async function launchCouncilAnalysis(db, modeContext, councilConfig, councilId,
|
|
|
453
467
|
logLabel,
|
|
454
468
|
initialStatusExtra,
|
|
455
469
|
onSuccess,
|
|
456
|
-
runUpdateExtra
|
|
470
|
+
runUpdateExtra,
|
|
471
|
+
config: modeConfig,
|
|
472
|
+
hookContext = {},
|
|
457
473
|
} = modeContext;
|
|
458
474
|
|
|
459
475
|
const { repoInstructions, requestInstructions } = instructions;
|
|
@@ -521,6 +537,17 @@ async function launchCouncilAnalysis(db, modeContext, councilConfig, councilId,
|
|
|
521
537
|
|
|
522
538
|
broadcastProgress(analysisId, initialStatus);
|
|
523
539
|
broadcastReviewEvent(reviewId, { type: 'review:analysis_started', analysisId });
|
|
540
|
+
const effectiveConfig = modeConfig || {};
|
|
541
|
+
if (hasHooks('analysis.started', effectiveConfig)) {
|
|
542
|
+
getCachedUser(effectiveConfig).then(user => {
|
|
543
|
+
fireHooks('analysis.started', buildAnalysisStartedPayload({
|
|
544
|
+
reviewId, analysisId, provider: 'council', model: councilId || 'inline-config',
|
|
545
|
+
mode: initialStatusExtra?.reviewType || 'pr',
|
|
546
|
+
prContext: hookContext.prContext, localContext: hookContext.localContext,
|
|
547
|
+
user,
|
|
548
|
+
}), effectiveConfig);
|
|
549
|
+
}).catch(err => { logger.warn(`Analysis hook failed: ${err.message}`); });
|
|
550
|
+
}
|
|
524
551
|
|
|
525
552
|
const analyzer = new Analyzer(db, 'council', 'council');
|
|
526
553
|
|
|
@@ -588,10 +615,34 @@ async function launchCouncilAnalysis(db, modeContext, councilConfig, councilId,
|
|
|
588
615
|
activeAnalyses.set(analysisId, completedStatus);
|
|
589
616
|
broadcastProgress(analysisId, completedStatus);
|
|
590
617
|
broadcastReviewEvent(initialStatus.reviewId, { type: 'review:analysis_completed' });
|
|
618
|
+
|
|
619
|
+
// Fire analysis.completed hook
|
|
620
|
+
if (hasHooks('analysis.completed', effectiveConfig)) {
|
|
621
|
+
getCachedUser(effectiveConfig).then(user => {
|
|
622
|
+
fireHooks('analysis.completed', buildAnalysisCompletedPayload({
|
|
623
|
+
reviewId: initialStatus.reviewId, analysisId, provider: 'council',
|
|
624
|
+
model: councilId || 'inline-config',
|
|
625
|
+
status: 'success', totalSuggestions: result.suggestions.length,
|
|
626
|
+
mode: initialStatusExtra?.reviewType || 'pr',
|
|
627
|
+
prContext: hookContext.prContext, localContext: hookContext.localContext, user,
|
|
628
|
+
}), effectiveConfig);
|
|
629
|
+
}).catch(() => {});
|
|
630
|
+
}
|
|
591
631
|
})
|
|
592
632
|
.catch(error => {
|
|
593
633
|
if (error.isCancellation) {
|
|
594
634
|
logger.info(`Council analysis cancelled for ${logLabel}`);
|
|
635
|
+
if (hasHooks('analysis.completed', effectiveConfig)) {
|
|
636
|
+
getCachedUser(effectiveConfig).then(user => {
|
|
637
|
+
fireHooks('analysis.completed', buildAnalysisCompletedPayload({
|
|
638
|
+
reviewId, analysisId, provider: 'council',
|
|
639
|
+
model: councilId || 'inline-config',
|
|
640
|
+
status: 'cancelled', totalSuggestions: 0,
|
|
641
|
+
mode: initialStatusExtra?.reviewType || 'pr',
|
|
642
|
+
prContext: hookContext.prContext, localContext: hookContext.localContext, user,
|
|
643
|
+
}), effectiveConfig);
|
|
644
|
+
}).catch(() => {});
|
|
645
|
+
}
|
|
595
646
|
return;
|
|
596
647
|
}
|
|
597
648
|
logger.error(`Council analysis failed for ${logLabel}: ${error.message}`);
|
|
@@ -607,6 +658,18 @@ async function launchCouncilAnalysis(db, modeContext, councilConfig, councilId,
|
|
|
607
658
|
broadcastProgress(analysisId, failedStatus);
|
|
608
659
|
|
|
609
660
|
analysisRunRepo.update(runId, { status: 'failed' }).catch(() => {});
|
|
661
|
+
|
|
662
|
+
if (hasHooks('analysis.completed', effectiveConfig)) {
|
|
663
|
+
getCachedUser(effectiveConfig).then(user => {
|
|
664
|
+
fireHooks('analysis.completed', buildAnalysisCompletedPayload({
|
|
665
|
+
reviewId, analysisId, provider: 'council',
|
|
666
|
+
model: councilId || 'inline-config',
|
|
667
|
+
status: 'failed', totalSuggestions: 0,
|
|
668
|
+
mode: initialStatusExtra?.reviewType || 'pr',
|
|
669
|
+
prContext: hookContext.prContext, localContext: hookContext.localContext, user,
|
|
670
|
+
}), effectiveConfig);
|
|
671
|
+
}).catch(() => {});
|
|
672
|
+
}
|
|
610
673
|
})
|
|
611
674
|
.finally(() => {
|
|
612
675
|
// Clean up unified tracking map entry
|
package/src/routes/chat.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// SPDX-License-Identifier:
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
/**
|
|
3
3
|
* Chat Routes
|
|
4
4
|
*
|
|
@@ -18,6 +18,32 @@ const { renderApiDocs, buildApiCheatSheet } = require('../chat/api-reference');
|
|
|
18
18
|
const { GitWorktreeManager } = require('../git/worktree');
|
|
19
19
|
const logger = require('../utils/logger');
|
|
20
20
|
const ws = require('../ws');
|
|
21
|
+
const { fireHooks, hasHooks } = require('../hooks/hook-runner');
|
|
22
|
+
const { buildChatStartedPayload, buildChatResumedPayload, buildChatHookContext, getCachedUser } = require('../hooks/payloads');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Fire a chat hook event (non-blocking). Skips async work when no hooks are configured.
|
|
26
|
+
* @param {string} event - 'chat.started' or 'chat.resumed'
|
|
27
|
+
* @param {Object} opts
|
|
28
|
+
* @param {Object} opts.req - Express request (used to read config)
|
|
29
|
+
* @param {Object} opts.review - Review record
|
|
30
|
+
* @param {number} opts.sessionId - Chat session ID
|
|
31
|
+
* @param {string} opts.provider - AI provider
|
|
32
|
+
* @param {string} opts.model - AI model
|
|
33
|
+
*/
|
|
34
|
+
function fireChatHook(event, { req, review, sessionId, provider, model }) {
|
|
35
|
+
const config = req.app.get('config') || {};
|
|
36
|
+
if (!hasHooks(event, config)) return;
|
|
37
|
+
|
|
38
|
+
const buildPayload = event === 'chat.started' ? buildChatStartedPayload : buildChatResumedPayload;
|
|
39
|
+
getCachedUser(config).then(user => {
|
|
40
|
+
const payload = buildPayload({
|
|
41
|
+
reviewId: review.id, sessionId, provider, model,
|
|
42
|
+
...buildChatHookContext(review), user,
|
|
43
|
+
});
|
|
44
|
+
fireHooks(event, payload, config);
|
|
45
|
+
}).catch(err => { logger.warn(`Chat hook failed: ${err.message}`); });
|
|
46
|
+
}
|
|
21
47
|
|
|
22
48
|
const router = express.Router();
|
|
23
49
|
|
|
@@ -302,6 +328,8 @@ router.post('/api/chat/session', async (req, res) => {
|
|
|
302
328
|
// Register broadcast listeners so events reach all connected clients
|
|
303
329
|
registerChatBroadcast(chatSessionManager, session.id, serverPort);
|
|
304
330
|
|
|
331
|
+
fireChatHook('chat.started', { req, review, sessionId: session.id, provider, model });
|
|
332
|
+
|
|
305
333
|
const responseData = { id: session.id, status: session.status };
|
|
306
334
|
|
|
307
335
|
// Include analysis context metadata so the frontend can show a context indicator
|
|
@@ -372,6 +400,8 @@ router.post('/api/chat/session/:id/message', async (req, res) => {
|
|
|
372
400
|
registerChatBroadcast(chatSessionManager, sessionId, req.socket.localPort);
|
|
373
401
|
logger.info(`[ChatRoute] Auto-resumed session ${sessionId} for message delivery`);
|
|
374
402
|
|
|
403
|
+
fireChatHook('chat.resumed', { req, review, sessionId, provider: session.provider, model: session.model });
|
|
404
|
+
|
|
375
405
|
// Inject port correction so the agent knows the current server address,
|
|
376
406
|
// even if the conversational history has a stale port from session creation.
|
|
377
407
|
const serverPort = req.socket.localPort;
|
|
@@ -518,6 +548,9 @@ router.post('/api/chat/session/:id/resume', async (req, res) => {
|
|
|
518
548
|
);
|
|
519
549
|
|
|
520
550
|
logger.info(`[ChatRoute] Explicitly resumed session ${sessionId}`);
|
|
551
|
+
|
|
552
|
+
fireChatHook('chat.resumed', { req, review, sessionId, provider: session.provider, model: session.model });
|
|
553
|
+
|
|
521
554
|
res.json({ data: { id: sessionId, status: 'active' } });
|
|
522
555
|
} catch (error) {
|
|
523
556
|
logger.error(`Error resuming chat session: ${error.message}`);
|
package/src/routes/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// SPDX-License-Identifier:
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
/**
|
|
3
3
|
* Configuration Routes
|
|
4
4
|
*
|
|
@@ -56,6 +56,7 @@ router.get('/api/config', (req, res) => {
|
|
|
56
56
|
pi_available: getCachedAvailability('pi')?.available || false,
|
|
57
57
|
assisted_by_url: config.assisted_by_url || 'https://github.com/in-the-loop-labs/pair-review',
|
|
58
58
|
enable_graphite: config.enable_graphite === true,
|
|
59
|
+
chat_spinner: config.chat_spinner || 'dots',
|
|
59
60
|
// Share configuration for external review viewers.
|
|
60
61
|
// - url: The base URL of the external share site
|
|
61
62
|
// - method: Plumbed through for future use (e.g., POST-based share flows).
|
package/src/routes/councils.js
CHANGED