edsger 0.40.0 โ†’ 0.41.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 (44) hide show
  1. package/dist/commands/pr-resolve/index.d.ts +10 -0
  2. package/dist/commands/pr-resolve/index.js +44 -0
  3. package/dist/commands/pr-review/index.d.ts +10 -0
  4. package/dist/commands/pr-review/index.js +43 -0
  5. package/dist/index.js +38 -0
  6. package/dist/phases/code-review/__tests__/diff-utils.test.d.ts +1 -0
  7. package/dist/phases/code-review/__tests__/diff-utils.test.js +101 -0
  8. package/dist/phases/code-review/diff-utils.d.ts +36 -0
  9. package/dist/phases/code-review/diff-utils.js +100 -0
  10. package/dist/phases/code-review/index.js +17 -153
  11. package/dist/phases/pr-execution/file-assigner.d.ts +12 -0
  12. package/dist/phases/pr-execution/file-assigner.js +34 -0
  13. package/dist/phases/pr-execution/index.js +25 -8
  14. package/dist/phases/pr-resolve/__tests__/prompts.test.d.ts +1 -0
  15. package/dist/phases/pr-resolve/__tests__/prompts.test.js +116 -0
  16. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.d.ts +1 -0
  17. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.js +138 -0
  18. package/dist/phases/pr-resolve/__tests__/workspace.test.d.ts +1 -0
  19. package/dist/phases/pr-resolve/__tests__/workspace.test.js +111 -0
  20. package/dist/phases/pr-resolve/github-reply.d.ts +13 -0
  21. package/dist/phases/pr-resolve/github-reply.js +59 -0
  22. package/dist/phases/pr-resolve/index.d.ts +27 -0
  23. package/dist/phases/pr-resolve/index.js +213 -0
  24. package/dist/phases/pr-resolve/prompts.d.ts +17 -0
  25. package/dist/phases/pr-resolve/prompts.js +106 -0
  26. package/dist/phases/pr-resolve/workspace.d.ts +29 -0
  27. package/dist/phases/pr-resolve/workspace.js +145 -0
  28. package/dist/phases/pr-review/__tests__/prompts.test.d.ts +1 -0
  29. package/dist/phases/pr-review/__tests__/prompts.test.js +49 -0
  30. package/dist/phases/pr-review/__tests__/review-comments.test.d.ts +1 -0
  31. package/dist/phases/pr-review/__tests__/review-comments.test.js +108 -0
  32. package/dist/phases/pr-review/index.d.ts +25 -0
  33. package/dist/phases/pr-review/index.js +213 -0
  34. package/dist/phases/pr-review/prompts.d.ts +5 -0
  35. package/dist/phases/pr-review/prompts.js +87 -0
  36. package/dist/phases/pr-shared/__tests__/agent-utils.test.d.ts +1 -0
  37. package/dist/phases/pr-shared/__tests__/agent-utils.test.js +91 -0
  38. package/dist/phases/pr-shared/__tests__/context.test.d.ts +1 -0
  39. package/dist/phases/pr-shared/__tests__/context.test.js +94 -0
  40. package/dist/phases/pr-shared/agent-utils.d.ts +39 -0
  41. package/dist/phases/pr-shared/agent-utils.js +69 -0
  42. package/dist/phases/pr-shared/context.d.ts +24 -0
  43. package/dist/phases/pr-shared/context.js +78 -0
  44. package/package.json +1 -1
