edsger 0.41.1 → 0.41.3

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 (127) hide show
  1. package/.claude/settings.local.json +23 -3
  2. package/.env.local +12 -0
  3. package/dist/api/features/__tests__/regression-prevention.test.d.ts +5 -0
  4. package/dist/api/features/__tests__/regression-prevention.test.js +338 -0
  5. package/dist/api/features/__tests__/status-updater.integration.test.d.ts +5 -0
  6. package/dist/api/features/__tests__/status-updater.integration.test.js +497 -0
  7. package/dist/commands/workflow/executors/phase-executor.js +1 -3
  8. package/dist/commands/workflow/phase-orchestrator.js +1 -3
  9. package/dist/commands/workflow/pipeline-runner.d.ts +17 -0
  10. package/dist/commands/workflow/pipeline-runner.js +393 -0
  11. package/dist/commands/workflow/runner.d.ts +26 -0
  12. package/dist/commands/workflow/runner.js +119 -0
  13. package/dist/commands/workflow/workflow-runner.d.ts +26 -0
  14. package/dist/commands/workflow/workflow-runner.js +119 -0
  15. package/dist/index.js +2 -2
  16. package/dist/phases/app-store-generation/index.js +1 -3
  17. package/dist/phases/branch-planning/index.js +1 -3
  18. package/dist/phases/bug-fixing/analyzer.js +1 -3
  19. package/dist/phases/code-implementation/analyzer-helpers.d.ts +28 -0
  20. package/dist/phases/code-implementation/analyzer-helpers.js +177 -0
  21. package/dist/phases/code-implementation/analyzer.d.ts +32 -0
  22. package/dist/phases/code-implementation/analyzer.js +629 -0
  23. package/dist/phases/code-implementation/context-fetcher.d.ts +17 -0
  24. package/dist/phases/code-implementation/context-fetcher.js +86 -0
  25. package/dist/phases/code-implementation/index.js +1 -3
  26. package/dist/phases/code-implementation/mcp-server.d.ts +1 -0
  27. package/dist/phases/code-implementation/mcp-server.js +93 -0
  28. package/dist/phases/code-implementation/prompts-improvement.d.ts +5 -0
  29. package/dist/phases/code-implementation/prompts-improvement.js +108 -0
  30. package/dist/phases/code-implementation-verification/verifier.d.ts +31 -0
  31. package/dist/phases/code-implementation-verification/verifier.js +196 -0
  32. package/dist/phases/code-refine/analyzer.d.ts +41 -0
  33. package/dist/phases/code-refine/analyzer.js +561 -0
  34. package/dist/phases/code-refine/context-fetcher.d.ts +94 -0
  35. package/dist/phases/code-refine/context-fetcher.js +423 -0
  36. package/dist/phases/code-refine/index.js +1 -3
  37. package/dist/phases/code-refine-verification/analysis/llm-analyzer.d.ts +22 -0
  38. package/dist/phases/code-refine-verification/analysis/llm-analyzer.js +134 -0
  39. package/dist/phases/code-refine-verification/verifier.d.ts +47 -0
  40. package/dist/phases/code-refine-verification/verifier.js +597 -0
  41. package/dist/phases/code-review/analyzer.d.ts +29 -0
  42. package/dist/phases/code-review/analyzer.js +363 -0
  43. package/dist/phases/code-review/context-fetcher.d.ts +92 -0
  44. package/dist/phases/code-review/context-fetcher.js +296 -0
  45. package/dist/phases/code-review/index.js +1 -3
  46. package/dist/phases/code-testing/analyzer.js +1 -3
  47. package/dist/phases/feature-analysis/analyzer-helpers.d.ts +10 -0
  48. package/dist/phases/feature-analysis/analyzer-helpers.js +47 -0
  49. package/dist/phases/feature-analysis/analyzer.d.ts +11 -0
  50. package/dist/phases/feature-analysis/analyzer.js +208 -0
  51. package/dist/phases/feature-analysis/context-fetcher.d.ts +26 -0
  52. package/dist/phases/feature-analysis/context-fetcher.js +134 -0
  53. package/dist/phases/feature-analysis/http-fallback.d.ts +20 -0
  54. package/dist/phases/feature-analysis/http-fallback.js +95 -0
  55. package/dist/phases/feature-analysis/index.js +1 -3
  56. package/dist/phases/feature-analysis/mcp-server.d.ts +1 -0
  57. package/dist/phases/feature-analysis/mcp-server.js +144 -0
  58. package/dist/phases/feature-analysis/prompts-improvement.d.ts +8 -0
  59. package/dist/phases/feature-analysis/prompts-improvement.js +109 -0
  60. package/dist/phases/feature-analysis-verification/verifier.d.ts +37 -0
  61. package/dist/phases/feature-analysis-verification/verifier.js +147 -0
  62. package/dist/phases/functional-testing/analyzer.js +1 -3
  63. package/dist/phases/growth-analysis/index.js +1 -3
  64. package/dist/phases/pr-execution/file-assigner.js +20 -12
  65. package/dist/phases/pr-execution/index.js +59 -1
  66. package/dist/phases/pr-resolve/prompts.js +2 -1
  67. package/dist/phases/pr-resolve/workspace.d.ts +1 -1
  68. package/dist/phases/pr-resolve/workspace.js +1 -1
  69. package/dist/phases/pr-review/__tests__/review-comments.test.js +4 -2
  70. package/dist/phases/pr-review/index.js +1 -3
  71. package/dist/phases/pr-shared/agent-utils.js +0 -1
  72. package/dist/phases/pr-shared/context.d.ts +1 -1
  73. package/dist/phases/pr-splitting/index.js +1 -3
  74. package/dist/phases/technical-design/analyzer-helpers.d.ts +25 -0
  75. package/dist/phases/technical-design/analyzer-helpers.js +39 -0
  76. package/dist/phases/technical-design/analyzer.d.ts +21 -0
  77. package/dist/phases/technical-design/analyzer.js +461 -0
  78. package/dist/phases/technical-design/context-fetcher.d.ts +12 -0
  79. package/dist/phases/technical-design/context-fetcher.js +39 -0
  80. package/dist/phases/technical-design/http-fallback.d.ts +17 -0
  81. package/dist/phases/technical-design/http-fallback.js +151 -0
  82. package/dist/phases/technical-design/index.js +1 -3
  83. package/dist/phases/technical-design/mcp-server.d.ts +1 -0
  84. package/dist/phases/technical-design/mcp-server.js +157 -0
  85. package/dist/phases/technical-design/prompts-improvement.d.ts +5 -0
  86. package/dist/phases/technical-design/prompts-improvement.js +93 -0
  87. package/dist/phases/technical-design-verification/verifier.d.ts +53 -0
  88. package/dist/phases/technical-design-verification/verifier.js +170 -0
  89. package/dist/phases/test-cases-analysis/index.js +1 -3
  90. package/dist/phases/user-stories-analysis/index.js +1 -3
  91. package/dist/services/feature-branches.d.ts +77 -0
  92. package/dist/services/feature-branches.js +205 -0
  93. package/dist/workflow-runner/config/phase-configs.d.ts +5 -0
  94. package/dist/workflow-runner/config/phase-configs.js +120 -0
  95. package/dist/workflow-runner/core/feature-filter.d.ts +16 -0
  96. package/dist/workflow-runner/core/feature-filter.js +46 -0
  97. package/dist/workflow-runner/core/index.d.ts +8 -0
  98. package/dist/workflow-runner/core/index.js +12 -0
  99. package/dist/workflow-runner/core/pipeline-evaluator.d.ts +24 -0
  100. package/dist/workflow-runner/core/pipeline-evaluator.js +32 -0
  101. package/dist/workflow-runner/core/state-manager.d.ts +24 -0
  102. package/dist/workflow-runner/core/state-manager.js +42 -0
  103. package/dist/workflow-runner/core/workflow-logger.d.ts +20 -0
  104. package/dist/workflow-runner/core/workflow-logger.js +65 -0
  105. package/dist/workflow-runner/executors/phase-executor.d.ts +8 -0
  106. package/dist/workflow-runner/executors/phase-executor.js +248 -0
  107. package/dist/workflow-runner/feature-workflow-runner.d.ts +26 -0
  108. package/dist/workflow-runner/feature-workflow-runner.js +119 -0
  109. package/dist/workflow-runner/index.d.ts +2 -0
  110. package/dist/workflow-runner/index.js +2 -0
  111. package/dist/workflow-runner/pipeline-runner.d.ts +17 -0
  112. package/dist/workflow-runner/pipeline-runner.js +393 -0
  113. package/dist/workflow-runner/workflow-processor.d.ts +54 -0
  114. package/dist/workflow-runner/workflow-processor.js +170 -0
  115. package/package.json +1 -1
  116. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +0 -4
  117. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +0 -133
  118. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +0 -4
  119. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +0 -336
  120. package/dist/services/lifecycle-agent/index.d.ts +0 -24
  121. package/dist/services/lifecycle-agent/index.js +0 -25
  122. package/dist/services/lifecycle-agent/phase-criteria.d.ts +0 -57
  123. package/dist/services/lifecycle-agent/phase-criteria.js +0 -335
  124. package/dist/services/lifecycle-agent/transition-rules.d.ts +0 -60
  125. package/dist/services/lifecycle-agent/transition-rules.js +0 -184
  126. package/dist/services/lifecycle-agent/types.d.ts +0 -190
  127. package/dist/services/lifecycle-agent/types.js +0 -12
