@in-the-loop-labs/pair-review 3.0.5 → 3.1.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.
Files changed (79) hide show
  1. package/package.json +2 -1
  2. package/plugin/.claude-plugin/plugin.json +1 -1
  3. package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
  4. package/plugin-code-critic/skills/analyze/references/level1-balanced.md +8 -0
  5. package/plugin-code-critic/skills/analyze/references/level1-fast.md +7 -0
  6. package/plugin-code-critic/skills/analyze/references/level1-thorough.md +8 -0
  7. package/plugin-code-critic/skills/analyze/references/level2-balanced.md +9 -0
  8. package/plugin-code-critic/skills/analyze/references/level2-fast.md +8 -0
  9. package/plugin-code-critic/skills/analyze/references/level2-thorough.md +9 -0
  10. package/plugin-code-critic/skills/analyze/references/level3-balanced.md +9 -0
  11. package/plugin-code-critic/skills/analyze/references/level3-fast.md +8 -0
  12. package/plugin-code-critic/skills/analyze/references/level3-thorough.md +9 -0
  13. package/plugin-code-critic/skills/analyze/references/orchestration-balanced.md +9 -0
  14. package/plugin-code-critic/skills/analyze/references/orchestration-fast.md +5 -0
  15. package/plugin-code-critic/skills/analyze/references/orchestration-thorough.md +9 -0
  16. package/public/css/analysis-config.css +83 -0
  17. package/public/css/pr.css +191 -4
  18. package/public/index.html +20 -0
  19. package/public/js/components/AIPanel.js +1 -1
  20. package/public/js/components/AdvancedConfigTab.js +83 -8
  21. package/public/js/components/AnalysisConfigModal.js +155 -5
  22. package/public/js/components/ChatPanel.js +22 -5
  23. package/public/js/components/CouncilProgressModal.js +239 -22
  24. package/public/js/components/TimeoutSelect.js +2 -0
  25. package/public/js/components/VoiceCentricConfigTab.js +179 -12
  26. package/public/js/index.js +119 -1
  27. package/public/js/local.js +141 -47
  28. package/public/js/modules/suggestion-manager.js +2 -1
  29. package/public/js/pr.js +71 -12
  30. package/public/js/repo-settings.js +2 -2
  31. package/public/local.html +32 -11
  32. package/public/pr.html +2 -0
  33. package/src/ai/analyzer.js +371 -111
  34. package/src/ai/claude-provider.js +2 -0
  35. package/src/ai/codex-provider.js +1 -1
  36. package/src/ai/copilot-provider.js +2 -0
  37. package/src/ai/executable-provider.js +534 -0
  38. package/src/ai/gemini-provider.js +2 -0
  39. package/src/ai/index.js +9 -1
  40. package/src/ai/pi-provider.js +10 -8
  41. package/src/ai/prompts/baseline/consolidation/balanced.js +54 -2
  42. package/src/ai/prompts/baseline/consolidation/fast.js +31 -1
  43. package/src/ai/prompts/baseline/consolidation/thorough.js +46 -3
  44. package/src/ai/prompts/baseline/level1/balanced.js +12 -0
  45. package/src/ai/prompts/baseline/level1/fast.js +11 -0
  46. package/src/ai/prompts/baseline/level1/thorough.js +12 -0
  47. package/src/ai/prompts/baseline/level2/balanced.js +13 -0
  48. package/src/ai/prompts/baseline/level2/fast.js +12 -0
  49. package/src/ai/prompts/baseline/level2/thorough.js +13 -0
  50. package/src/ai/prompts/baseline/level3/balanced.js +13 -0
  51. package/src/ai/prompts/baseline/level3/fast.js +12 -0
  52. package/src/ai/prompts/baseline/level3/thorough.js +13 -0
  53. package/src/ai/prompts/baseline/orchestration/balanced.js +15 -0
  54. package/src/ai/prompts/baseline/orchestration/fast.js +11 -0
  55. package/src/ai/prompts/baseline/orchestration/thorough.js +15 -0
  56. package/src/ai/prompts/render-for-skill.js +3 -0
  57. package/src/ai/prompts/shared/output-schema.js +8 -0
  58. package/src/ai/provider.js +89 -4
  59. package/src/chat/prompt-builder.js +17 -1
  60. package/src/chat/session-manager.js +32 -28
  61. package/src/config.js +15 -2
  62. package/src/database.js +59 -15
  63. package/src/git/base-branch.js +133 -52
  64. package/src/local-review.js +15 -9
  65. package/src/main.js +3 -2
  66. package/src/routes/analyses.js +34 -8
  67. package/src/routes/chat.js +15 -8
  68. package/src/routes/config.js +3 -120
  69. package/src/routes/councils.js +15 -6
  70. package/src/routes/executable-analysis.js +494 -0
  71. package/src/routes/local.js +160 -26
  72. package/src/routes/mcp.js +9 -4
  73. package/src/routes/pr.js +166 -29
  74. package/src/routes/reviews.js +31 -5
  75. package/src/routes/shared.js +72 -5
  76. package/src/routes/worktrees.js +4 -2
  77. package/src/utils/comment-formatter.js +28 -11
  78. package/src/utils/instructions.js +22 -8
  79. package/src/utils/logger.js +20 -10