@@ -0,0 +1,10 @@
1
+ /**
2
+ * CLI command: edsger pr-resolve <productId> --pr-url <url>
3
+ * Resolves PR change requests: fixes what's genuinely better, explains what it won't change.
4
+ */
5
+ export interface PRResolveCliOptions {
6
+ prUrl: string;
7
+ prId?: string;
8
+ verbose?: boolean;
9
+ }
10
+ export declare function runPRResolve(productId: string, options: PRResolveCliOptions): Promise<void>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * CLI command: edsger pr-resolve <productId> --pr-url <url>
3
+ * Resolves PR change requests: fixes what's genuinely better, explains what it won't change.
4
+ */
5
+ import { getGitHubConfigByProduct } from '../../api/github.js';
6
+ import { resolveStandalonePR } from '../../phases/pr-resolve/index.js';
7
+ import { logError, logInfo, logSuccess } from '../../utils/logger.js';
8
+ export async function runPRResolve(productId, options) {
9
+ const { prUrl, prId, verbose } = options;
10
+ logInfo(`Starting PR resolve for product ${productId}`);
11
+ logInfo(`PR URL: ${prUrl}`);
12
+ // Get GitHub config via product developer settings
13
+ const githubConfig = await getGitHubConfigByProduct(productId, verbose);
14
+ if (!githubConfig.configured ||
15
+ !githubConfig.token ||
16
+ !githubConfig.owner ||
17
+ !githubConfig.repo) {
18
+ logError(`GitHub not configured for product ${productId}: ${githubConfig.message || 'No installation found'}`);
19
+ process.exit(1);
20
+ }
21
+ const result = await resolveStandalonePR({
22
+ productId,
23
+ pullRequestUrl: prUrl,
24
+ githubToken: githubConfig.token,
25
+ owner: githubConfig.owner,
26
+ repo: githubConfig.repo,
27
+ prId,
28
+ verbose,
29
+ });
30
+ if (result.status === 'success') {
31
+ logSuccess(`PR resolve completed: ${result.message}`);
32
+ if (result.summary) {
33
+ logInfo(`Summary: ${result.summary}`);
34
+ }
35
+ if (result.threadsAddressed !== undefined) {
36
+ logInfo(`Threads addressed: ${result.threadsAddressed}`);
37
+ logInfo(`Threads skipped: ${result.threadsSkipped}`);
38
+ }
39
+ }
40
+ else {
41
+ logError(`PR resolve failed: ${result.message}`);
42
+ process.exit(1);
43
+ }
44
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * CLI command: edsger pr-review <productId> --pr-url <url>
3
+ * Reviews a standalone GitHub PR and posts comments.
4
+ */
5
+ export interface PRReviewCliOptions {
6
+ prUrl: string;
7
+ prId?: string;
8
+ verbose?: boolean;
9
+ }
10
+ export declare function runPRReview(productId: string, options: PRReviewCliOptions): Promise<void>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * CLI command: edsger pr-review <productId> --pr-url <url>
3
+ * Reviews a standalone GitHub PR and posts comments.
4
+ */
5
+ import { getGitHubConfigByProduct } from '../../api/github.js';
6
+ import { reviewStandalonePR } from '../../phases/pr-review/index.js';
7
+ import { logError, logInfo, logSuccess } from '../../utils/logger.js';
8
+ export async function runPRReview(productId, options) {
9
+ const { prUrl, prId, verbose } = options;
10
+ logInfo(`Starting PR review for product ${productId}`);
11
+ logInfo(`PR URL: ${prUrl}`);
12
+ // Get GitHub config via product developer settings
13
+ const githubConfig = await getGitHubConfigByProduct(productId, verbose);
14
+ if (!githubConfig.configured ||
15
+ !githubConfig.token ||
16
+ !githubConfig.owner ||
17
+ !githubConfig.repo) {
18
+ logError(`GitHub not configured for product ${productId}: ${githubConfig.message || 'No installation found'}`);
19
+ process.exit(1);
20
+ }
21
+ const result = await reviewStandalonePR({
22
+ productId,
23
+ pullRequestUrl: prUrl,
24
+ githubToken: githubConfig.token,
25
+ owner: githubConfig.owner,
26
+ repo: githubConfig.repo,
27
+ prId,
28
+ verbose,
29
+ });
30
+ if (result.status === 'success') {
31
+ logSuccess(`PR review completed: ${result.message}`);
32
+ if (result.reviewUrl) {
33
+ logInfo(`Review URL: ${result.reviewUrl}`);
34
+ }
35
+ if (result.commentsCount !== undefined) {
36
+ logInfo(`Comments posted: ${result.commentsCount}`);
37
+ }
38
+ }
39
+ else {
40
+ logError(`PR review failed: ${result.message}`);
41
+ process.exit(1);
42
+ }
43
+ }
package/dist/index.js CHANGED
@@ -18,6 +18,8 @@ import { runInit } from './commands/init/index.js';
18
18
  import { runIntelligence } from './commands/intelligence/index.js';
19
19
  import { runRefactor } from './commands/refactor/refactor.js';
20
20
  import { runTaskWorker } from './commands/task-worker/index.js';
21
+ import { runPRReview } from './commands/pr-review/index.js';
22
+ import { runPRResolve } from './commands/pr-resolve/index.js';
21
23
  import { runWorkflow } from './commands/workflow/index.js';
22
24
  import { logError, logInfo } from './utils/logger.js';
23
25
  // Get package.json version dynamically
@@ -272,6 +274,42 @@ program
272
274
  }
