@in-the-loop-labs/pair-review 1.4.3 → 1.5.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 (48) hide show
  1. package/package.json +1 -1
  2. package/plugin/.claude-plugin/plugin.json +1 -1
  3. package/plugin/skills/review-requests/SKILL.md +54 -0
  4. package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
  5. package/public/css/pr.css +1081 -54
  6. package/public/css/repo-settings.css +452 -140
  7. package/public/js/components/AdvancedConfigTab.js +1364 -0
  8. package/public/js/components/AnalysisConfigModal.js +488 -112
  9. package/public/js/components/CouncilProgressModal.js +1416 -0
  10. package/public/js/components/TextInputDialog.js +231 -0
  11. package/public/js/components/TimeoutSelect.js +367 -0
  12. package/public/js/components/VoiceCentricConfigTab.js +1334 -0
  13. package/public/js/local.js +162 -83
  14. package/public/js/modules/analysis-history.js +185 -11
  15. package/public/js/modules/comment-manager.js +13 -0
  16. package/public/js/modules/file-comment-manager.js +28 -0
  17. package/public/js/pr.js +233 -115
  18. package/public/js/repo-settings.js +575 -106
  19. package/public/local.html +11 -1
  20. package/public/pr.html +6 -1
  21. package/public/repo-settings.html +28 -21
  22. package/public/setup.html +8 -2
  23. package/src/ai/analyzer.js +1262 -111
  24. package/src/ai/claude-cli.js +2 -2
  25. package/src/ai/claude-provider.js +6 -6
  26. package/src/ai/codex-provider.js +6 -6
  27. package/src/ai/copilot-provider.js +3 -3
  28. package/src/ai/cursor-agent-provider.js +6 -6
  29. package/src/ai/gemini-provider.js +6 -6
  30. package/src/ai/opencode-provider.js +6 -6
  31. package/src/ai/pi-provider.js +6 -6
  32. package/src/ai/prompts/baseline/consolidation/balanced.js +208 -0
  33. package/src/ai/prompts/baseline/consolidation/fast.js +175 -0
  34. package/src/ai/prompts/baseline/consolidation/thorough.js +283 -0
  35. package/src/ai/prompts/config.js +1 -1
  36. package/src/ai/prompts/index.js +26 -2
  37. package/src/ai/provider.js +4 -2
  38. package/src/database.js +417 -14
  39. package/src/main.js +1 -1
  40. package/src/routes/analysis.js +495 -10
  41. package/src/routes/config.js +36 -15
  42. package/src/routes/councils.js +351 -0
  43. package/src/routes/local.js +33 -11
  44. package/src/routes/mcp.js +9 -2
  45. package/src/routes/setup.js +12 -2
  46. package/src/routes/shared.js +126 -13
  47. package/src/server.js +34 -4
  48. package/src/utils/stats-calculator.js +2 -0