@@ -0,0 +1,597 @@
1
+ /**
2
+ * Code Refine Verification
3
+ * Verifies that all PR review comments have been addressed and resolves them
4
+ * Uses GitHub GraphQL API to accurately detect unresolved review threads
5
+ */
6
+ import { Octokit } from '@octokit/rest';
7
+ import { query } from '@anthropic-ai/claude-code';
8
+ import { logInfo, logError } from '../../utils/logger.js';
9
+ import { parsePullRequestUrl, fetchPRReviews, } from '../code-refine/context.js';
10
+ import { getFeature } from '../../api/features/get-feature.js';
11
+ /**
12
+ * Fetch complete file content from a specific ref (branch/commit)
13
+ */
14
+ async function fetchFileContent(octokit, owner, repo, ref, path, verbose) {
15
+ try {
16
+ const { data } = await octokit.repos.getContent({
17
+ owner,
18
+ repo,
19
+ ref,
20
+ path,
21
+ });
22
+ // Handle file content (not directory)
23
+ if ('content' in data && !Array.isArray(data)) {
24
+ // Content is base64 encoded
25
+ return Buffer.from(data.content, 'base64').toString('utf-8');
26
+ }
27
+ return undefined;
28
+ }
29
+ catch (error) {
30
+ if (verbose) {
31
+ logError(`Failed to fetch content for ${path} at ref ${ref}: ${error}`);
32
+ }
33
+ return undefined;
34
+ }
35
+ }
36
+ /**
37
+ * Fetch PR file changes (diff information and full content)
38
+ */
39
+ async function fetchPRFileChanges(octokit, owner, repo, prNumber, verbose) {
40
+ try {
41
+ if (verbose) {
42
+ logInfo('📂 Fetching PR file changes and content...');
43
+ }
44
+ // Get PR details to get the head ref
45
+ const { data: pr } = await octokit.pulls.get({
46
+ owner,
47
+ repo,
48
+ pull_number: prNumber,
49
+ });
50
+ const headRef = pr.head.sha;
51
+ // Get list of changed files
52
+ const { data: files } = await octokit.pulls.listFiles({
53
+ owner,
54
+ repo,
55
+ pull_number: prNumber,
56
+ per_page: 100,
57
+ });
58
+ if (verbose) {
59
+ logInfo(`✅ Found ${files.length} changed files`);
60
+ logInfo(`📥 Fetching full content for changed files from ${headRef.substring(0, 7)}...`);
61
+ }
62
+ // Fetch full content for each file in parallel
63
+ const fileChanges = await Promise.all(files.map(async (file) => {
64
+ let fullContent;
65
+ // Only fetch content for files that were not deleted
66
+ if (file.status !== 'removed') {
67
+ fullContent = await fetchFileContent(octokit, owner, repo, headRef, file.filename, verbose);
68
+ }
69
+ return {
70
+ filename: file.filename,
71
+ status: file.status,
72
+ additions: file.additions,
73
+ deletions: file.deletions,
74
+ changes: file.changes,
75
+ patch: file.patch,
76
+ fullContent,
77
+ };
78
+ }));
79
+ if (verbose) {
80
+ const filesWithContent = fileChanges.filter((f) => f.fullContent).length;
81
+ logInfo(`✅ Retrieved full content for ${filesWithContent}/${fileChanges.length} files`);
82
+ }
83
+ return fileChanges;
84
+ }
85
+ catch (error) {
86
+ logError(`Failed to fetch PR file changes: ${error}`);
87
+ return [];
88
+ }
89
+ }
90
+ /**
91
+ * Fetch unresolved review threads using GitHub GraphQL API
92
+ * This provides accurate resolution status unlike REST API
93
+ */
94
+ async function fetchUnresolvedReviewThreads(octokit, owner, repo, prNumber, verbose) {
95
+ try {
96
+ if (verbose) {
97
+ logInfo('🔍 Fetching review threads via GraphQL API...');
98
+ }
99
+ const query = `
100
+ query($owner: String!, $repo: String!, $prNumber: Int!) {
101
+ repository(owner: $owner, name: $repo) {
102
+ pullRequest(number: $prNumber) {
103
+ reviewThreads(first: 100) {
104
+ nodes {
105
+ id
106
+ isResolved
107
+ isOutdated
108
+ comments(first: 100) {
109
+ totalCount
110
+ nodes {
111
+ id
112
+ author {
113
+ login
114
+ }
115
+ body
116
+ path
117
+ line
118
+ url
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ `;
127
+ const result = await octokit.graphql(query, {
128
+ owner,
129
+ repo,
130
+ prNumber,
131
+ });
132
+ const allThreads = result?.repository?.pullRequest?.reviewThreads?.nodes || [];
133
+ // Filter for truly unresolved threads
134
+ // - Exclude resolved threads (isResolved = true)
135
+ // - Exclude outdated threads (isOutdated = true) - these mean code has changed, should auto-resolve
136
+ const unresolvedThreads = allThreads.filter((thread) => !thread.isResolved && !thread.isOutdated);
137
+ // Separate outdated threads that should be auto-resolved
138
+ const outdatedThreads = allThreads.filter((thread) => !thread.isResolved && thread.isOutdated);
139
+ if (verbose) {
140
+ logInfo(`📊 Found ${unresolvedThreads.length} unresolved review threads (out of ${allThreads.length} total)`);
141
+ if (outdatedThreads.length > 0) {
142
+ logInfo(`📊 Found ${outdatedThreads.length} outdated threads (code changed, will auto-resolve)`);
143
+ }
144
+ }
145
+ // Auto-resolve outdated threads
146
+ if (outdatedThreads.length > 0) {
147
+ const markedCount = await resolveReviewThreads(octokit, outdatedThreads, verbose);
148
+ if (verbose) {
149
+ logInfo(`✅ Auto-resolved ${markedCount} outdated threads`);
150
+ }
151
+ }
152
+ return unresolvedThreads;
153
+ }
154
+ catch (error) {
155
+ logError(`Failed to fetch review threads via GraphQL: ${error}`);
156
+ // Fallback to empty array if GraphQL fails
157
+ return [];
158
+ }
159
+ }
160
+ /**
161
+ * Analyze whether a review thread has been addressed by examining code changes
162
+ * Uses LLM to intelligently determine if the feedback was addressed
163
+ */
164
+ async function analyzeThreadWithLLM(thread, fileChange, config, verbose) {
165
+ const firstComment = thread.comments.nodes[0];
166
+ if (!firstComment) {
167
+ return {
168
+ isAddressed: false,
169
+ reason: 'Comment thread exists but has no comments',
170
+ };
171
+ }
172
+ // If file was not changed at all, feedback definitely not addressed
173
+ if (!fileChange || !fileChange.patch) {
174
+ return {
175
+ isAddressed: false,
176
+ reason: `File ${firstComment.path} has not been modified in this PR`,
177
+ };
178
+ }
179
+ // If file was deleted, thread should be resolved as obsolete
180
+ if (fileChange.status === 'removed') {
181
+ return {
182
+ isAddressed: true,
183
+ reason: 'File has been removed',
184
+ };
185
+ }
186
+ try {
187
+ if (verbose) {
188
+ logInfo(`🤖 Using LLM to analyze if comment in ${firstComment.path}:${firstComment.line} has been addressed...`);
189
+ }
190
+ const analysisPrompt = createThreadAnalysisPrompt(thread, fileChange, firstComment);
191
+ let lastResponse = '';
192
+ let analysisResult = null;
193
+ function* userMessage() {
194
+ yield {
195
+ type: 'user',
196
+ message: { role: 'user', content: analysisPrompt },
197
+ };
198
+ }
199
+ for await (const message of query({
200
+ prompt: userMessage(),
201
+ options: {
202
+ model: config.claude.model || 'sonnet',
203
+ maxTurns: 10,
204
+ permissionMode: 'bypassPermissions',
205
+ },
206
+ })) {
207
+ if (message.type === 'assistant' && message.message?.content) {
208
+ for (const content of message.message.content) {
209
+ if (content.type === 'text') {
210
+ lastResponse += content.text + '\n';
211
+ }
212
+ }
213
+ }
214
+ if (message.type === 'result') {
215
+ if (message.subtype === 'success') {
216
+ const responseText = message.result || lastResponse;
217
+ // Try to extract JSON from response
218
+ const jsonMatch = responseText.match(/```json\s*\n([\s\S]*?)\n\s*```/);
219
+ if (jsonMatch) {
220
+ analysisResult = JSON.parse(jsonMatch[1]);
221
+ }
222
+ else {
223
+ // Try to parse directly
224
+ try {
225
+ analysisResult = JSON.parse(responseText);
226
+ }
227
+ catch {
228
+ // Fallback: look for isAddressed boolean in text
229
+ const isAddressedMatch = /isAddressed["']?\s*:\s*(true|false)/i.exec(responseText);
230
+ if (isAddressedMatch) {
231
+ analysisResult = {
232
+ isAddressed: isAddressedMatch[1].toLowerCase() === 'true',
233
+ reason: responseText.split('\n').find((line) => line.trim()) ||
234
+ 'Analysis completed',
235
+ };
236
+ }
237
+ }
238
+ }
239
+ }
240
+ }
241
+ }
242
+ if (analysisResult) {
243
+ if (verbose) {
244
+ logInfo(` ${analysisResult.isAddressed ? '✅' : '❌'} ${analysisResult.reason}`);
245
+ }
246
+ return analysisResult;
247
+ }
248
+ // Fallback if LLM analysis failed
249
+ return {
250
+ isAddressed: false,
251
+ reason: 'Unable to analyze - please review manually',
252
+ };
253
+ }
254
+ catch (error) {
255
+ if (verbose) {
256
+ logError(`LLM analysis failed: ${error}`);
257
+ }
258
+ return {
259
+ isAddressed: false,
260
+ reason: 'Analysis failed - please review manually',
261
+ };
262
+ }
263
+ }
264
+ /**
265
+ * Create prompt for LLM to analyze if a comment thread has been addressed
266
+ */
267
+ function createThreadAnalysisPrompt(thread, fileChange, firstComment) {
268
+ const allComments = thread.comments.nodes
269
+ .map((c, idx) => `Comment ${idx + 1} by @${c.author.login}:\n${c.body}`)
270
+ .join('\n\n');
271
+ // Build the code context section
272
+ let codeContext = '';
273
+ // Include diff (what changed)
274
+ if (fileChange.patch) {
275
+ codeContext += `**Code Changes (Diff):**
276
+ \`\`\`diff
277
+ ${fileChange.patch}
278
+ \`\`\`
279
+
280
+ `;
281
+ }
282
+ // Include full file content for better context
283
+ if (fileChange.fullContent) {
284
+ // Truncate very large files to avoid context limits
285
+ const maxLength = 10000;
286
+ const content = fileChange.fullContent.length > maxLength
287
+ ? fileChange.fullContent.substring(0, maxLength) +
288
+ '\n\n... (file truncated, showing first 10000 characters)'
289
+ : fileChange.fullContent;
290
+ codeContext += `**Complete File Content (After Changes):**
291
+ \`\`\`
292
+ ${content}
293
+ \`\`\`
294
+
295
+ `;
296
+ }
297
+ if (!codeContext) {
298
+ codeContext = '(No code information available)\n\n';
299
+ }
300
+ return `You are analyzing whether a code review comment has been addressed by subsequent code changes.
301
+
302
+ **Review Thread Information:**
303
+ - File: ${firstComment.path}
304
+ - Line: ${firstComment.line || 'N/A'}
305
+ - Thread has ${thread.comments.totalCount} comment(s)
306
+
307
+ **Review Comments:**
308
+ ${allComments}
309
+
310
+ ${codeContext}
311
+
312
+ **Your Task:**
313
+ Analyze whether the code changes adequately address the feedback in the review comments. You have access to:
314
+ 1. **The diff** showing what was changed
315
+ 2. **The complete file content** after changes for full context
316
+
317
+ Consider:
318
+ 1. Does the feedback request a specific code change?
319
+ 2. Have those changes (or equivalent changes) been made?
320
+ 3. If the feedback points to a specific line, use the full file content to understand the broader context
321
+ 4. Check if related areas of code were modified to address the concern
322
+ 5. Use the complete file to verify the fix is properly integrated
323
+
324
+ **Important:**
325
+ - The comment may point to a specific line, but the fix might be in nearby code
326
+ - Focus on whether the **underlying issue** was addressed, not just whether that exact line was modified
327
+ - Use the full file content to understand the complete context and verify the implementation
328
+
329
+ Return your analysis in this JSON format:
330
+ \`\`\`json
331
+ {
332
+ "isAddressed": true or false,
333
+ "reason": "Brief explanation of why you determined the feedback was or was not addressed"
334
+ }
335
+ \`\`\`
336
+
337
+ Example responses:
338
+ - If a comment says "add null check" and the code shows a null check was added: {"isAddressed": true, "reason": "Null check added in the updated code"}
339
+ - If a comment suggests a refactor but no relevant changes are visible: {"isAddressed": false, "reason": "No refactoring changes visible in diff or file content"}
340
+ - If code was modified in related areas addressing the concern: {"isAddressed": true, "reason": "Related code modified to address the concern"}
341
+ - If the full context shows the issue is resolved differently: {"isAddressed": true, "reason": "Issue resolved through alternative implementation visible in full file"}
342
+ `;
343
+ }
344
+ /**
345
+ * Mark review threads as resolved using GraphQL API
346
+ */
347
+ async function resolveReviewThreads(octokit, threads, verbose) {
348
+ let markedCount = 0;
349
+ for (const thread of threads) {
350
+ try {
351
+ if (verbose) {
352
+ logInfo(`✅ Marking thread ${thread.id} as resolved...`);
353
+ }
354
+ const mutation = `
355
+ mutation($threadId: ID!) {
356
+ resolveReviewThread(input: {threadId: $threadId}) {
357
+ thread {
358
+ id
359
+ isResolved
360
+ }
361
+ }
362
+ }
363
+ `;
364
+ await octokit.graphql(mutation, {
365
+ threadId: thread.id,
366
+ });
367
+ markedCount++;
368
+ if (verbose) {
369
+ logInfo(`✅ Thread ${thread.id} marked as resolved`);
370
+ }
371
+ }
372
+ catch (error) {
373
+ if (verbose) {
374
+ logError(`Failed to resolve thread ${thread.id}: ${error}`);
375
+ }
376
+ }
377
+ }
378
+ return markedCount;
379
+ }
380
+ /**
381
+ * Verify and resolve PR review comments using GraphQL API and LLM analysis
382
+ */
383
+ export async function verifyAndResolveComments(options) {
384
+ const { featureId, githubToken, config, verbose } = options;
385
+ if (verbose) {
386
+ logInfo(`Starting code refine verification for feature ID: ${featureId}`);
387
+ }
388
+ try {
389
+ // Fetch feature info using shared API
390
+ const feature = await getFeature(featureId, verbose);
391
+ if (!feature.pull_request_url) {
392
+ throw new Error(`Feature ${featureId} does not have a pull request URL. Cannot perform verification.`);
393
+ }
394
+ // Parse PR URL
395
+ const prInfo = parsePullRequestUrl(feature.pull_request_url);
396
+ if (!prInfo) {
397
+ throw new Error(`Invalid pull request URL: ${feature.pull_request_url}. Expected format: https://github.com/owner/repo/pull/123`);
398
+ }
399
+ const { owner, repo, prNumber } = prInfo;
400
+ // Initialize Octokit with GitHub token (supports both REST and GraphQL)
401
+ const octokit = new Octokit({
402
+ auth: githubToken,
403
+ });
404
+ // Fetch unresolved review threads, reviews, and file changes
405
+ const [unresolvedThreads, reviews, fileChanges] = await Promise.all([
406
+ fetchUnresolvedReviewThreads(octokit, owner, repo, prNumber, verbose),
407
+ fetchPRReviews(octokit, owner, repo, prNumber, verbose),
408
+ fetchPRFileChanges(octokit, owner, repo, prNumber, verbose),
409
+ ]);
410
+ // Check if there's anything to verify
411
+ if (reviews.length === 0 && unresolvedThreads.length === 0) {
412
+ if (verbose) {
413
+ logInfo('✅ No reviews or unresolved review threads found. Verification complete.');
414
+ }
415
+ return {
416
+ status: 'success',
417
+ message: 'No reviews or review comments to verify',
418
+ data: {
419
+ featureId,
420
+ totalReviews: 0,
421
+ unresolvedReviews: 0,
422
+ totalComments: 0,
423
+ resolvedComments: 0,
424
+ unresolvedComments: 0,
425
+ },
426
+ };
427
+ }
428
+ // Use LLM to intelligently analyze unresolved threads
429
+ if (verbose && unresolvedThreads.length > 0) {
430
+ logInfo(`🔍 Analyzing ${unresolvedThreads.length} unresolved threads with LLM...`);
431
+ }
432
+ // Analyze each thread with LLM to determine if it's truly unresolved
433
+ const threadAnalysisResults = await Promise.all(unresolvedThreads.map(async (thread) => {
434
+ const firstComment = thread.comments.nodes[0];
435
+ const fileChange = fileChanges.find((fc) => fc.filename === firstComment?.path);
436
+ const analysis = await analyzeThreadWithLLM(thread, fileChange, config, verbose);
437
+ return {
438
+ thread,
439
+ analysis,
440
+ };
441
+ }));
442
+ // Separate threads that LLM determined are addressed vs not addressed
443
+ const addressedThreads = threadAnalysisResults.filter((result) => result.analysis.isAddressed);
444
+ const trulyUnresolvedThreads = threadAnalysisResults.filter((result) => !result.analysis.isAddressed);
445
+ if (verbose) {
446
+ logInfo(`📊 LLM Analysis: ${addressedThreads.length} threads addressed, ${trulyUnresolvedThreads.length} still need attention`);
447
+ }
448
+ // Auto-resolve threads that LLM determined are addressed
449
+ if (addressedThreads.length > 0) {
450
+ if (verbose) {
451
+ logInfo(`✅ Auto-resolving ${addressedThreads.length} threads that have been addressed...`);
452
+ }
453
+ const resolvedCount = await resolveReviewThreads(octokit, addressedThreads.map((r) => r.thread), verbose);
454
+ if (verbose) {
455
+ logInfo(`✅ Successfully resolved ${resolvedCount} threads`);
456
+ }
457
+ }
458
+ // Check reviews - they need to be dismissed or re-reviewed
459
+ const unresolvedReviews = reviews.filter((review) => review.state === 'CHANGES_REQUESTED');
460
+ if (verbose) {
461
+ logInfo(`📊 Review Threads: ${trulyUnresolvedThreads.length} still unresolved (after LLM analysis)`);
462
+ if (reviews.length > 0) {
463
+ logInfo(`📊 Reviews: ${reviews.length - unresolvedReviews.length} addressed, ${unresolvedReviews.length} still requesting changes`);
464
+ }
465
+ }
466
+ // If all threads are truly resolved (after LLM analysis) AND no reviews requesting changes, success
467
+ if (trulyUnresolvedThreads.length === 0 && unresolvedReviews.length === 0) {
468
+ if (verbose) {
469
+ logInfo('✅ All comments have been addressed! All review threads are resolved.');
470
+ }
471
+ const successMessage = reviews.length > 0
472
+ ? 'All reviews and review comments have been addressed and resolved'
473
+ : 'All review comments have been addressed and resolved';
474
+ return {
475
+ status: 'success',
476
+ message: successMessage,
477
+ data: {
478
+ featureId,
479
+ totalReviews: reviews.length,
480
+ unresolvedReviews: 0,
481
+ totalComments: unresolvedThreads.length,
482
+ resolvedComments: addressedThreads.length,
483
+ unresolvedComments: 0,
484
+ commentsMarkedResolved: addressedThreads.length,
485
+ },
486
+ };
487
+ }
488
+ else {
489
+ // Verification failed - build detailed info with specific failure reasons from LLM analysis
490
+ if (verbose) {
491
+ if (unresolvedReviews.length > 0) {
492
+ logInfo(`⚠️ ${unresolvedReviews.length} reviews still requesting changes`);
493
+ unresolvedReviews.forEach((review) => {
494
+ logInfo(` - Review ${review.id} by @${review.user.login}`);
495
+ if (review.body) {
496
+ logInfo(` ${review.body.substring(0, 100)}...`);
497
+ }
498
+ });
499
+ }
500
+ if (trulyUnresolvedThreads.length > 0) {
501
+ logInfo(`⚠️ ${trulyUnresolvedThreads.length} review threads still need to be addressed`);
502
+ trulyUnresolvedThreads.forEach((result) => {
503
+ const firstComment = result.thread.comments.nodes[0];
504
+ if (firstComment) {
505
+ logInfo(` - Comment by @${firstComment.author.login}`);
506
+ logInfo(` File: ${firstComment.path}:${firstComment.line || '?'}`);
507
+ logInfo(` ${firstComment.body.substring(0, 100)}...`);
508
+ logInfo(` LLM Analysis: ${result.analysis.reason}`);
509
+ }
510
+ });
511
+ }
512
+ }
513
+ // Build targeted suggestions based on LLM analysis
514
+ const suggestions = [];
515
+ // Create specific suggestions for each truly unresolved comment (based on LLM analysis)
516
+ trulyUnresolvedThreads.forEach((result, index) => {
517
+ const firstComment = result.thread.comments.nodes[0];
518
+ if (firstComment) {
519
+ suggestions.push(`${index + 1}. [${firstComment.path}:${firstComment.line || '?'}] by @${firstComment.author.login}: ${result.analysis.reason}`);
520
+ }
521
+ });
522
+ // Add review-specific suggestions if any
523
+ if (unresolvedReviews.length > 0) {
524
+ suggestions.push(`\n${unresolvedReviews.length} review(s) requesting changes need to be addressed:`);
525
+ unresolvedReviews.forEach((review) => {
526
+ suggestions.push(` - @${review.user.login}: ${review.body ? review.body.substring(0, 150) : 'No details provided'}${review.body && review.body.length > 150 ? '...' : ''}`);
527
+ });
528
+ }
529
+ // Build detailed unresolved info with LLM-analyzed failure reasons
530
+ const unresolvedCommentDetails = trulyUnresolvedThreads.map((result) => {
531
+ const firstComment = result.thread.comments.nodes[0];
532
+ return {
533
+ commentId: firstComment.id,
534
+ author: firstComment.author.login,
535
+ file: firstComment.path,
536
+ line: firstComment.line,
537
+ body: firstComment.body,
538
+ failureReason: result.analysis.reason, // Use LLM analysis result
539
+ url: firstComment.url,
540
+ };
541
+ });
542
+ const unresolvedReviewDetails = unresolvedReviews.map((review) => ({
543
+ reviewId: review.id,
544
+ author: review.user.login,
545
+ state: review.state,
546
+ body: review.body,
547
+ submittedAt: review.submitted_at,
548
+ }));
549
+ let errorMessage = '';
550
+ if (unresolvedReviews.length > 0 && trulyUnresolvedThreads.length > 0) {
551
+ errorMessage = `${unresolvedReviews.length} reviews and ${trulyUnresolvedThreads.length} review threads still need to be addressed (based on LLM analysis)`;
552
+ }
553
+ else if (unresolvedReviews.length > 0) {
554
+ errorMessage = `${unresolvedReviews.length} reviews still requesting changes`;
555
+ }
556
+ else {
557
+ errorMessage = `${trulyUnresolvedThreads.length} review comments still need to be addressed (based on LLM analysis)`;
558
+ }
559
+ return {
560
+ status: 'error',
561
+ message: errorMessage,
562
+ data: {
563
+ featureId,
564
+ totalReviews: reviews.length,
565
+ unresolvedReviews: unresolvedReviews.length,
566
+ totalComments: unresolvedThreads.length, // Original count before LLM analysis
567
+ resolvedComments: addressedThreads.length, // LLM determined these are addressed
568
+ unresolvedComments: trulyUnresolvedThreads.length, // LLM determined these still need work
569
+ commentsMarkedResolved: addressedThreads.length,
570
+ suggestions,
571
+ unresolvedReviewDetails,
572
+ unresolvedCommentDetails,
573
+ },
574
+ };
575
+ }
576
+ }
577
+ catch (error) {
578
+ const errorMessage = error instanceof Error ? error.message : String(error);
579
+ logError(`Code refine verification failed: ${errorMessage}`);
580
+ return {
581
+ status: 'error',
582
+ message: `Verification failed: ${errorMessage}`,
583
+ data: {
584
+ featureId,
585
+ totalReviews: 0,
586
+ unresolvedReviews: 0,
587
+ totalComments: 0,
588
+ resolvedComments: 0,
589
+ unresolvedComments: 0,
590
+ suggestions: [
591
+ `Verification failed with error: ${errorMessage}`,
592
+ 'Please check the error message and try again',
593
+ ],
594
+ },
595
+ };
596
+ }
597
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Code Review Analyzer
3
+ * Reviews GitHub PR code and creates review comments with REQUEST_CHANGES
4
+ */
5
+ import { EdsgerConfig } from '../../types/index.js';
6
+ export interface CodeReviewOptions {
7
+ featureId: string;
8
+ githubToken: string;
9
+ verbose?: boolean;
10
+ }
11
+ export interface ReviewComment {
12
+ path: string;
13
+ line: number;
14
+ side: 'LEFT' | 'RIGHT';
15
+ body: string;
16
+ }
17
+ export interface CodeReviewResult {
18
+ featureId: string;
19
+ status: 'success' | 'error';
20
+ message: string;
21
+ reviewId?: number;
22
+ reviewUrl?: string;
23
+ commentsCount?: number;
24
+ summary?: string;
25
+ }
26
+ /**
27
+ * Main code review function
28
+ */
29
+ export declare const reviewPullRequest: (options: CodeReviewOptions, config: EdsgerConfig) => Promise<CodeReviewResult>;