273
275
  });
274
276
  // ============================================================
277
+ // Subcommand: edsger pr-review <productId>
278
+ // ============================================================
279
+ program
280
+ .command('pr-review <productId>')
281
+ .description('AI-review a GitHub PR for a product')
282
+ .requiredOption('--pr-url <url>', 'GitHub PR URL')
283
+ .option('--pr-id <id>', 'Pull request record ID in database')
284
+ .option('-v, --verbose', 'Verbose output')
285
+ .action(async (productId, opts) => {
286
+ try {
287
+ await runPRReview(productId, opts);
288
+ }
289
+ catch (error) {
290
+ logError(error instanceof Error ? error.message : String(error));
291
+ process.exit(1);
292
+ }
293
+ });
294
+ // ============================================================
295
+ // Subcommand: edsger pr-resolve <productId>
296
+ // ============================================================
297
+ program
298
+ .command('pr-resolve <productId>')
299
+ .description('AI-resolve change requests on a GitHub PR')
300
+ .requiredOption('--pr-url <url>', 'GitHub PR URL')
301
+ .option('--pr-id <id>', 'Pull request record ID in database')
302
+ .option('-v, --verbose', 'Verbose output')
303
+ .action(async (productId, opts) => {
304
+ try {
305
+ await runPRResolve(productId, opts);
306
+ }
307
+ catch (error) {
308
+ logError(error instanceof Error ? error.message : String(error));
309
+ process.exit(1);
310
+ }
311
+ });
312
+ // ============================================================
275
313
  // Default command (options-based, backward compatible)
276
314
  // ============================================================
277
315
  program