@@ -37,9 +37,9 @@ class ClaudeCLI {
37
37
  */
38
38
  async execute(prompt, options = {}) {
39
39
  return new Promise((resolve, reject) => {
40
- const { cwd = process.cwd(), timeout = 300000, level = 'unknown' } = options; // 5 minute default timeout
40
+ const { cwd = process.cwd(), timeout = 300000, level = 'unknown', logPrefix } = options; // 5 minute default timeout
41
41
 
42
- const levelPrefix = `[Level ${level}]`;
42
+ const levelPrefix = logPrefix || `[Level ${level}]`;
43
43
  logger.info(`${levelPrefix} Executing Claude CLI...`);
44
44
  logger.info(`${levelPrefix} Writing prompt: ${prompt.length} bytes`);
45
45
 
@@ -241,9 +241,9 @@ class ClaudeProvider extends AIProvider {
241
241
  */
242
242
  async execute(prompt, options = {}) {
243
243
  return new Promise((resolve, reject) => {
244
- const { cwd = process.cwd(), timeout = 300000, level = 'unknown', analysisId, registerProcess, onStreamEvent } = options;
244
+ const { cwd = process.cwd(), timeout = 300000, level = 'unknown', analysisId, registerProcess, onStreamEvent, logPrefix } = options;
245
245
 
246
- const levelPrefix = `[Level ${level}]`;
246
+ const levelPrefix = logPrefix || `[Level ${level}]`;
247
247
  logger.info(`${levelPrefix} Executing Claude CLI...`);
248
248
  logger.info(`${levelPrefix} Writing prompt: ${prompt.length} bytes`);
249
249
 
@@ -359,7 +359,7 @@ class ClaudeProvider extends AIProvider {
359
359
  logger.info(`${levelPrefix} Claude CLI completed: ${lineCount} JSONL events received`);
360
360
 
361
361
  // Parse the Claude JSONL stream response
362
- const parsed = this.parseClaudeResponse(stdout, level);
362
+ const parsed = this.parseClaudeResponse(stdout, level, levelPrefix);
363
363
  if (parsed.success) {
364
364
  logger.success(`${levelPrefix} Successfully parsed JSON response`);
365
365
  // Dump the parsed data for debugging
@@ -384,7 +384,7 @@ class ClaudeProvider extends AIProvider {
384
384
  // Use async IIFE to handle the async LLM extraction
385
385
  (async () => {
386
386
  try {
387
- const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess });
387
+ const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess, logPrefix: levelPrefix });
388
388
  if (llmExtracted.success) {
389
389
  logger.success(`${levelPrefix} LLM extraction fallback succeeded`);
390
390
  settle(resolve, llmExtracted.data);
@@ -633,8 +633,8 @@ class ClaudeProvider extends AIProvider {
633
633
  * @param {string|number} level - Analysis level for logging
634
634
  * @returns {{success: boolean, data?: Object, error?: string}}
635
635
  */
636
- parseClaudeResponse(stdout, level) {
637
- const levelPrefix = `[Level ${level}]`;
636
+ parseClaudeResponse(stdout, level, logPrefix) {
637
+ const levelPrefix = logPrefix || `[Level ${level}]`;
638
638
 
639
639
  try {
640
640
  // Split by newlines and parse each JSON line
@@ -132,9 +132,9 @@ class CodexProvider extends AIProvider {
132
132
  */
133
133
  async execute(prompt, options = {}) {
134
134
  return new Promise((resolve, reject) => {
135
- const { cwd = process.cwd(), timeout = 300000, level = 'unknown', analysisId, registerProcess, onStreamEvent } = options;
135
+ const { cwd = process.cwd(), timeout = 300000, level = 'unknown', analysisId, registerProcess, onStreamEvent, logPrefix } = options;
136
136
 
137
- const levelPrefix = `[Level ${level}]`;
137
+ const levelPrefix = logPrefix || `[Level ${level}]`;
138
138
  logger.info(`${levelPrefix} Executing Codex CLI...`);
139
139
  logger.info(`${levelPrefix} Writing prompt: ${prompt.length} bytes`);
140
140
 
@@ -256,7 +256,7 @@ class CodexProvider extends AIProvider {
256
256
  }
257
257
 
258
258
  // Parse the Codex JSONL response
259
- const parsed = this.parseCodexResponse(stdout, level);
259
+ const parsed = this.parseCodexResponse(stdout, level, levelPrefix);
260
260
  if (parsed.success) {
261
261
  logger.success(`${levelPrefix} Successfully parsed JSON response`);
262
262
  // Dump the parsed data for debugging
@@ -278,7 +278,7 @@ class CodexProvider extends AIProvider {
278
278
  // Use async IIFE to handle the async LLM extraction
279
279
  (async () => {
280
280
  try {
281
- const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess });
281
+ const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess, logPrefix: levelPrefix });
282
282
  if (llmExtracted.success) {
283
283
  logger.success(`${levelPrefix} LLM extraction fallback succeeded`);
284
284
  settle(resolve, llmExtracted.data);
@@ -337,8 +337,8 @@ class CodexProvider extends AIProvider {
337
337
  * @param {string|number} level - Analysis level for logging
338
338
  * @returns {{success: boolean, data?: Object, error?: string}}
339
339
  */
340
- parseCodexResponse(stdout, level) {
341
- const levelPrefix = `[Level ${level}]`;
340
+ parseCodexResponse(stdout, level, logPrefix) {
341
+ const levelPrefix = logPrefix || `[Level ${level}]`;
342
342
 
343
343
  try {
344
344
  // Split by newlines and parse each JSON line
@@ -197,9 +197,9 @@ class CopilotProvider extends AIProvider {
197
197
  return new Promise((resolve, reject) => {
198
198
  // Note: Copilot does not support streaming — output is plain text returned on process exit, not JSONL.
199
199
  // onStreamEvent is therefore not destructured here (no StreamParser integration).
200
- const { cwd = process.cwd(), timeout = 300000, level = 'unknown', analysisId, registerProcess } = options;
200
+ const { cwd = process.cwd(), timeout = 300000, level = 'unknown', analysisId, registerProcess, logPrefix } = options;
201
201
 
202
- const levelPrefix = `[Level ${level}]`;
202
+ const levelPrefix = logPrefix || `[Level ${level}]`;
203
203
  logger.info(`${levelPrefix} Executing Copilot CLI...`);
204
204
  logger.info(`${levelPrefix} Writing prompt: ${prompt.length} bytes`);
205
205
 
@@ -310,7 +310,7 @@ class CopilotProvider extends AIProvider {
310
310
  // Use async IIFE to handle the async LLM extraction
311
311
  (async () => {
312
312
  try {
313
- const llmExtracted = await this.extractJSONWithLLM(stdout, { level, analysisId, registerProcess });
313
+ const llmExtracted = await this.extractJSONWithLLM(stdout, { level, analysisId, registerProcess, logPrefix: levelPrefix });
314
314
  if (llmExtracted.success) {
315
315
  logger.success(`${levelPrefix} LLM extraction fallback succeeded`);
316
316
  settle(resolve, llmExtracted.data);
@@ -211,9 +211,9 @@ class CursorAgentProvider extends AIProvider {
211
211
  */
212
212
  async execute(prompt, options = {}) {
213
213
  return new Promise((resolve, reject) => {
214
- const { cwd = process.cwd(), timeout = 300000, level = 'unknown', analysisId, registerProcess, onStreamEvent } = options;
214
+ const { cwd = process.cwd(), timeout = 300000, level = 'unknown', analysisId, registerProcess, onStreamEvent, logPrefix } = options;
215
215
 
216
- const levelPrefix = `[Level ${level}]`;
216
+ const levelPrefix = logPrefix || `[Level ${level}]`;
217
217
  logger.info(`${levelPrefix} Executing Cursor Agent CLI...`);
218
218
  logger.info(`${levelPrefix} Writing prompt: ${prompt.length} chars`);
219
219
 
@@ -335,7 +335,7 @@ class CursorAgentProvider extends AIProvider {
335
335
  logger.info(`${levelPrefix} Cursor Agent CLI completed: ${lineCount} JSONL events received`);
336
336
 
337
337
  // Parse the Cursor Agent JSONL stream response
338
- const parsed = this.parseCursorAgentResponse(stdout, level);
338
+ const parsed = this.parseCursorAgentResponse(stdout, level, levelPrefix);
339
339
  if (parsed.success) {
340
340
  logger.success(`${levelPrefix} Successfully parsed JSON response`);
341
341
  // Dump the parsed data for debugging
@@ -361,7 +361,7 @@ class CursorAgentProvider extends AIProvider {
361
361
  // orphan processes if timeout fired between close-handler entry
362
362
  // and reaching this point.
363
363
  if (settled) return;
364
- const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess });
364
+ const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess, logPrefix: levelPrefix });
365
365
  if (llmExtracted.success) {
366
366
  logger.success(`${levelPrefix} LLM extraction fallback succeeded`);
367
367
  settle(resolve, llmExtracted.data);
@@ -425,8 +425,8 @@ class CursorAgentProvider extends AIProvider {
425
425
  * @param {string|number} level - Analysis level for logging
426
426
  * @returns {{success: boolean, data?: Object, error?: string}}
427
427
  */
428
- parseCursorAgentResponse(stdout, level) {
429
- const levelPrefix = `[Level ${level}]`;
428
+ parseCursorAgentResponse(stdout, level, logPrefix) {
429
+ const levelPrefix = logPrefix || `[Level ${level}]`;
430
430
 
431
431
  try {
432
432
  // Split by newlines and parse each JSON line
@@ -174,9 +174,9 @@ class GeminiProvider extends AIProvider {
174
174
  */
175
175
  async execute(prompt, options = {}) {
176
176
  return new Promise((resolve, reject) => {
177
- const { cwd = process.cwd(), timeout = 300000, level = 'unknown', analysisId, registerProcess, onStreamEvent } = options;
177
+ const { cwd = process.cwd(), timeout = 300000, level = 'unknown', analysisId, registerProcess, onStreamEvent, logPrefix } = options;
178
178
 
179
- const levelPrefix = `[Level ${level}]`;
179
+ const levelPrefix = logPrefix || `[Level ${level}]`;
180
180
  logger.info(`${levelPrefix} Executing Gemini CLI...`);
181
181
  logger.info(`${levelPrefix} Writing prompt: ${prompt.length} bytes`);
182
182
 
@@ -298,7 +298,7 @@ class GeminiProvider extends AIProvider {
298
298
  }
299
299
 
300
300
  // Parse the Gemini JSONL stream response
301
- const parsed = this.parseGeminiResponse(stdout, level);
301
+ const parsed = this.parseGeminiResponse(stdout, level, levelPrefix);
302
302
  if (parsed.success) {
303
303
  logger.success(`${levelPrefix} Successfully parsed JSON response`);
304
304
  // Dump the parsed data for debugging
@@ -320,7 +320,7 @@ class GeminiProvider extends AIProvider {
320
320
  // Use async IIFE to handle the async LLM extraction
321
321
  (async () => {
322
322
  try {
323
- const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess });
323
+ const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess, logPrefix: levelPrefix });
324
324
  if (llmExtracted.success) {
325
325
  logger.success(`${levelPrefix} LLM extraction fallback succeeded`);
326
326
  settle(resolve, llmExtracted.data);
@@ -382,8 +382,8 @@ class GeminiProvider extends AIProvider {
382
382
  * @param {string|number} level - Analysis level for logging
383
383
  * @returns {{success: boolean, data?: Object, error?: string}}
384
384
  */
385
- parseGeminiResponse(stdout, level) {
386
- const levelPrefix = `[Level ${level}]`;
385
+ parseGeminiResponse(stdout, level, logPrefix) {
386
+ const levelPrefix = logPrefix || `[Level ${level}]`;
387
387
 
388
388
  try {
389
389
  // Split by newlines and parse each JSON line
@@ -102,9 +102,9 @@ class OpenCodeProvider extends AIProvider {
102
102
  */
103
103
  async execute(prompt, options = {}) {
104
104
  return new Promise((resolve, reject) => {
105
- const { cwd = process.cwd(), timeout = 300000, level = 'unknown', analysisId, registerProcess, onStreamEvent } = options;
105
+ const { cwd = process.cwd(), timeout = 300000, level = 'unknown', analysisId, registerProcess, onStreamEvent, logPrefix } = options;
106
106
 
107
- const levelPrefix = `[Level ${level}]`;
107
+ const levelPrefix = logPrefix || `[Level ${level}]`;
108
108
  logger.info(`${levelPrefix} Executing OpenCode CLI...`);
109
109
  logger.info(`${levelPrefix} Writing prompt via stdin: ${prompt.length} bytes`);
110
110
 
@@ -238,7 +238,7 @@ class OpenCodeProvider extends AIProvider {
238
238
  logger.info(`${levelPrefix} OpenCode CLI completed - received ${lineCount} JSONL events`);
239
239
 
240
240
  // Parse the OpenCode JSONL response
241
- const parsed = this.parseOpenCodeResponse(stdout, level);
241
+ const parsed = this.parseOpenCodeResponse(stdout, level, levelPrefix);
242
242
  if (parsed.success) {
243
243
  logger.success(`${levelPrefix} Successfully parsed JSON response`);
244
244
 
@@ -262,7 +262,7 @@ class OpenCodeProvider extends AIProvider {
262
262
  // Use async IIFE to handle the async LLM extraction
263
263
  (async () => {
264
264
  try {
265
- const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess });
265
+ const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess, logPrefix: levelPrefix });
266
266
  if (llmExtracted.success) {
267
267
  logger.success(`${levelPrefix} LLM extraction fallback succeeded`);
268
268
  settle(resolve, llmExtracted.data);
@@ -442,8 +442,8 @@ class OpenCodeProvider extends AIProvider {
442
442
  * @param {string|number} level - Analysis level for logging
443
443
  * @returns {{success: boolean, data?: Object, error?: string}}
444
444
  */
445
- parseOpenCodeResponse(stdout, level) {
446
- const levelPrefix = `[Level ${level}]`;
445
+ parseOpenCodeResponse(stdout, level, logPrefix) {
446
+ const levelPrefix = logPrefix || `[Level ${level}]`;
447
447
 
448
448
  try {
449
449
  // Split by newlines and parse each JSON line
@@ -241,9 +241,9 @@ class PiProvider extends AIProvider {
241
241
  */
242
242
  async execute(prompt, options = {}) {
243
243
  return new Promise((resolve, reject) => {
244
- const { cwd = process.cwd(), timeout = 300000, level = 'unknown', analysisId, registerProcess, onStreamEvent } = options;
244
+ const { cwd = process.cwd(), timeout = 300000, level = 'unknown', analysisId, registerProcess, onStreamEvent, logPrefix } = options;
245
245
 
246
- const levelPrefix = `[Level ${level}]`;
246
+ const levelPrefix = logPrefix || `[Level ${level}]`;
247
247
  logger.info(`${levelPrefix} Executing Pi CLI...`);
248
248
  logger.info(`${levelPrefix} Writing prompt via stdin: ${prompt.length} bytes`);
249
249
 
@@ -388,7 +388,7 @@ class PiProvider extends AIProvider {
388
388
  logger.info(`${levelPrefix} Pi CLI completed - received ${lineCount} JSONL events`);
389
389
 
390
390
  // Parse the Pi JSONL response
391
- const parsed = this.parsePiResponse(stdout, level);
391
+ const parsed = this.parsePiResponse(stdout, level, levelPrefix);
392
392
  if (parsed.success) {
393
393
  logger.success(`${levelPrefix} Successfully parsed JSON response`);
394
394
 
@@ -419,7 +419,7 @@ class PiProvider extends AIProvider {
419
419
  if (settled) return;
420
420
 
421
421
  try {
422
- const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess });
422
+ const llmExtracted = await this.extractJSONWithLLM(llmFallbackInput, { level, analysisId, registerProcess, logPrefix: levelPrefix });
423
423
  if (llmExtracted.success) {
424
424
  logger.success(`${levelPrefix} LLM extraction fallback succeeded`);
425
425
  settle(resolve, llmExtracted.data);
@@ -612,8 +612,8 @@ class PiProvider extends AIProvider {
612
612
  * @param {string|number} level - Analysis level for logging
613
613
  * @returns {{success: boolean, data?: Object, error?: string}}
614
614
  */
615
- parsePiResponse(stdout, level) {
616
- const levelPrefix = `[Level ${level}]`;
615
+ parsePiResponse(stdout, level, logPrefix) {
616
+ const levelPrefix = logPrefix || `[Level ${level}]`;
617
617
 
618
618
  try {
619
619
  // Split by newlines and parse each JSON line
@@ -0,0 +1,208 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ /**
3
+ * Consolidation Balanced Prompt - Cross-Reviewer Suggestion Merging
4
+ *
5
+ * This is the canonical baseline prompt for Consolidation analysis.
6
+ * It merges suggestions from multiple independent AI reviewers who
7
+ * analyzed the same code changes, deduplicating and resolving conflicts.
8
+ *
9
+ * Unlike Orchestration (which merges across analysis levels 1/2/3),
10
+ * Consolidation merges across reviewers within the same scope.
11
+ *
12
+ * Section categories:
13
+ * - locked: Cannot be modified by variants (data integrity)
14
+ * - required: Must be present, content can be rephrased
15
+ * - optional: Can be removed entirely if unhelpful
16
+ */
17
+
18
+ /**
19
+ * Tagged prompt template for Consolidation Balanced analysis
20
+ *
21
+ * Placeholders:
22
+ * - {{reviewIntro}} - Review introduction line
23
+ * - {{lineNumberGuidance}} - Line number guidance section
24
+ * - {{customInstructions}} - Custom instructions section (optional)
25
+ * - {{reviewerSuggestions}} - Formatted reviewer suggestions input
26
+ * - {{suggestionCount}} - Total number of input suggestions
27
+ * - {{reviewerCount}} - Number of reviewers being consolidated
28
+ */
29
+ const taggedPrompt = `<section name="role" required="true">
30
+ {{reviewIntro}}
31
+ </section>
32
+
33
+ <section name="task-header" required="true">
34
+ # Cross-Reviewer Consolidation Task
35
+ </section>
36
+
37
+ <section name="line-number-guidance" required="true">
38
+ {{lineNumberGuidance}}
39
+ </section>
40
+
41
+ <section name="critical-output" locked="true">
42
+ **>>> CRITICAL: Output ONLY valid JSON. No markdown, no \`\`\`json blocks. Start with { end with }. <<<**
43
+ </section>
44
+
45
+ <section name="role-description" required="true">
46
+ ## Your Role
47
+ Multiple independent AI reviewers have analyzed the same code changes. Your job is to merge their findings into a single, high-quality set of suggestions by deduplicating, resolving conflicts, and preserving unique insights.
48
+ </section>
49
+
50
+ <section name="custom-instructions" optional="true">
51
+ {{customInstructions}}
52
+ </section>
53
+
54
+ <section name="input-suggestions" locked="true">
55
+ ## Input: {{reviewerCount}} Reviewer(s), {{suggestionCount}} Total Suggestions
56
+
57
+ {{reviewerSuggestions}}
58
+ </section>
59
+
60
+ <section name="consolidation-rules" required="true">
61
+ ## Consolidation Guidelines
62
+
63
+ ### 1. Deduplication
64
+ - **Merge suggestions** that identify the same issue at the same location
65
+ - When merging, combine the best elements from each reviewer's description
66
+ - Use the most specific and actionable framing
67
+
68
+ ### 2. Conflict Resolution
69
+ - When reviewers **disagree**, prefer the analysis with stronger evidence
70
+ - If genuinely uncertain, keep the suggestion with reduced confidence
71
+ - Consider whether one reviewer had context the other missed
72
+
73
+ ### 3. Unique Insights
74
+ - **Preserve suggestions** that only one reviewer noticed
75
+ - A unique finding from one reviewer can be the most valuable insight
76
+ - Don't discard something just because only one reviewer flagged it
77
+
78
+ ### 4. Quality Filter
79
+ - Drop suggestions with very low confidence (< 0.3) unless multiple reviewers agree
80
+ - Boost confidence when multiple reviewers independently identify the same issue
81
+ </section>
82
+
83
+ <section name="consensus-handling" required="true">
84
+ ### 5. Consensus Handling
85
+ - **Agreement**: When multiple reviewers flag the same issue, increase confidence by 0.1-0.2 (cap at 1.0)
86
+ - **Partial overlap**: Merge related but distinct observations into a richer suggestion
87
+ - **Contradiction**: Use your judgment; prefer the more actionable analysis
88
+ </section>
89
+
90
+ <section name="output-schema" locked="true">
91
+ ## Output Format
92
+
93
+ **>>> CRITICAL: Output ONLY valid JSON. No markdown, no \`\`\`json blocks. Start with { end with }. <<<**
94
+
95
+ Output JSON with this structure:
96
+ {
97
+ "suggestions": [
98
+ {
99
+ "file": "path/to/file",
100
+ "line": 42,
101
+ "old_or_new": "NEW",
102
+ "type": "bug|improvement|praise|suggestion|design|performance|security|code-style",
103
+ "title": "Brief title",
104
+ "description": "Detailed explanation",
105
+ "suggestion": "How to fix/improve (omit for praise)",
106
+ "confidence": 0.0-1.0
107
+ }
108
+ ],
109
+ "fileLevelSuggestions": [{
110
+ "file": "path/to/file",
111
+ "type": "bug|improvement|praise|suggestion|design|performance|security|code-style",
112
+ "title": "Brief title describing file-level concern",
113
+ "description": "Explanation of the file-level observation",
114
+ "suggestion": "How to address the file-level concern (omit for praise items)",
115
+ "confidence": 0.0-1.0
116
+ }],
117
+ "summary": "Brief consolidation summary. Write as if a single reviewer produced this analysis — do NOT mention 'consolidation', 'merging', or 'multiple reviewers'."
118
+ }
119
+ </section>
120
+
121
+ <section name="diff-instructions" required="true">
122
+ ## Line Number Reference (old_or_new field)
123
+ - **"NEW"** (default): For added [+] and context lines
124
+ - **"OLD"**: ONLY for deleted [-] lines
125
+ Preserve the old_or_new value from input suggestions when merging.
126
+ </section>
127
+
128
+ <section name="guidelines" required="true">
129
+ ## Important Notes
130
+ - **Quality over quantity** — better to have fewer excellent suggestions than many mediocre ones
131
+ - **Cross-reviewer agreement** increases confidence significantly
132
+ - **Preserve actionability** — every suggestion should give clear next steps
133
+ - **Only include modified files** — discard suggestions for unmodified files
134
+ </section>`;
135
+
136
+ /**
137
+ * Section definitions with metadata
138
+ * Used for parsing and validation
139
+ */
140
+ const sections = [
141
+ { name: 'role', required: true },
142
+ { name: 'task-header', required: true },
143
+ { name: 'line-number-guidance', required: true },
144
+ { name: 'critical-output', locked: true },
145
+ { name: 'role-description', required: true },
146
+ { name: 'custom-instructions', optional: true },
147
+ { name: 'input-suggestions', locked: true },
148
+ { name: 'consolidation-rules', required: true },
149
+ { name: 'consensus-handling', required: true },
150
+ { name: 'output-schema', locked: true },
151
+ { name: 'diff-instructions', required: true },
152
+ { name: 'guidelines', required: true }
153
+ ];
154
+
155
+ /**
156
+ * Default section order for Consolidation Balanced
157
+ */
158
+ const defaultOrder = [
159
+ 'role',
160
+ 'task-header',
161
+ 'line-number-guidance',
162
+ 'critical-output',
163
+ 'role-description',
164
+ 'custom-instructions',
165
+ 'input-suggestions',
166
+ 'consolidation-rules',
167
+ 'consensus-handling',
168
+ 'output-schema',
169
+ 'diff-instructions',
170
+ 'guidelines'
171
+ ];
172
+
173
+ /**
174
+ * Parse the tagged prompt into section objects
175
+ * @returns {Array<Object>} Array of section objects with name, attributes, and content
176
+ */
177
+ function parseSections() {
178
+ const sectionRegex = /<section\s+name="([^"]+)"([^>]*)>([\s\S]*?)<\/section>/g;
179
+ const parsed = [];
180
+ let match;
181
+
182
+ while ((match = sectionRegex.exec(taggedPrompt)) !== null) {
183
+ const [, name, attrs, content] = match;
184
+ const section = {
185
+ name,
186
+ content: content.trim(),
187
+ locked: attrs.includes('locked="true"'),
188
+ required: attrs.includes('required="true"'),
189
+ optional: attrs.includes('optional="true"')
190
+ };
191
+
192
+ const tierMatch = attrs.match(/tier="([^"]+)"/);
193
+ if (tierMatch) {
194
+ section.tier = tierMatch[1].split(',').map(t => t.trim());
195
+ }
196
+
197
+ parsed.push(section);
198
+ }
199
+
200
+ return parsed;
201
+ }
202
+
203
+ module.exports = {
204
+ taggedPrompt,
205
+ sections,
206
+ defaultOrder,
207
+ parseSections
208
+ };
@@ -0,0 +1,175 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ /**
3
+ * Consolidation Fast Prompt - Quick Cross-Reviewer Suggestion Merging
4
+ *
5
+ * This is the fast tier variant of Consolidation analysis. It is optimized
6
+ * for speed with shorter, more directive prompts.
7
+ *
8
+ * Tier-specific optimizations applied:
9
+ * - Simplified: consolidation-rules to essential directives
10
+ * - Removed: consensus-handling section (folded into rules)
11
+ * - Shortened: guidelines to essentials
12
+ *
13
+ * Section categories:
14
+ * - locked: Cannot be modified by variants (data integrity)
15
+ * - required: Must be present, content can be rephrased
16
+ * - optional: Can be removed entirely if unhelpful
17
+ */
18
+
19
+ /**
20
+ * Tagged prompt template for Consolidation Fast analysis
21
+ *
22
+ * Placeholders:
23
+ * - {{reviewIntro}} - Review introduction line
24
+ * - {{lineNumberGuidance}} - Line number guidance section
25
+ * - {{customInstructions}} - Custom instructions section (optional)
26
+ * - {{reviewerSuggestions}} - Formatted reviewer suggestions input
27
+ * - {{suggestionCount}} - Total number of input suggestions
28
+ * - {{reviewerCount}} - Number of reviewers being consolidated
29
+ */
30
+ const taggedPrompt = `<section name="role" required="true" tier="fast">
31
+ {{reviewIntro}}
32
+ </section>
33
+
34
+ <section name="task-header" required="true" tier="fast">
35
+ # Cross-Reviewer Consolidation
36
+ </section>
37
+
38
+ <section name="line-number-guidance" required="true">
39
+ {{lineNumberGuidance}}
40
+ </section>
41
+
42
+ <section name="critical-output" locked="true">
43
+ **>>> CRITICAL: Output ONLY valid JSON. No markdown, no \`\`\`json blocks. Start with { end with }. <<<**
44
+ </section>
45
+
46
+ <section name="role-description" required="true" tier="fast">
47
+ ## Task
48
+ Merge suggestions from multiple AI reviewers. Deduplicate. Resolve conflicts. Keep high-value items only.
49
+ </section>
50
+
51
+ <section name="custom-instructions" optional="true" tier="fast,balanced,thorough">
52
+ {{customInstructions}}
53
+ </section>
54
+
55
+ <section name="input-suggestions" locked="true">
56
+ ## Input: {{reviewerCount}} Reviewer(s), {{suggestionCount}} Total Suggestions
57
+
58
+ {{reviewerSuggestions}}
59
+ </section>
60
+
61
+ <section name="consolidation-rules" required="true" tier="fast">
62
+ ## Rules
63
+ - Merge duplicate suggestions (same file/line/issue). Boost confidence for consensus.
64
+ - When reviewers disagree, keep the more specific analysis.
65
+ - Preserve unique insights from individual reviewers.
66
+ - Drop very low confidence (< 0.3) items unless multiple reviewers agree.
67
+ </section>
68
+
69
+ <section name="output-schema" locked="true">
70
+ ## JSON Schema
71
+ {
72
+ "suggestions": [{
73
+ "file": "path/to/file",
74
+ "line": 42,
75
+ "old_or_new": "NEW",
76
+ "type": "bug|improvement|praise|suggestion|design|performance|security|code-style",
77
+ "title": "Brief title",
78
+ "description": "Detailed explanation",
79
+ "suggestion": "How to fix/improve (omit for praise)",
80
+ "confidence": 0.0-1.0
81
+ }],
82
+ "fileLevelSuggestions": [{
83
+ "file": "path/to/file",
84
+ "type": "bug|improvement|praise|suggestion|design|performance|security|code-style",
85
+ "title": "Brief title describing file-level concern",
86
+ "description": "Explanation of the file-level observation",
87
+ "suggestion": "How to address the file-level concern (omit for praise items)",
88
+ "confidence": 0.0-1.0
89
+ }],
90
+ "summary": "Key findings as if from single reviewer (no mention of consolidation/merging)"
91
+ }
92
+ </section>
93
+
94
+ <section name="diff-instructions" required="true" tier="fast">
95
+ ## old_or_new
96
+ "NEW" (default): added [+] and context lines. "OLD": deleted [-] only. Preserve from input.
97
+ </section>
98
+
99
+ <section name="guidelines" required="true" tier="fast">
100
+ ## Notes
101
+ Quality over quantity. Higher confidence for multi-reviewer agreement. Only modified files. Omit uncertain suggestions.
102
+ </section>`;
103
+
104
+ /**
105
+ * Section definitions with metadata
106
+ * Used for parsing and validation
107
+ */
108
+ const sections = [
109
+ { name: 'role', required: true, tier: ['fast'] },
110
+ { name: 'task-header', required: true, tier: ['fast'] },
111
+ { name: 'line-number-guidance', required: true },
112
+ { name: 'critical-output', locked: true },
113
+ { name: 'role-description', required: true, tier: ['fast'] },
114
+ { name: 'custom-instructions', optional: true, tier: ['fast', 'balanced', 'thorough'] },
115
+ { name: 'input-suggestions', locked: true },
116
+ { name: 'consolidation-rules', required: true, tier: ['fast'] },
117
+ { name: 'output-schema', locked: true },
118
+ { name: 'diff-instructions', required: true, tier: ['fast'] },
119
+ { name: 'guidelines', required: true, tier: ['fast'] }
120
+ ];
121
+
122
+ /**
123
+ * Default section order for Consolidation Fast
124
+ * Note: Removed consensus-handling section
125
+ */
126
+ const defaultOrder = [
127
+ 'role',
128
+ 'task-header',
129
+ 'line-number-guidance',
130
+ 'critical-output',
131
+ 'role-description',
132
+ 'custom-instructions',
133
+ 'input-suggestions',
134
+ 'consolidation-rules',
135
+ 'output-schema',
136
+ 'diff-instructions',
137
+ 'guidelines'
138
+ ];
139
+
140
+ /**
141
+ * Parse the tagged prompt into section objects
142
+ * @returns {Array<Object>} Array of section objects with name, attributes, and content
143
+ */
144
+ function parseSections() {
145
+ const sectionRegex = /<section\s+name="([^"]+)"([^>]*)>([\s\S]*?)<\/section>/g;
146
+ const parsed = [];
147
+ let match;
148
+
149
+ while ((match = sectionRegex.exec(taggedPrompt)) !== null) {
150
+ const [, name, attrs, content] = match;
151
+ const section = {
152
+ name,
153
+ content: content.trim(),
154
+ locked: attrs.includes('locked="true"'),
155
+ required: attrs.includes('required="true"'),
156
+ optional: attrs.includes('optional="true"')
157
+ };
158
+
159
+ const tierMatch = attrs.match(/tier="([^"]+)"/);
160
+ if (tierMatch) {
161
+ section.tier = tierMatch[1].split(',').map(t => t.trim());
162
+ }
163
+
164
+ parsed.push(section);
165
+ }
166
+
167
+ return parsed;
168
+ }
169
+
170
+ module.exports = {
171
+ taggedPrompt,
172
+ sections,
173
+ defaultOrder,
174
+ parseSections
175
+ };