@@ -300,6 +300,25 @@ router.post('/api/analyses/results', async (req, res) => {
300
300
  }
301
301
  });
302
302
 
303
+ /**
304
+ * List currently active (running) analyses across all reviews.
305
+ * Returns lightweight projections from the in-memory activeAnalyses map.
306
+ */
307
+ router.get('/api/analyses/active', (req, res) => {
308
+ const active = [];
309
+ for (const status of activeAnalyses.values()) {
310
+ if (status.status !== 'running') continue;
311
+ active.push({
312
+ analysisId: status.id,
313
+ reviewId: status.reviewId,
314
+ reviewType: status.reviewType || null,
315
+ repository: status.repository || null,
316
+ prNumber: status.prNumber || null
317
+ });
318
+ }
319
+ res.json({ active });
320
+ });
321
+
303
322
  // ==========================================================================
304
323
  // Parameterised :id routes — registered AFTER static paths
305
324
  // ==========================================================================
@@ -396,7 +415,10 @@ router.post('/api/analyses/:id/cancel', async (req, res) => {
396
415
  : analysis.levels?.[3],
397
416
  4: analysis.levels?.[4]?.status === 'running'
398
417
  ? { status: 'cancelled', progress: 'Cancelled' }
399
- : analysis.levels?.[4]
418
+ : analysis.levels?.[4],
419
+ exec: analysis.levels?.exec?.status === 'running'
420
+ ? { status: 'cancelled', progress: 'Cancelled' }
421
+ : analysis.levels?.exec
400
422
  }
401
423
  };
402
424
 