@@ -0,0 +1,101 @@
1
+ import assert from 'node:assert';
2
+ import { describe, it } from 'node:test';
3
+ import { buildLineToPositionMap, findClosestPosition } from '../diff-utils.js';
4
+ describe('buildLineToPositionMap', () => {
5
+ it('maps simple additions correctly', () => {
6
+ const patch = `@@ -1,3 +1,4 @@
7
+ line 1
8
+ +new line
9
+ line 2
10
+ line 3`;
11
+ const map = buildLineToPositionMap(patch);
12
+ // line 1 is at position 1 (new file line 1)
13
+ assert.strictEqual(map.get(1), 1);
14
+ // new line is at position 2 (new file line 2)
15
+ assert.strictEqual(map.get(2), 2);
16
+ // line 2 is at position 3 (new file line 3)
17
+ assert.strictEqual(map.get(3), 3);
18
+ // line 3 is at position 4 (new file line 4)
19
+ assert.strictEqual(map.get(4), 4);
20
+ });
21
+ it('handles deletions - deleted lines have no new file line number', () => {
22
+ const patch = `@@ -1,3 +1,2 @@
23
+ line 1
24
+ -deleted line
25
+ line 3`;
26
+ const map = buildLineToPositionMap(patch);
27
+ assert.strictEqual(map.get(1), 1);
28
+ // deleted line takes position 2 but no new line
29
+ // line 3 in old file becomes line 2 in new file, position 3
30
+ assert.strictEqual(map.get(2), 3);
31
+ });
32
+ it('handles multiple hunks', () => {
33
+ const patch = `@@ -1,2 +1,2 @@
34
+ line 1
35
+ +added
36
+ @@ -10,2 +10,2 @@
37
+ line 10
38
+ +added at 11`;
39
+ const map = buildLineToPositionMap(patch);
40
+ // First hunk: line 1 -> pos 1, added -> pos 2
41
+ assert.strictEqual(map.get(1), 1);
42
+ assert.strictEqual(map.get(2), 2);
43
+ // Second hunk: line 10 -> pos 3, added at 11 -> pos 4
44
+ assert.strictEqual(map.get(10), 3);
45
+ assert.strictEqual(map.get(11), 4);
46
+ });
47
+ it('returns empty map for patch with only hunk header', () => {
48
+ const patch = '@@ -0,0 +1 @@';
49
+ const map = buildLineToPositionMap(patch);
50
+ // Hunk header only, no content lines
51
+ assert.strictEqual(map.size, 0);
52
+ });
53
+ it('handles hunk starting at line other than 1', () => {
54
+ const patch = `@@ -50,3 +50,3 @@
55
+ context
56
+ -old
57
+ +new`;
58
+ const map = buildLineToPositionMap(patch);
59
+ assert.strictEqual(map.get(50), 1);
60
+ // -old takes position 2 (no new line)
61
+ // +new takes position 3, new file line 51
62
+ assert.strictEqual(map.get(51), 3);
63
+ });
64
+ });
65
+ describe('findClosestPosition', () => {
66
+ it('returns exact match when available', () => {
67
+ const map = new Map([
68
+ [10, 5],
69
+ [11, 6],
70
+ [12, 7],
71
+ ]);
72
+ const result = findClosestPosition(11, map);
73
+ assert.deepStrictEqual(result, { position: 6, actualLine: 11 });
74
+ });
75
+ it('returns nearby line below first', () => {
76
+ const map = new Map([
77
+ [10, 5],
78
+ [15, 8],
79
+ ]);
80
+ // Line 12 not in map, closest below is 15 (offset 3)
81
+ // closest above is 10 (offset 2) - but we check below first at each offset
82
+ // offset 1: check 13 (no), check 11 (no)
83
+ // offset 2: check 14 (no), check 10 (yes!)
84
+ const result = findClosestPosition(12, map);
85
+ assert.deepStrictEqual(result, { position: 5, actualLine: 10 });
86
+ });
87
+ it('returns null when no line within range', () => {
88
+ const map = new Map([
89
+ [1, 1],
90
+ [100, 50],
91
+ ]);
92
+ // Line 50 is too far from both 1 and 100
93
+ const result = findClosestPosition(50, map);
94
+ assert.strictEqual(result, null);
95
+ });
96
+ it('returns null for empty map', () => {
97
+ const map = new Map();
98
+ const result = findClosestPosition(5, map);
99
+ assert.strictEqual(result, null);
100
+ });
101
+ });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Shared diff utilities for mapping file line numbers to GitHub diff positions.
3
+ * Used by both feature-linked code review and standalone PR review.
4
+ */
5
+ /**
6
+ * Parse unified diff patch to build a mapping from file line numbers to diff positions
7
+ *
8
+ * GitHub API uses "position" parameter which is the line number in the unified diff:
9
+ * - Position starts at 1 for the first line AFTER the @@ hunk header
10
+ * - Increments for every line (context, additions, deletions)
11
+ * - Continues through all hunks in the file
12
+ *
13
+ * Example unified diff:
14
+ * ```
15
+ * @@ -1,3 +1,4 @@ <- not counted in position
16
+ * context line <- position 1, file line 1
17
+ * -old line <- position 2, (deleted line, no file line number)
18
+ * +new line <- position 3, file line 2
19
+ * +another new <- position 4, file line 3
20
+ * context <- position 5, file line 4
21
+ * ```
22
+ *
23
+ * Returns: Map<file line number, diff position>
24
+ */
25
+ export declare function buildLineToPositionMap(patch: string): Map<number, number>;
26
+ /**
27
+ * Find the diff position for a target line number, or the closest nearby position
28
+ *
29
+ * @param targetLine - The file line number we want to comment on
30
+ * @param lineToPosition - Map of file line numbers to diff positions
31
+ * @returns Object with position and actual line number, or null if not found
32
+ */
33
+ export declare function findClosestPosition(targetLine: number, lineToPosition: Map<number, number>): {
34
+ position: number;
35
+ actualLine: number;
36
+ } | null;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Shared diff utilities for mapping file line numbers to GitHub diff positions.
3
+ * Used by both feature-linked code review and standalone PR review.
4
+ */
5
+ /**
6
+ * Parse unified diff patch to build a mapping from file line numbers to diff positions
7
+ *
8
+ * GitHub API uses "position" parameter which is the line number in the unified diff:
9
+ * - Position starts at 1 for the first line AFTER the @@ hunk header
10
+ * - Increments for every line (context, additions, deletions)
11
+ * - Continues through all hunks in the file
12
+ *
13
+ * Example unified diff:
14
+ * ```
15
+ * @@ -1,3 +1,4 @@ <- not counted in position
16
+ * context line <- position 1, file line 1
17
+ * -old line <- position 2, (deleted line, no file line number)
18
+ * +new line <- position 3, file line 2
19
+ * +another new <- position 4, file line 3
20
+ * context <- position 5, file line 4
21
+ * ```
22
+ *
23
+ * Returns: Map<file line number, diff position>
24
+ */
25
+ export function buildLineToPositionMap(patch) {
26
+ const lineToPosition = new Map();
27
+ const lines = patch.split('\n');
28
+ let position = 0; // Position in the diff (starts at 1 after first line after @@)
29
+ let currentNewLine = 0; // Current line number in the new file (RIGHT side)
30
+ for (const line of lines) {
31
+ // Parse hunk headers like @@ -1,7 +1,7 @@
32
+ // This extracts the starting line number for the new file (+side)
33
+ const hunkMatch = line.match(/^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
34
+ if (hunkMatch) {
35
+ currentNewLine = parseInt(hunkMatch[1], 10);
36
+ // Don't increment position for the @@ line itself
37
+ continue;
38
+ }
39
+ // Every line after @@ increments the position counter
40
+ position++;
41
+ // Map line numbers based on line type
42
+ if (line.startsWith('+')) {
43
+ // Addition: this line exists in the new file at currentNewLine
44
+ lineToPosition.set(currentNewLine, position);
45
+ currentNewLine++;
46
+ }
47
+ else if (line.startsWith('-')) {
48
+ // Deletion: this line existed in old file, not in new file
49
+ // Position still increments, but currentNewLine does not
50
+ }
51
+ else if (!line.startsWith('\\')) {
52
+ // Context line (no prefix): exists in both old and new file
53
+ lineToPosition.set(currentNewLine, position);
54
+ currentNewLine++;
55
+ }
56
+ // Lines starting with '\' (like "") are metadata
57
+ }
58
+ return lineToPosition;
59
+ }
60
+ /**
61
+ * Find the diff position for a target line number, or the closest nearby position
62
+ *
63
+ * @param targetLine - The file line number we want to comment on
64
+ * @param lineToPosition - Map of file line numbers to diff positions
65
+ * @returns Object with position and actual line number, or null if not found
66
+ */
67
+ export function findClosestPosition(targetLine, lineToPosition) {
68
+ // Check if exact line exists in the diff
69
+ const exactPosition = lineToPosition.get(targetLine);
70
+ if (exactPosition !== undefined) {
71
+ return {
72
+ position: exactPosition,
73
+ actualLine: targetLine,
74
+ };
75
+ }
76
+ // Try to find a nearby line within ยฑ10 lines range
77
+ const range = 10;
78
+ for (let offset = 1; offset <= range; offset++) {
79
+ // Try below first (more likely to be relevant for review comments)
80
+ const lineBelow = targetLine + offset;
81
+ const belowPosition = lineToPosition.get(lineBelow);
82
+ if (belowPosition !== undefined) {
83
+ return {
84
+ position: belowPosition,
85
+ actualLine: lineBelow,
86
+ };
87
+ }
88
+ // Try above
89
+ const lineAbove = targetLine - offset;
90
+ const abovePosition = lineToPosition.get(lineAbove);
91
+ if (abovePosition !== undefined) {
92
+ return {
93
+ position: abovePosition,
94
+ actualLine: lineAbove,
95
+ };
96
+ }
97
+ }
98
+ // No valid position found within range
99
+ return null;
100
+ }
@@ -11,172 +11,36 @@ import { getBaseBranchInfo, getBranches, updateBranch, } from '../../services/br
11
11
  import { formatChecklistsForContext, } from '../../services/checklist.js';
12
12
  import { formatFeedbacksForContext, getFeedbacksForPhase, } from '../../services/feedbacks.js';
13
13
  import { prepareCustomBranchGitEnvironmentAsync, preparePhaseGitEnvironmentAsync, syncFeatBranchWithMain, } from '../../utils/git-branch-manager.js';
14
- import { logDebug, logError, logInfo } from '../../utils/logger.js';
14
+ import { logError, logInfo } from '../../utils/logger.js';
15
+ import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
15
16
  import { fetchCodeReviewContext, formatContextForPrompt, } from './context.js';
16
- function userMessage(content) {
17
- return {
18
- type: 'user',
19
- message: { role: 'user', content },
20
- };
21
- }
22
- async function* prompt(reviewPrompt) {
23
- yield userMessage(reviewPrompt);
24
- await new Promise((res) => {
25
- setTimeout(res, 10000);
26
- });
27
- }
28
- /**
29
- * Parse unified diff patch to build a mapping from file line numbers to diff positions
30
- *
31
- * GitHub API uses "position" parameter which is the line number in the unified diff:
32
- * - Position starts at 1 for the first line AFTER the @@ hunk header
33
- * - Increments for every line (context, additions, deletions)
34
- * - Continues through all hunks in the file
35
- *
36
- * Example unified diff:
37
- * ```
38
- * @@ -1,3 +1,4 @@ <- not counted in position
39
- * context line <- position 1, file line 1
40
- * -old line <- position 2, (deleted line, no file line number)
41
- * +new line <- position 3, file line 2
42
- * +another new <- position 4, file line 3
43
- * context <- position 5, file line 4
44
- * ```
45
- *
46
- * Returns: Map<file line number, diff position>
47
- */
48
- function buildLineToPositionMap(patch) {
49
- const lineToPosition = new Map();
50
- const lines = patch.split('\n');
51
- let position = 0; // Position in the diff (starts at 1 after first line after @@)
52
- let currentNewLine = 0; // Current line number in the new file (RIGHT side)
53
- for (const line of lines) {
54
- // Parse hunk headers like @@ -1,7 +1,7 @@
55
- // This extracts the starting line number for the new file (+side)
56
- const hunkMatch = line.match(/^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
57
- if (hunkMatch) {
58
- currentNewLine = parseInt(hunkMatch[1], 10);
59
- // Don't increment position for the @@ line itself
60
- continue;
61
- }
62
- // Every line after @@ increments the position counter
63
- position++;
64
- // Map line numbers based on line type
65
- if (line.startsWith('+')) {
66
- // Addition: this line exists in the new file at currentNewLine
67
- lineToPosition.set(currentNewLine, position);
68
- currentNewLine++;
69
- }
70
- else if (line.startsWith('-')) {
71
- // Deletion: this line existed in old file, not in new file
72
- // Position still increments, but currentNewLine does not
73
- // We can't comment on this line using file line numbers
74
- }
75
- else if (!line.startsWith('\\')) {
76
- // Context line (no prefix): exists in both old and new file
77
- lineToPosition.set(currentNewLine, position);
78
- currentNewLine++;
79
- }
80
- // Lines starting with '\' (like "") are metadata
81
- }
82
- return lineToPosition;
83
- }
84
- /**
85
- * Find the diff position for a target line number, or the closest nearby position
86
- *
87
- * @param targetLine - The file line number we want to comment on
88
- * @param lineToPosition - Map of file line numbers to diff positions
89
- * @returns Object with position and actual line number, or null if not found
90
- */
91
- function findClosestPosition(targetLine, lineToPosition) {
92
- // Check if exact line exists in the diff
93
- const exactPosition = lineToPosition.get(targetLine);
94
- if (exactPosition !== undefined) {
95
- return {
96
- position: exactPosition,
97
- actualLine: targetLine,
98
- };
99
- }
100
- // Try to find a nearby line within ยฑ10 lines range
101
- const range = 10;
102
- for (let offset = 1; offset <= range; offset++) {
103
- // Try below first (more likely to be relevant for review comments)
104
- const lineBelow = targetLine + offset;
105
- const belowPosition = lineToPosition.get(lineBelow);
106
- if (belowPosition !== undefined) {
107
- return {
108
- position: belowPosition,
109
- actualLine: lineBelow,
110
- };
111
- }
112
- // Try above
113
- const lineAbove = targetLine - offset;
114
- const abovePosition = lineToPosition.get(lineAbove);
115
- if (abovePosition !== undefined) {
116
- return {
117
- position: abovePosition,
118
- actualLine: lineAbove,
119
- };
120
- }
121
- }
122
- // No valid position found within range
123
- return null;
124
- }
125
- /**
126
- * Extract text content from assistant message content array
127
- */
128
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
129
- function extractTextFromContent(content, verbose) {
130
- let text = '';
131
- for (const item of content) {
132
- if (item.type === 'text') {
133
- text += `${item.text}\n`;
134
- logDebug(`๐Ÿ” ${item.text}`, verbose);
135
- }
136
- }
137
- return text;
138
- }
139
- /**
140
- * Parse review result from a JSON response string
141
- */
142
- function tryParseReviewResult(responseText) {
143
- const jsonBlockMatch = responseText.match(/```json\s*\n([\s\S]*?)\n\s*```/);
144
- const jsonResult = jsonBlockMatch
145
- ? JSON.parse(jsonBlockMatch[1])
146
- : JSON.parse(responseText);
147
- if (jsonResult && jsonResult.review_result) {
148
- return jsonResult.review_result;
149
- }
150
- throw new Error('Invalid JSON structure');
151
- }
17
+ import { buildLineToPositionMap, findClosestPosition } from './diff-utils.js';
152
18
  /**
153
19
  * Process a query result message and extract the structured review result
154
20
  */
155
21
  function parseQueryResultMessage(message, lastAssistantResponse) {
156
22
  if (message.subtype === 'success') {
157
- logInfo('\n๐Ÿ› ๏ธ Code review analysis completed, parsing results...');
158
- try {
159
- return tryParseReviewResult(message.result || lastAssistantResponse);
160
- }
161
- catch (error) {
162
- logError(`Failed to parse structured review result: ${error}`);
163
- return parseCodeReviewResponse(message.result || lastAssistantResponse);
23
+ logInfo('\n Code review analysis completed, parsing results...');
24
+ const responseText = message.result || lastAssistantResponse;
25
+ const result = tryExtractResult(responseText, 'review_result');
26
+ if (result) {
27
+ return result;
164
28
  }
29
+ logError('Failed to parse structured review result');
30
+ return parseCodeReviewResponse(responseText);
165
31
  }
166
- logError(`\nโš ๏ธ Code review incomplete: ${message.subtype}`);
32
+ logError(`\n Code review incomplete: ${message.subtype}`);
167
33
  if (message.subtype === 'error_max_turns') {
168
- logError('๐Ÿ’ก Try simplifying the review scope');
34
+ logError('Try simplifying the review scope');
169
35
  }
170
36
  if (!lastAssistantResponse) {
171
37
  return null;
172
38
  }
173
- try {
174
- return tryParseReviewResult(lastAssistantResponse);
175
- }
176
- catch (error) {
177
- logError(`Failed to parse assistant response: ${error}`);
178
- return parseCodeReviewResponse(lastAssistantResponse);
39
+ const result = tryExtractResult(lastAssistantResponse, 'review_result');
40
+ if (result) {
41
+ return result;
179
42
  }
43
+ return parseCodeReviewResponse(lastAssistantResponse);
180
44
  }
181
45
  /**
182
46
  * Log base branch rebase info for verbose mode
@@ -374,7 +238,7 @@ export const reviewPullRequest = async (options, config, checklistContext
374
238
  // Use Claude to analyze the code (not using Claude Code SDK for actual implementation)
375
239
  // This is a simplified approach - just analyze the code
376
240
  for await (const message of query({
377
- prompt: prompt(reviewPrompt),
241
+ prompt: createPromptGenerator(reviewPrompt),
378
242
  options: {
379
243
  systemPrompt: {
380
244
  type: 'preset',
@@ -38,6 +38,18 @@ export declare function removeDeletedFilesFromPRs(changedFiles: ChangedFileInfo[
38
38
  change_type: string;
39
39
  }[];
40
40
  }, verbose?: boolean) => Promise<unknown>, verbose?: boolean): Promise<number>;
41
+ /**
42
+ * Remove files from PR file lists that are no longer in the current diff.
43
+ * Used after rebase when the full diff against main may differ from what
44
+ * was previously recorded in PR file lists.
45
+ * Returns the number of PRs updated.
46
+ */
47
+ export declare function removeStaleFilesFromPRs(changedFiles: ChangedFileInfo[], pullRequests: PullRequest[], updater?: (prId: string, updates: {
48
+ files: {
49
+ path: string;
50
+ change_type: string;
51
+ }[];
52
+ }, verbose?: boolean) => Promise<unknown>, verbose?: boolean): Promise<number>;
41
53
  /**
42
54
  * Detect unassigned files and use LLM to assign them to existing PRs.
43
55
  * Returns the number of files assigned, or 0 if none needed assignment.
@@ -194,6 +194,40 @@ export async function removeDeletedFilesFromPRs(changedFiles, pullRequests, upda
194
194
  }
195
195
  return updatedCount;
196
196
  }
197
+ /**
198
+ * Remove files from PR file lists that are no longer in the current diff.
199
+ * Used after rebase when the full diff against main may differ from what
200
+ * was previously recorded in PR file lists.
201
+ * Returns the number of PRs updated.
202
+ */
203
+ export async function removeStaleFilesFromPRs(changedFiles, pullRequests, updater = updatePullRequest, verbose) {
204
+ const currentPaths = new Set(changedFiles.map((f) => f.path));
205
+ let updatedCount = 0;
206
+ for (const pr of pullRequests) {
207
+ if (!pr.files || pr.files.length === 0) {
208
+ continue;
209
+ }
210
+ const filtered = pr.files.filter((f) => currentPaths.has(f.path));
211
+ if (filtered.length === pr.files.length) {
212
+ continue;
213
+ }
214
+ const removed = pr.files.length - filtered.length;
215
+ try {
216
+ await updater(pr.id, { files: filtered }, verbose);
217
+ updatedCount++;
218
+ if (verbose) {
219
+ logInfo(` Cleaned PR "${pr.name}": removed ${removed} stale file(s)`);
220
+ }
221
+ }
222
+ catch (error) {
223
+ logError(`Failed to clean PR ${pr.id}: ${error instanceof Error ? error.message : String(error)}`);
224
+ }
225
+ }
226
+ if (verbose && updatedCount > 0) {
227
+ logInfo(`๐Ÿงน Removed stale files from ${updatedCount} PR(s)`);
228
+ }
229
+ return updatedCount;
230
+ }
197
231
  /**
198
232
  * Detect unassigned files and use LLM to assign them to existing PRs.
199
233
  * Returns the number of files assigned, or 0 if none needed assignment.