@in-the-loop-labs/pair-review 3.5.2 → 3.7.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 +4 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/analysis-config.css +1807 -0
- package/public/css/pr.css +1029 -2169
- package/public/index.html +11 -0
- package/public/js/components/AIPanel.js +39 -23
- package/public/js/components/AdvancedConfigTab.js +56 -4
- package/public/js/components/AnalysisConfigModal.js +41 -25
- package/public/js/components/ChatPanel.js +163 -3
- package/public/js/components/KeyboardShortcuts.js +10 -26
- package/public/js/components/ReviewModal.js +135 -13
- package/public/js/components/TourBar.js +248 -0
- package/public/js/components/VoiceCentricConfigTab.js +36 -0
- package/public/js/index.js +175 -16
- package/public/js/local.js +64 -8
- package/public/js/modules/cancel-background-job.js +183 -0
- package/public/js/modules/hunk-summary-renderer.js +116 -0
- package/public/js/modules/storage-cleanup.js +16 -0
- package/public/js/modules/suggestion-manager.js +25 -1
- package/public/js/modules/tour-renderer.js +755 -0
- package/public/js/pr.js +1826 -56
- package/public/js/repo-links.js +328 -0
- package/public/js/utils/modal-detection.js +77 -0
- package/public/js/utils/provider-model.js +88 -0
- package/public/js/utils/storage-keys.js +50 -0
- package/public/local.html +24 -0
- package/public/pr.html +24 -0
- package/public/repo-settings.html +1 -0
- package/public/setup.html +2 -0
- package/src/ai/abort-signal-wiring.js +130 -0
- package/src/ai/analyzer.js +125 -18
- package/src/ai/background-queue.js +290 -0
- package/src/ai/claude-cli.js +1 -1
- package/src/ai/claude-provider.js +50 -7
- package/src/ai/codex-provider.js +28 -5
- package/src/ai/copilot-provider.js +22 -3
- package/src/ai/cursor-agent-provider.js +22 -6
- package/src/ai/executable-provider.js +4 -19
- package/src/ai/gemini-provider.js +22 -5
- package/src/ai/hunk-hashing.js +161 -0
- package/src/ai/index.js +2 -0
- package/src/ai/opencode-provider.js +21 -5
- package/src/ai/pi-provider.js +21 -5
- package/src/ai/prompts/hunk-summary.js +199 -0
- package/src/ai/prompts/tour.js +232 -0
- package/src/ai/provider.js +21 -1
- package/src/ai/summary-generator.js +469 -0
- package/src/ai/tour-generator.js +568 -0
- package/src/config.js +778 -10
- package/src/database.js +282 -1
- package/src/external/github-adapter.js +114 -25
- package/src/git/base-branch.js +11 -4
- package/src/github/client.js +482 -588
- package/src/github/errors.js +55 -0
- package/src/github/impl/graphql/pending-review-comments.js +230 -0
- package/src/github/impl/graphql/pending-review.js +153 -0
- package/src/github/impl/graphql/review-lifecycle.js +161 -0
- package/src/github/impl/graphql/stack-walker.js +210 -0
- package/src/github/impl/host/pending-review-comments.js +338 -0
- package/src/github/impl/rest/pending-review.js +251 -0
- package/src/github/impl/rest/review-lifecycle.js +226 -0
- package/src/github/impl/rest/stack-walker.js +309 -0
- package/src/github/operations/pending-review-comments.js +79 -0
- package/src/github/operations/pending-review.js +89 -0
- package/src/github/operations/review-lifecycle.js +126 -0
- package/src/github/operations/stack-walker.js +87 -0
- package/src/github/parser.js +230 -4
- package/src/github/stack-walker.js +14 -189
- package/src/links/repo-links.js +230 -0
- package/src/local-review.js +201 -172
- package/src/main.js +133 -30
- package/src/routes/analyses.js +30 -7
- package/src/routes/bulk-analysis-configs.js +295 -0
- package/src/routes/config.js +118 -3
- package/src/routes/context-files.js +2 -29
- package/src/routes/external-comments.js +20 -10
- package/src/routes/github-collections.js +3 -1
- package/src/routes/local.js +410 -13
- package/src/routes/mcp.js +47 -4
- package/src/routes/middleware/validate-review-id.js +53 -0
- package/src/routes/pr.js +556 -71
- package/src/routes/reviews.js +145 -29
- package/src/routes/setup.js +8 -3
- package/src/routes/stack-analysis.js +33 -9
- package/src/routes/worktrees.js +3 -2
- package/src/server.js +2 -0
- package/src/setup/pr-setup.js +37 -11
- package/src/setup/stack-setup.js +13 -3
- package/src/single-port.js +6 -3
- package/src/utils/diff-hunks.js +65 -0
- package/src/utils/json-extractor.js +5 -2
package/public/setup.html
CHANGED
|
@@ -769,6 +769,7 @@
|
|
|
769
769
|
var targetUrl = new URL(data.reviewUrl, window.location.origin);
|
|
770
770
|
var qs = new URLSearchParams(window.location.search);
|
|
771
771
|
if (qs.get('analyze') === 'true') targetUrl.searchParams.set('analyze', qs.get('analyze'));
|
|
772
|
+
if (qs.get('analysisConfigId')) targetUrl.searchParams.set('analysisConfigId', qs.get('analysisConfigId'));
|
|
772
773
|
window.location.href = targetUrl.toString();
|
|
773
774
|
return;
|
|
774
775
|
}
|
|
@@ -829,6 +830,7 @@
|
|
|
829
830
|
var targetUrl = new URL(msg.reviewUrl, window.location.origin);
|
|
830
831
|
var qs = new URLSearchParams(window.location.search);
|
|
831
832
|
if (qs.get('analyze') === 'true') targetUrl.searchParams.set('analyze', qs.get('analyze'));
|
|
833
|
+
if (qs.get('analysisConfigId')) targetUrl.searchParams.set('analysisConfigId', qs.get('analysisConfigId'));
|
|
832
834
|
window.location.href = targetUrl.toString();
|
|
833
835
|
}
|
|
834
836
|
}, 400);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const logger = require('../utils/logger');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Attach an `AbortSignal` to a spawned child process so that aborting the
|
|
8
|
+
* signal kills the child with `SIGTERM`. Returns a cleanup function that
|
|
9
|
+
* detaches the abort listener — call it from the `close` / `error` /
|
|
10
|
+
* `settle` handler so the listener never outlives the process.
|
|
11
|
+
*
|
|
12
|
+
* Pattern: every provider that spawns an upstream CLI for tour/summary
|
|
13
|
+
* generation calls this once right after `spawn(...)`. The returned
|
|
14
|
+
* `cancelled` getter is included so the post-exit path can distinguish a
|
|
15
|
+
* user-initiated cancel (exit due to SIGTERM we sent) from a real failure.
|
|
16
|
+
*
|
|
17
|
+
* If `signal` is already aborted at the time of wiring, the child is
|
|
18
|
+
* killed immediately and `cancelled` is set to true. Callers should still
|
|
19
|
+
* check `cancelled` before treating the eventual exit as a "real" error.
|
|
20
|
+
*
|
|
21
|
+
* Shell-mode caveat: when the caller spawned with `shell: true`, the
|
|
22
|
+
* `child` we hold is the shell, not the underlying CLI. `child.kill()`
|
|
23
|
+
* only terminates the shell; the grandchild CLI keeps burning tokens.
|
|
24
|
+
* Pass `{ shell: true }` here so we signal the whole process group via
|
|
25
|
+
* `process.kill(-pid, 'SIGTERM')` instead. On Windows we fall back to
|
|
26
|
+
* `taskkill /T /F /PID`. Prefer `shell: false` invocation when an
|
|
27
|
+
* abortSignal is in play — fewer moving parts.
|
|
28
|
+
*
|
|
29
|
+
* @param {import('child_process').ChildProcess} child - Spawned process.
|
|
30
|
+
* @param {AbortSignal | null | undefined} signal - Signal to listen on.
|
|
31
|
+
* @param {Object} [opts]
|
|
32
|
+
* @param {string} [opts.logPrefix] - Log prefix for diagnostics.
|
|
33
|
+
* @param {boolean} [opts.shell=false] - True when the child was spawned
|
|
34
|
+
* with `shell: true`. Causes group-kill semantics so the grandchild CLI
|
|
35
|
+
* dies along with the shell wrapper.
|
|
36
|
+
* @returns {{cancelled: () => boolean, detach: () => void}}
|
|
37
|
+
*/
|
|
38
|
+
function wireAbortToChild(child, signal, opts = {}) {
|
|
39
|
+
let cancelled = false;
|
|
40
|
+
if (!signal) {
|
|
41
|
+
return { cancelled: () => cancelled, detach: () => {} };
|
|
42
|
+
}
|
|
43
|
+
const prefix = opts.logPrefix || '';
|
|
44
|
+
const isShell = opts.shell === true;
|
|
45
|
+
|
|
46
|
+
const killChild = () => {
|
|
47
|
+
// `kill` / process group signaling returns false (or throws ESRCH) if
|
|
48
|
+
// the process is already gone, which is fine — we just need the side
|
|
49
|
+
// effect when it IS still alive.
|
|
50
|
+
if (isShell && child.pid && process.platform !== 'win32') {
|
|
51
|
+
// Group-kill the shell AND its CLI descendant. Requires the caller
|
|
52
|
+
// to have spawned with `detached: true` so the child became a
|
|
53
|
+
// process-group leader (`-pid` targets the group).
|
|
54
|
+
try {
|
|
55
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
56
|
+
return;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (err && err.code === 'ESRCH') {
|
|
59
|
+
// Group already gone — nothing to kill.
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Fall through to single-process kill as a best effort.
|
|
63
|
+
logger.warn(
|
|
64
|
+
`${prefix} process.kill(-pid) failed (${err.message}); falling back to child.kill`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (isShell && child.pid && process.platform === 'win32') {
|
|
69
|
+
// Windows has no process groups: spawn taskkill /T /F to wipe the
|
|
70
|
+
// tree rooted at our shell pid.
|
|
71
|
+
try {
|
|
72
|
+
spawn('taskkill', ['/T', '/F', '/PID', String(child.pid)], { stdio: 'ignore' })
|
|
73
|
+
.on('error', (err) => {
|
|
74
|
+
logger.warn(`${prefix} taskkill failed: ${err.message}`);
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
logger.warn(
|
|
79
|
+
`${prefix} spawn(taskkill) failed (${err.message}); falling back to child.kill`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
child.kill('SIGTERM');
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const onAbort = () => {
|
|
87
|
+
cancelled = true;
|
|
88
|
+
try {
|
|
89
|
+
killChild();
|
|
90
|
+
} catch (err) {
|
|
91
|
+
logger.warn(`${prefix} child.kill on abort failed: ${err.message}`);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (signal.aborted) {
|
|
96
|
+
// Pre-aborted: trigger the kill immediately. The eventual `close`
|
|
97
|
+
// handler will see `cancelled === true` and short-circuit.
|
|
98
|
+
onAbort();
|
|
99
|
+
} else {
|
|
100
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
cancelled: () => cancelled,
|
|
105
|
+
detach: () => {
|
|
106
|
+
try {
|
|
107
|
+
signal.removeEventListener('abort', onAbort);
|
|
108
|
+
} catch {
|
|
109
|
+
// Older AbortSignal polyfills may lack removeEventListener; safe to ignore.
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Build a standardized cancellation error. Providers should throw this
|
|
117
|
+
* (or reject with it) when they detect the abort wiring fired, so the
|
|
118
|
+
* BackgroundQueue's broadcast can mark the job as `cancelled: true`.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} [message] - Human-readable context (defaults to 'cancelled').
|
|
121
|
+
* @returns {Error}
|
|
122
|
+
*/
|
|
123
|
+
function makeAbortError(message) {
|
|
124
|
+
const err = new Error(message || 'cancelled');
|
|
125
|
+
err.name = 'AbortError';
|
|
126
|
+
err.isCancellation = true;
|
|
127
|
+
return err;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { wireAbortToChild, makeAbortError };
|
package/src/ai/analyzer.js
CHANGED
|
@@ -215,11 +215,79 @@ function buildDedupContext(prMetadata, { reviewId, serverPort, runId, excludeRun
|
|
|
215
215
|
return { owner, repo, pullNumber: prMetadata.pr_number, reviewId, serverPort, runId, excludeRunIds };
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Fetch existing PR review comments via the injected GitHubClient/Octokit.
|
|
220
|
+
*
|
|
221
|
+
* Replaces the previous `gh api repos/.../comments --paginate` shell-out so the
|
|
222
|
+
* analyzer no longer depends on the `gh` CLI. The result is embedded directly
|
|
223
|
+
* in the dedup prompt section, removing the need for the AI to spawn a process
|
|
224
|
+
* to fetch the data.
|
|
225
|
+
*
|
|
226
|
+
* Pagination is delegated to Octokit's `paginate` helper so PRs with more than
|
|
227
|
+
* 100 comments are handled transparently. The Octokit instance is taken from
|
|
228
|
+
* the caller-supplied `GitHubClient` so per-repo host bindings (api host,
|
|
229
|
+
* token) are honoured.
|
|
230
|
+
*
|
|
231
|
+
* @param {Object} githubClient - GitHubClient instance (must expose `.octokit`)
|
|
232
|
+
* @param {Object} target - { owner, repo, pullNumber }
|
|
233
|
+
* @param {string} [logPrefix=''] - Log prefix for traceability
|
|
234
|
+
* @returns {Promise<Array<{path: string, line: number|null, start_line: number|null, original_line: number|null, original_start_line: number|null, body: string}>|null>}
|
|
235
|
+
* Array of simplified comment objects, or `null` if the fetch failed or the
|
|
236
|
+
* client/target was incomplete.
|
|
237
|
+
*/
|
|
238
|
+
async function fetchExistingReviewComments(githubClient, target, logPrefix = '') {
|
|
239
|
+
if (!githubClient || !githubClient.octokit) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const { owner, repo, pullNumber } = target || {};
|
|
243
|
+
if (!owner || !repo || !pullNumber) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
logger.info(`${logPrefix}[Dedup] Fetching existing PR review comments for ${owner}/${repo}#${pullNumber} via Octokit`);
|
|
249
|
+
const comments = await githubClient.octokit.paginate(
|
|
250
|
+
githubClient.octokit.rest.pulls.listReviewComments,
|
|
251
|
+
{
|
|
252
|
+
owner,
|
|
253
|
+
repo,
|
|
254
|
+
pull_number: pullNumber,
|
|
255
|
+
per_page: 100
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const simplified = (comments || []).map(c => ({
|
|
260
|
+
path: c.path,
|
|
261
|
+
line: c.line ?? null,
|
|
262
|
+
start_line: c.start_line ?? null,
|
|
263
|
+
original_line: c.original_line ?? null,
|
|
264
|
+
original_start_line: c.original_start_line ?? null,
|
|
265
|
+
body: c.body || ''
|
|
266
|
+
}));
|
|
267
|
+
|
|
268
|
+
logger.info(`${logPrefix}[Dedup] Fetched ${simplified.length} existing review comment(s) for dedup`);
|
|
269
|
+
return simplified;
|
|
270
|
+
} catch (err) {
|
|
271
|
+
logger.warn(`${logPrefix}[Dedup] Failed to fetch existing review comments: ${err.message}`);
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
218
276
|
/**
|
|
219
277
|
* Build dedup instructions text for excluding previously identified issues.
|
|
220
278
|
*
|
|
279
|
+
* The GitHub section is rendered when `context.githubComments` is a non-empty
|
|
280
|
+
* array of pre-fetched comments (see `fetchExistingReviewComments`). The
|
|
281
|
+
* comments are embedded as JSON directly in the prompt so the AI does not
|
|
282
|
+
* need to spawn `gh` (which is unavailable on alt-hosts) to fetch them.
|
|
283
|
+
*
|
|
284
|
+
* If `excludePrevious.github` is set but no comments were supplied (no client,
|
|
285
|
+
* empty list, or fetch failed), the GitHub section is silently omitted — a
|
|
286
|
+
* warning will already have been logged at fetch time.
|
|
287
|
+
*
|
|
221
288
|
* @param {Object|null} excludePrevious - { github: bool, feedback: bool } (or falsy if disabled)
|
|
222
|
-
* @param {Object} context - {
|
|
289
|
+
* @param {Object} context - { reviewId, serverPort, runId, excludeRunIds, githubComments }
|
|
290
|
+
* @param {Array} [context.githubComments] - Pre-fetched PR review comments (path, line, original_line, body)
|
|
223
291
|
* @param {string} [context.runId] - Single run ID to exclude (backward compat)
|
|
224
292
|
* @param {string[]} [context.excludeRunIds] - Array of run IDs to exclude (takes precedence over runId)
|
|
225
293
|
* @returns {string} Instruction text for the dedup-instructions prompt section, or empty string
|
|
@@ -236,13 +304,13 @@ function buildDedupInstructions(excludePrevious, context) {
|
|
|
236
304
|
|
|
237
305
|
After consolidating suggestions, check your results against previously identified issues and remove any that are duplicates or substantially similar. If you have zero suggestions after consolidation, skip this step entirely.`);
|
|
238
306
|
|
|
239
|
-
|
|
307
|
+
const githubComments = Array.isArray(context.githubComments) ? context.githubComments : null;
|
|
308
|
+
if (excludePrevious.github && githubComments && githubComments.length > 0) {
|
|
240
309
|
sections.push(`### GitHub PR Review Comments
|
|
241
|
-
|
|
242
|
-
\`\`\`
|
|
243
|
-
|
|
310
|
+
The following inline review comments already exist on this pull request. Each entry has \`path\` (file), \`line\`/\`original_line\` (end line), \`start_line\`/\`original_start_line\` (start line for multi-line comments; null for single-line comments), and \`body\` (content):
|
|
311
|
+
\`\`\`json
|
|
312
|
+
${JSON.stringify(githubComments, null, 2)}
|
|
244
313
|
\`\`\`
|
|
245
|
-
Each comment has \`path\` (file), \`line\`/\`original_line\` (line number), and \`body\` (content).
|
|
246
314
|
A suggestion is a duplicate if it matches on **all three** of: (1) same file, (2) overlapping or adjacent line range (within 5 lines), and (3) substantially similar issue — i.e., the same category of issue (error handling, validation, naming, etc.) applied to the same code. If a previous comment partially overlaps your suggestion — e.g., it flags missing error handling while your suggestion flags missing error handling *and* input validation — keep only the novel portion that the previous comment does not address. If there is no novel portion, exclude it entirely.`);
|
|
247
315
|
}
|
|
248
316
|
|
|
@@ -321,11 +389,12 @@ class Analyzer {
|
|
|
321
389
|
* @param {string} [options.tier='balanced'] - Analysis tier (fast, balanced, thorough)
|
|
322
390
|
* @param {Object} [options.excludePrevious] - { github: bool, feedback: bool } for dedup
|
|
323
391
|
* @param {number} [options.serverPort] - Server port for dedup API calls
|
|
392
|
+
* @param {Object} [options.githubClient] - GitHubClient used to pre-fetch existing PR review comments for dedup (PR mode only)
|
|
324
393
|
* @returns {Promise<Object>} Analysis results
|
|
325
394
|
*/
|
|
326
395
|
async analyzeAllLevels(prId, worktreePath, prMetadata, progressCallback = null, instructions = null, changedFiles = null, options = {}) {
|
|
327
396
|
const runId = options.runId || uuidv4();
|
|
328
|
-
const { analysisId, skipRunCreation, skipLevel3, reviewerNum, excludePrevious, serverPort } = options;
|
|
397
|
+
const { analysisId, skipRunCreation, skipLevel3, reviewerNum, excludePrevious, serverPort, githubClient } = options;
|
|
329
398
|
const logPrefix = options.logPrefix || '';
|
|
330
399
|
// Respect provider-configured timeout (e.g. Pi's 15 min, executable providers)
|
|
331
400
|
const ProviderClass = getProviderClass(this.provider);
|
|
@@ -526,7 +595,7 @@ class Analyzer {
|
|
|
526
595
|
// Build dedup context from prMetadata and options
|
|
527
596
|
const dedupContext = buildDedupContext(prMetadata, { reviewId: prId, serverPort, runId });
|
|
528
597
|
|
|
529
|
-
const orchestrationResult = await this.orchestrateWithAI(allSuggestions, prMetadata, mergedInstructions, worktreePath, { analysisId, tier, progressCallback, timeout: executionTimeout, logPrefix, reviewerNum, excludePrevious, dedupContext });
|
|
598
|
+
const orchestrationResult = await this.orchestrateWithAI(allSuggestions, prMetadata, mergedInstructions, worktreePath, { analysisId, tier, progressCallback, timeout: executionTimeout, logPrefix, reviewerNum, excludePrevious, dedupContext, githubClient });
|
|
530
599
|
|
|
531
600
|
// Report orchestration step as completed
|
|
532
601
|
if (progressCallback) {
|
|
@@ -2661,10 +2730,11 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
2661
2730
|
* @param {string} options.analysisId - Analysis ID for process tracking (enables cancellation)
|
|
2662
2731
|
* @param {Object} [options.excludePrevious] - { github: bool, feedback: bool } for dedup
|
|
2663
2732
|
* @param {Object} [options.dedupContext] - { owner, repo, pullNumber, reviewId, serverPort }
|
|
2733
|
+
* @param {Object} [options.githubClient] - GitHubClient used to pre-fetch existing PR review comments for dedup
|
|
2664
2734
|
* @returns {Promise<Array>} Curated suggestions array
|
|
2665
2735
|
*/
|
|
2666
2736
|
async orchestrateWithAI(allSuggestions, prMetadata, customInstructions = null, worktreePath = null, options = {}) {
|
|
2667
|
-
const { analysisId, tier = 'balanced', progressCallback, providerOverride, modelOverride, timeout = 600000, logPrefix: lp = '', reviewerNum, excludePrevious, dedupContext } = options;
|
|
2737
|
+
const { analysisId, tier = 'balanced', progressCallback, providerOverride, modelOverride, timeout = 600000, logPrefix: lp = '', reviewerNum, excludePrevious, dedupContext, githubClient } = options;
|
|
2668
2738
|
// Build adapter-level log prefix: when reviewerNum is set (council mode),
|
|
2669
2739
|
// use compact format like [R1 Orch] so concurrent reviewers are disambiguated
|
|
2670
2740
|
const adapterLogPrefix = reviewerNum ? `[R${reviewerNum} Orch]` : '';
|
|
@@ -2685,8 +2755,26 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
2685
2755
|
// Create provider instance for consolidation (use overrides if provided)
|
|
2686
2756
|
const aiProvider = createProvider(providerOverride || this.provider, modelOverride || this.model, this.providerOverrides);
|
|
2687
2757
|
|
|
2758
|
+
// Pre-fetch existing PR review comments for dedup (replaces the prior
|
|
2759
|
+
// `gh api` shell-out — the analyzer no longer depends on the `gh` CLI).
|
|
2760
|
+
// The fetch runs only when the caller opted in via excludePrevious.github
|
|
2761
|
+
// *and* supplied a GitHubClient. When the client is unavailable (e.g.
|
|
2762
|
+
// local mode or a caller that has not yet been updated) the GitHub dedup
|
|
2763
|
+
// section is silently omitted; buildDedupInstructions handles that case.
|
|
2764
|
+
let resolvedDedupContext = dedupContext;
|
|
2765
|
+
if (excludePrevious?.github && githubClient && dedupContext?.owner && dedupContext?.repo && dedupContext?.pullNumber) {
|
|
2766
|
+
const githubComments = await fetchExistingReviewComments(
|
|
2767
|
+
githubClient,
|
|
2768
|
+
{ owner: dedupContext.owner, repo: dedupContext.repo, pullNumber: dedupContext.pullNumber },
|
|
2769
|
+
lp
|
|
2770
|
+
);
|
|
2771
|
+
resolvedDedupContext = { ...dedupContext, githubComments: githubComments || [] };
|
|
2772
|
+
} else if (excludePrevious?.github && !githubClient) {
|
|
2773
|
+
logger.warn(`${lp}[Dedup] excludePrevious.github is enabled but no githubClient was supplied — GitHub dedup section will be omitted`);
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2688
2776
|
// Build the consolidation prompt
|
|
2689
|
-
const prompt = this.buildOrchestrationPrompt(allSuggestions, prMetadata, customInstructions, worktreePath, tier, lp, { excludePrevious, dedupContext });
|
|
2777
|
+
const prompt = this.buildOrchestrationPrompt(allSuggestions, prMetadata, customInstructions, worktreePath, tier, lp, { excludePrevious, dedupContext: resolvedDedupContext });
|
|
2690
2778
|
|
|
2691
2779
|
// Execute AI for cross-level consolidation
|
|
2692
2780
|
logger.info(`${lp}[Consolidation] Running AI consolidation to curate and merge suggestions...`);
|
|
@@ -2849,11 +2937,12 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
2849
2937
|
* @param {Function} [options.progressCallback] - Progress callback
|
|
2850
2938
|
* @param {Object} [options.excludePrevious] - { github: bool, feedback: bool } for dedup
|
|
2851
2939
|
* @param {number} [options.serverPort] - Server port for dedup API calls
|
|
2940
|
+
* @param {Object} [options.githubClient] - GitHubClient used to pre-fetch existing PR review comments for dedup
|
|
2852
2941
|
* @returns {Promise<Object>} Analysis results { runId, suggestions, summary }
|
|
2853
2942
|
*/
|
|
2854
2943
|
async runReviewerCentricCouncil(reviewContext, councilConfig, options = {}) {
|
|
2855
2944
|
const { reviewId, worktreePath, prMetadata, changedFiles, instructions } = reviewContext;
|
|
2856
|
-
const { analysisId, progressCallback, excludePrevious, serverPort } = options;
|
|
2945
|
+
const { analysisId, progressCallback, excludePrevious, serverPort, githubClient } = options;
|
|
2857
2946
|
const parentRunId = options.runId || uuidv4();
|
|
2858
2947
|
|
|
2859
2948
|
logger.section('Review Council Analysis Starting (Reviewer-Centric)');
|
|
@@ -3027,7 +3116,8 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
3027
3116
|
logPrefix: `[${reviewerLabel}] `,
|
|
3028
3117
|
reviewerNum: 1,
|
|
3029
3118
|
excludePrevious,
|
|
3030
|
-
serverPort
|
|
3119
|
+
serverPort,
|
|
3120
|
+
githubClient
|
|
3031
3121
|
}
|
|
3032
3122
|
);
|
|
3033
3123
|
|
|
@@ -3305,7 +3395,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
3305
3395
|
|
|
3306
3396
|
const consolidated = await this._crossVoiceConsolidate(
|
|
3307
3397
|
voiceReviews, prMetadata, consolInstructions, worktreePath,
|
|
3308
|
-
{ provider: consolProvider, model: consolModel, tier: consolTier, timeout: consolConfig.timeout, analysisId, progressCallback, excludePrevious, dedupContext, providerOverrides: this.providerOverrides }
|
|
3398
|
+
{ provider: consolProvider, model: consolModel, tier: consolTier, timeout: consolConfig.timeout, analysisId, progressCallback, excludePrevious, dedupContext, githubClient, providerOverrides: this.providerOverrides }
|
|
3309
3399
|
);
|
|
3310
3400
|
|
|
3311
3401
|
const finalSuggestions = this.validateAndFinalizeSuggestions(
|
|
@@ -3391,7 +3481,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
3391
3481
|
*/
|
|
3392
3482
|
async runCouncilAnalysis(reviewContext, councilConfig, options = {}) {
|
|
3393
3483
|
const { reviewId, worktreePath, prMetadata, changedFiles, instructions } = reviewContext;
|
|
3394
|
-
const { analysisId, progressCallback, excludePrevious, serverPort } = options;
|
|
3484
|
+
const { analysisId, progressCallback, excludePrevious, serverPort, githubClient } = options;
|
|
3395
3485
|
const runId = options.runId || uuidv4();
|
|
3396
3486
|
|
|
3397
3487
|
logger.section('Review Council Analysis Starting');
|
|
@@ -3647,7 +3737,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
3647
3737
|
|
|
3648
3738
|
const orchestrationResult = await this.orchestrateWithAI(
|
|
3649
3739
|
allSuggestions, prMetadata, orchInstructions, worktreePath,
|
|
3650
|
-
{ analysisId, tier: orchTier, progressCallback, providerOverride: orchProvider, modelOverride: orchModel, timeout: orchConfig.timeout || 600000, excludePrevious, dedupContext }
|
|
3740
|
+
{ analysisId, tier: orchTier, progressCallback, providerOverride: orchProvider, modelOverride: orchModel, timeout: orchConfig.timeout || 600000, excludePrevious, dedupContext, githubClient }
|
|
3651
3741
|
);
|
|
3652
3742
|
|
|
3653
3743
|
// Report cross-level orchestration step as completed
|
|
@@ -3958,10 +4048,26 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
3958
4048
|
* @private
|
|
3959
4049
|
*/
|
|
3960
4050
|
async _crossVoiceConsolidate(voiceReviews, prMetadata, customInstructions, worktreePath, config) {
|
|
3961
|
-
const { provider, model, tier, timeout, analysisId, progressCallback, excludePrevious, dedupContext, providerOverrides } = config;
|
|
4051
|
+
const { provider, model, tier, timeout, analysisId, progressCallback, excludePrevious, dedupContext, githubClient, providerOverrides } = config;
|
|
3962
4052
|
|
|
3963
4053
|
const aiProvider = createProvider(provider, model, providerOverrides || {});
|
|
3964
4054
|
|
|
4055
|
+
// Pre-fetch existing PR review comments for dedup (replaces the prior
|
|
4056
|
+
// `gh api` shell-out — the analyzer no longer depends on the `gh` CLI).
|
|
4057
|
+
// See orchestrateWithAI for the matching code path used by the other two
|
|
4058
|
+
// top-level analysis flows (analyzeAllLevels, runCouncilAnalysis).
|
|
4059
|
+
let resolvedDedupContext = dedupContext;
|
|
4060
|
+
if (excludePrevious?.github && githubClient && dedupContext?.owner && dedupContext?.repo && dedupContext?.pullNumber) {
|
|
4061
|
+
const githubComments = await fetchExistingReviewComments(
|
|
4062
|
+
githubClient,
|
|
4063
|
+
{ owner: dedupContext.owner, repo: dedupContext.repo, pullNumber: dedupContext.pullNumber },
|
|
4064
|
+
'[ReviewerCouncil]'
|
|
4065
|
+
);
|
|
4066
|
+
resolvedDedupContext = { ...dedupContext, githubComments: githubComments || [] };
|
|
4067
|
+
} else if (excludePrevious?.github && !githubClient) {
|
|
4068
|
+
logger.warn('[ReviewerCouncil][Dedup] excludePrevious.github is enabled but no githubClient was supplied — GitHub dedup section will be omitted');
|
|
4069
|
+
}
|
|
4070
|
+
|
|
3965
4071
|
const voiceDescriptions = voiceReviews.map(v => {
|
|
3966
4072
|
let desc = `### Reviewer: ${v.voiceKey}`;
|
|
3967
4073
|
if (v.isExecutable) desc += ' [external tool]';
|
|
@@ -3987,7 +4093,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
3987
4093
|
reviewIntro: `You are consolidating code review results from ${voiceReviews.length} independent AI reviewers for ${reviewDescription}. Each reviewer independently analyzed the same code changes and produced a complete review.`,
|
|
3988
4094
|
customInstructions: customInstructions ? this.buildCustomInstructionsSection(customInstructions) : '',
|
|
3989
4095
|
lineNumberGuidance: this.buildOrchestrationLineNumberGuidance(worktreePath),
|
|
3990
|
-
dedupInstructions: buildDedupInstructions(excludePrevious,
|
|
4096
|
+
dedupInstructions: buildDedupInstructions(excludePrevious, resolvedDedupContext || {}),
|
|
3991
4097
|
reviewerSuggestions: voiceDescriptions,
|
|
3992
4098
|
suggestionCount: voiceReviews.reduce((sum, v) => sum + v.suggestionCount, 0),
|
|
3993
4099
|
reviewerCount: voiceReviews.length
|
|
@@ -4052,4 +4158,5 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
4052
4158
|
|
|
4053
4159
|
module.exports = Analyzer;
|
|
4054
4160
|
module.exports.buildDedupContext = buildDedupContext;
|
|
4055
|
-
module.exports.buildDedupInstructions = buildDedupInstructions;
|
|
4161
|
+
module.exports.buildDedupInstructions = buildDedupInstructions;
|
|
4162
|
+
module.exports.fetchExistingReviewComments = fetchExistingReviewComments;
|