@@ -446,7 +468,7 @@ router.post('/api/analyses/:id/cancel', async (req, res) => {
446
468
  * @param {Object} modeContext - Mode-specific values
447
469
  * @param {Object} councilConfig - Validated council configuration
448
470
  * @param {string} councilId - Council ID (for the model field in analysis_runs), or null for inline config
449
- * @param {Object} instructions - { repoInstructions, requestInstructions }
471
+ * @param {Object} instructions - { globalInstructions, repoInstructions, requestInstructions }
450
472
  * @param {string} [configType='advanced'] - Config type
451
473
  * @returns {{ analysisId: string, runId: string }}
452
474
  */
@@ -469,10 +491,12 @@ async function launchCouncilAnalysis(db, modeContext, councilConfig, councilId,
469
491
  onSuccess,
470
492
  runUpdateExtra,
471
493
  config: modeConfig,
494
+ excludePrevious,
495
+ serverPort,
472
496
  hookContext = {},
473
497
  } = modeContext;
474
498
 
475
- const { repoInstructions, requestInstructions } = instructions;
499
+ const { globalInstructions, repoInstructions, requestInstructions } = instructions;
476
500
 
477
501
  const isVoiceCentric = configType === 'council';
478
502
 
@@ -496,6 +520,7 @@ async function launchCouncilAnalysis(db, modeContext, councilConfig, councilId,
496
520
  provider: 'council',
497
521
  model: councilId || 'inline-config',
498
522
  tier: null,
523
+ globalInstructions,
499
524
  repoInstructions,
500
525
  requestInstructions,
501
526
  headSha: headSha || null,
@@ -521,7 +546,8 @@ async function launchCouncilAnalysis(db, modeContext, councilConfig, councilId,
521
546
  1: isLevelEnabled(councilConfig, '1') ? { status: 'running', progress: 'Starting...' } : { status: 'skipped', progress: 'Skipped' },
522
547
  2: isLevelEnabled(councilConfig, '2') ? { status: 'running', progress: 'Starting...' } : { status: 'skipped', progress: 'Skipped' },
523
548
  3: isLevelEnabled(councilConfig, '3') ? { status: 'running', progress: 'Starting...' } : { status: 'skipped', progress: 'Skipped' },
524
- 4: { status: 'pending', progress: 'Pending' }
549
+ 4: { status: 'pending', progress: 'Pending' },
550
+ exec: { status: 'pending', progress: 'Pending' }
525
551
  },
526
552
  isCouncil: true,
527
553
  councilConfig,
@@ -563,12 +589,12 @@ async function launchCouncilAnalysis(db, modeContext, councilConfig, councilId,
563
589
  worktreePath,
564
590
  prMetadata,
565
591
  changedFiles,
566
- instructions: { repoInstructions, requestInstructions }
592
+ instructions: { globalInstructions, repoInstructions, requestInstructions }
567
593
  };
568
594
 
569
595
  const analysisPromise = isVoiceCentric
570
- ? analyzer.runReviewerCentricCouncil(reviewContext, councilConfig, { analysisId, runId, progressCallback })
571
- : analyzer.runCouncilAnalysis(reviewContext, councilConfig, { analysisId, runId, progressCallback });
596
+ ? analyzer.runReviewerCentricCouncil(reviewContext, councilConfig, { analysisId, runId, progressCallback, excludePrevious, serverPort })
597
+ : analyzer.runCouncilAnalysis(reviewContext, councilConfig, { analysisId, runId, progressCallback, excludePrevious, serverPort });
572
598
 
573
599
  analysisPromise
574
600
  .then(async result => {
@@ -607,7 +633,7 @@ async function launchCouncilAnalysis(db, modeContext, councilConfig, councilId,
607
633
  4: { status: 'completed', progress: 'Results finalized' }
608
634
  }
609
635
  };
610
- for (const levelKey of ['1', '2', '3']) {
636
+ for (const levelKey of ['1', '2', '3', 'exec']) {
611
637
  if (currentStatus.levels?.[levelKey]?.status === 'running') {
612
638
  completedStatus.levels[levelKey] = { status: 'completed', progress: 'Complete' };
613
639
  }
@@ -20,6 +20,7 @@ const logger = require('../utils/logger');
20
20
  const ws = require('../ws');
21
21
  const { fireHooks, hasHooks } = require('../hooks/hook-runner');
22
22
  const { buildChatStartedPayload, buildChatResumedPayload, buildChatHookContext, getCachedUser } = require('../hooks/payloads');
23
+ const { resolveFormat } = require('../utils/comment-formatter');
23
24
 
24
25
  /**
25
26
  * Fire a chat hook event (non-blocking). Skips async work when no hooks are configured.
@@ -259,8 +260,10 @@ router.post('/api/chat/session', async (req, res) => {
259
260
 
260
261
  const chatInstructions = await getChatInstructions(db, review);
261
262
  const prData = await fetchPrData(db, review);
263
+ const config = req.app.get('config') || {};
264
+ const formatConfig = resolveFormat(config.comment_format);
262
265
 
263
- finalSystemPrompt = buildChatPrompt({ review, prData, chatInstructions });
266
+ finalSystemPrompt = buildChatPrompt({ review, prData, chatInstructions, commentFormatTemplate: formatConfig.template });
264
267
 
265
268
  if (!skipAnalysisContext) {
266
269
  // Fetch all AI suggestions from the latest analysis run
@@ -268,7 +271,7 @@ router.post('/api/chat/session', async (req, res) => {
268
271
  SELECT
269
272
  id, ai_run_id, ai_level, ai_confidence,
270
273
  file, line_start, line_end, type, title, body,
271
- reasoning, status, is_file_level
274
+ reasoning, status, is_file_level, severity
272
275
  FROM comments
273
276
  WHERE review_id = ?
274
277
  AND source = 'ai'
@@ -323,7 +326,7 @@ router.post('/api/chat/session', async (req, res) => {
323
326
  initialContext: initialContextWithPort
324
327
  });
325
328
 
326
- logger.info(`Chat session created: ${session.id} (provider=${provider}, model=${model})`);
329
+ logger.info(`Chat session created: ${session.id} (provider=${provider})`);
327
330
 
328
331
  // Register broadcast listeners so events reach all connected clients
329
332
  registerChatBroadcast(chatSessionManager, session.id, serverPort);
@@ -390,8 +393,10 @@ router.post('/api/chat/session/:id/message', async (req, res) => {
390
393
  }
391
394
  const chatInstructions = await getChatInstructions(db, review);
392
395
  const prData = await fetchPrData(db, review);
396
+ const config = req.app.get('config') || {};
397
+ const fmtConfig = resolveFormat(config.comment_format);
393
398
 
394
- const systemPrompt = buildChatPrompt({ review, prData, chatInstructions });
399
+ const systemPrompt = buildChatPrompt({ review, prData, chatInstructions, commentFormatTemplate: fmtConfig.template });
395
400
  const cwd = await resolveReviewCwd(db, review);
396
401
 
397
402
  try {
@@ -405,7 +410,7 @@ router.post('/api/chat/session/:id/message', async (req, res) => {
405
410
  // Inject port correction so the agent knows the current server address,
406
411
  // even if the conversational history has a stale port from session creation.
407
412
  const serverPort = req.socket.localPort;
408
- portCorrectionContext = `[Server port: ${serverPort}] The pair-review API is at http://localhost:${serverPort}`;
413
+ portCorrectionContext = `[Server port: ${serverPort}] The pair-review API is at http://localhost:${serverPort}\n\n[Comment format template: ${fmtConfig.template}]`;
409
414
  // Note: we intentionally do NOT re-inject the API cheat sheet on resume.
410
415
  // The agent already has the endpoint shapes from the original session context —
411
416
  // it only needs the updated port to adjust its curl calls.
@@ -530,8 +535,10 @@ router.post('/api/chat/session/:id/resume', async (req, res) => {
530
535
  // initial session's context.
531
536
  const chatInstructions = await getChatInstructions(db, review);
532
537
  const prData = await fetchPrData(db, review);
538
+ const config = req.app.get('config') || {};
539
+ const fmtConfig = resolveFormat(config.comment_format);
533
540
 
534
- const systemPrompt = buildChatPrompt({ review, prData, chatInstructions });
541
+ const systemPrompt = buildChatPrompt({ review, prData, chatInstructions, commentFormatTemplate: fmtConfig.template });
535
542
  const cwd = await resolveReviewCwd(db, review);
536
543
 
537
544
  await chatSessionManager.resumeSession(sessionId, { systemPrompt, cwd });
@@ -544,7 +551,7 @@ router.post('/api/chat/session/:id/resume', async (req, res) => {
544
551
  // Uses resumeContext (consumed on next sendMessage) instead of saveContextMessage
545
552
  // (which only writes to DB and never reaches the agent process).
546
553
  chatSessionManager.setResumeContext(sessionId,
547
- `[Server port: ${serverPort}] The pair-review API is at http://localhost:${serverPort}`
554
+ `[Server port: ${serverPort}] The pair-review API is at http://localhost:${serverPort}\n\n[Comment format template: ${fmtConfig.template}]`
548
555
  );
549
556
 
550
557
  logger.info(`[ChatRoute] Explicitly resumed session ${sessionId}`);
@@ -622,7 +629,7 @@ router.get('/api/chat/analysis-context/:runId', async (req, res) => {
622
629
  SELECT
623
630
  id, ai_run_id, ai_level, ai_confidence,
624
631
  file, line_start, line_end, type, title, body,
625
- reasoning, status, is_file_level
632
+ reasoning, status, is_file_level, severity
626
633
  FROM comments
627
634
  WHERE review_id = ?
628
635
  AND source = 'ai'
@@ -20,10 +20,9 @@ const {
20
20
  isCheckInProgress
21
21
  } = require('../ai');
22
22
  const { normalizeRepository } = require('../utils/paths');
23
- const { isRunningViaNpx, saveConfig } = require('../config');
23
+ const { isRunningViaNpx, getGitHubToken } = require('../config');
24
24
  const { version } = require('../../package.json');
25
25
  const { getAllChatProviders, getAllCachedChatAvailability } = require('../chat/chat-providers');
26
- const { PRESETS } = require('../utils/comment-formatter');
27
26
  const logger = require('../utils/logger');
28
27
 
29
28
  const router = express.Router();
@@ -45,6 +44,7 @@ router.get('/api/config', (req, res) => {
45
44
  res.json({
46
45
  version,
47
46
  theme: config.theme || 'light',
47
+ has_github_token: Boolean(getGitHubToken(config)),
48
48
  comment_button_action: config.comment_button_action || 'submit',
49
49
  comment_format: config.comment_format || 'legacy',
50
50
  // Include npx detection for frontend command examples
@@ -53,6 +53,7 @@ router.get('/api/config', (req, res) => {
53
53
  chat_provider: config.chat_provider || 'pi',
54
54
  chat_providers: chatProviders,
55
55
  chat_enable_shortcuts: config.chat?.enable_shortcuts !== false,
56
+ chat_enter_to_send: config.chat?.enter_to_send !== false,
56
57
  pi_available: getCachedAvailability('pi')?.available || false,
57
58
  assisted_by_url: config.assisted_by_url || 'https://github.com/in-the-loop-labs/pair-review',
58
59
  enable_graphite: config.enable_graphite === true,
@@ -74,124 +75,6 @@ router.get('/api/config', (req, res) => {
74
75
  });
75
76
  });
76
77
 
77
- /**
78
- * Update user configuration
79
- * Updates safe configuration values
80
- */
81
- router.patch('/api/config', async (req, res) => {
82
- try {
83
- const { comment_button_action, chat_enable_shortcuts, comment_format } = req.body;
84
-
85
- // Validate comment_button_action if provided
86
- if (comment_button_action !== undefined) {
87
- if (!['submit', 'preview'].includes(comment_button_action)) {
88
- return res.status(400).json({
89
- error: 'Invalid comment_button_action. Must be "submit" or "preview"'
90
- });
91
- }
92
- }
93
-
94
- if (chat_enable_shortcuts !== undefined) {
95
- if (typeof chat_enable_shortcuts !== 'boolean') {
96
- return res.status(400).json({
97
- error: 'Invalid chat_enable_shortcuts. Must be a boolean'
98
- });
99
- }
100
- }
101
-
102
- // Validate comment_format if provided
103
- if (comment_format !== undefined) {
104
- const validPresets = Object.keys(PRESETS);
105
- if (typeof comment_format === 'string') {
106
- if (!validPresets.includes(comment_format)) {
107
- return res.status(400).json({
108
- error: `Invalid comment_format preset. Must be one of: ${validPresets.join(', ')}`
109
- });
110
- }
111
- } else if (typeof comment_format === 'object' && comment_format !== null) {
112
- if (!comment_format.template || typeof comment_format.template !== 'string') {
113
- return res.status(400).json({
114
- error: 'Custom comment_format must have a "template" string'
115
- });
116
- }
117
- // Validate categoryOverrides if provided (must be a string->string mapping)
118
- if (comment_format.categoryOverrides !== undefined) {
119
- if (typeof comment_format.categoryOverrides !== 'object' || comment_format.categoryOverrides === null || Array.isArray(comment_format.categoryOverrides)) {
120
- return res.status(400).json({
121
- error: 'categoryOverrides must be an object mapping category names to replacement strings'
122
- });
123
- }
124
- for (const [, value] of Object.entries(comment_format.categoryOverrides)) {
125
- if (typeof value !== 'string') {
126
- return res.status(400).json({
127
- error: 'categoryOverrides must be a string-to-string mapping'
128
- });
129
- }
130
- }
131
- }
132
- // Validate emojiOverrides if provided (must be a string->string mapping)
133
- if (comment_format.emojiOverrides !== undefined) {
134
- if (typeof comment_format.emojiOverrides !== 'object' || comment_format.emojiOverrides === null || Array.isArray(comment_format.emojiOverrides)) {
135
- return res.status(400).json({
136
- error: 'emojiOverrides must be an object mapping category names to emoji strings'
137
- });
138
- }
139
- for (const [, value] of Object.entries(comment_format.emojiOverrides)) {
140
- if (typeof value !== 'string') {
141
- return res.status(400).json({
142
- error: 'emojiOverrides must be a string-to-string mapping'
143
- });
144
- }
145
- }
146
- }
147
- } else {
148
- return res.status(400).json({
149
- error: 'comment_format must be a preset name string or a custom template object'
150
- });
151
- }
152
- }
153
-
154
- // Get current config
155
- const config = req.app.get('config') || {};
156
-
157
- // Update allowed fields
158
- if (comment_button_action !== undefined) {
159
- config.comment_button_action = comment_button_action;
160
- }
161
-
162
- if (chat_enable_shortcuts !== undefined) {
163
- if (!config.chat) config.chat = {};
164
- config.chat.enable_shortcuts = chat_enable_shortcuts;
165
- }
166
-
167
- if (comment_format !== undefined) {
168
- config.comment_format = comment_format;
169
- }
170
-
171
- // Save config to file
172
- await saveConfig(config);
173
-
174
- // Update app config
175
- req.app.set('config', config);
176
-
177
- res.json({
178
- success: true,
179
- config: {
180
- theme: config.theme || 'light',
181
- comment_button_action: config.comment_button_action || 'submit',
182
- chat_enable_shortcuts: config.chat?.enable_shortcuts !== false,
183
- comment_format: config.comment_format || 'legacy'
184
- }
185
- });
186
-
187
- } catch (error) {
188
- logger.error('Error updating config:', error);
189
- res.status(500).json({
190
- error: 'Failed to update configuration'
191
- });
192
- }
193
- });
194
-
195
78
  /**
196
79
  * Get repository-specific settings
197
80
  * Returns default_instructions, default_provider, and default_model for the repository
@@ -11,6 +11,7 @@ const express = require('express');
11
11
  const { v4: uuidv4 } = require('uuid');
12
12
  const logger = require('../utils/logger');
13
13
  const { CouncilRepository } = require('../database');
14
+ const { getProviderClass } = require('../ai/provider');
14
15
 
15
16
  const router = express.Router();
16
17
 
@@ -129,12 +130,20 @@ function validateCouncilFormat(config) {
129
130
  return 'config.levels is required and must be an object';
130
131
  }
131
132
 
132
- const validLevels = ['1', '2', '3'];
133
- const hasEnabled = Object.entries(config.levels).some(([key, val]) =>
134
- validLevels.includes(key) && val === true
135
- );
136
- if (!hasEnabled) {
137
- return 'At least one level (1, 2, or 3) must be enabled';
133
+ // Skip level requirement when all voices are executable providers
134
+ const allExecutable = config.voices.every(v => {
135
+ const ProviderClass = getProviderClass(v.provider);
136
+ return ProviderClass?.isExecutable;
137
+ });
138
+
139
+ if (!allExecutable) {
140
+ const validLevels = ['1', '2', '3'];
141
+ const hasEnabled = Object.entries(config.levels).some(([key, val]) =>
142
+ validLevels.includes(key) && val === true
143
+ );
144
+ if (!hasEnabled) {
145
+ return 'At least one level (1, 2, or 3) must be enabled for non-executable providers';
146
+ }
138
147
  }
139
148
 
140
149
  // Validate consolidation (optional)