edsger 0.41.0 → 0.41.2
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/.claude/settings.local.json +23 -3
- package/.env.local +12 -0
- package/dist/api/features/__tests__/regression-prevention.test.d.ts +5 -0
- package/dist/api/features/__tests__/regression-prevention.test.js +338 -0
- package/dist/api/features/__tests__/status-updater.integration.test.d.ts +5 -0
- package/dist/api/features/__tests__/status-updater.integration.test.js +497 -0
- package/dist/commands/workflow/pipeline-runner.d.ts +17 -0
- package/dist/commands/workflow/pipeline-runner.js +393 -0
- package/dist/commands/workflow/runner.d.ts +26 -0
- package/dist/commands/workflow/runner.js +119 -0
- package/dist/commands/workflow/workflow-runner.d.ts +26 -0
- package/dist/commands/workflow/workflow-runner.js +119 -0
- package/dist/index.js +0 -0
- package/dist/phases/code-implementation/analyzer-helpers.d.ts +28 -0
- package/dist/phases/code-implementation/analyzer-helpers.js +177 -0
- package/dist/phases/code-implementation/analyzer.d.ts +32 -0
- package/dist/phases/code-implementation/analyzer.js +629 -0
- package/dist/phases/code-implementation/context-fetcher.d.ts +17 -0
- package/dist/phases/code-implementation/context-fetcher.js +86 -0
- package/dist/phases/code-implementation/mcp-server.d.ts +1 -0
- package/dist/phases/code-implementation/mcp-server.js +93 -0
- package/dist/phases/code-implementation/prompts-improvement.d.ts +5 -0
- package/dist/phases/code-implementation/prompts-improvement.js +108 -0
- package/dist/phases/code-implementation-verification/verifier.d.ts +31 -0
- package/dist/phases/code-implementation-verification/verifier.js +196 -0
- package/dist/phases/code-refine/analyzer.d.ts +41 -0
- package/dist/phases/code-refine/analyzer.js +561 -0
- package/dist/phases/code-refine/context-fetcher.d.ts +94 -0
- package/dist/phases/code-refine/context-fetcher.js +423 -0
- package/dist/phases/code-refine-verification/analysis/llm-analyzer.d.ts +22 -0
- package/dist/phases/code-refine-verification/analysis/llm-analyzer.js +134 -0
- package/dist/phases/code-refine-verification/verifier.d.ts +47 -0
- package/dist/phases/code-refine-verification/verifier.js +597 -0
- package/dist/phases/code-review/analyzer.d.ts +29 -0
- package/dist/phases/code-review/analyzer.js +363 -0
- package/dist/phases/code-review/context-fetcher.d.ts +92 -0
- package/dist/phases/code-review/context-fetcher.js +296 -0
- package/dist/phases/feature-analysis/analyzer-helpers.d.ts +10 -0
- package/dist/phases/feature-analysis/analyzer-helpers.js +47 -0
- package/dist/phases/feature-analysis/analyzer.d.ts +11 -0
- package/dist/phases/feature-analysis/analyzer.js +208 -0
- package/dist/phases/feature-analysis/context-fetcher.d.ts +26 -0
- package/dist/phases/feature-analysis/context-fetcher.js +134 -0
- package/dist/phases/feature-analysis/http-fallback.d.ts +20 -0
- package/dist/phases/feature-analysis/http-fallback.js +95 -0
- package/dist/phases/feature-analysis/mcp-server.d.ts +1 -0
- package/dist/phases/feature-analysis/mcp-server.js +144 -0
- package/dist/phases/feature-analysis/prompts-improvement.d.ts +8 -0
- package/dist/phases/feature-analysis/prompts-improvement.js +109 -0
- package/dist/phases/feature-analysis-verification/verifier.d.ts +37 -0
- package/dist/phases/feature-analysis-verification/verifier.js +147 -0
- package/dist/phases/pr-execution/file-assigner.js +20 -12
- package/dist/phases/technical-design/analyzer-helpers.d.ts +25 -0
- package/dist/phases/technical-design/analyzer-helpers.js +39 -0
- package/dist/phases/technical-design/analyzer.d.ts +21 -0
- package/dist/phases/technical-design/analyzer.js +461 -0
- package/dist/phases/technical-design/context-fetcher.d.ts +12 -0
- package/dist/phases/technical-design/context-fetcher.js +39 -0
- package/dist/phases/technical-design/http-fallback.d.ts +17 -0
- package/dist/phases/technical-design/http-fallback.js +151 -0
- package/dist/phases/technical-design/mcp-server.d.ts +1 -0
- package/dist/phases/technical-design/mcp-server.js +157 -0
- package/dist/phases/technical-design/prompts-improvement.d.ts +5 -0
- package/dist/phases/technical-design/prompts-improvement.js +93 -0
- package/dist/phases/technical-design-verification/verifier.d.ts +53 -0
- package/dist/phases/technical-design-verification/verifier.js +170 -0
- package/dist/services/feature-branches.d.ts +77 -0
- package/dist/services/feature-branches.js +205 -0
- package/dist/workflow-runner/config/phase-configs.d.ts +5 -0
- package/dist/workflow-runner/config/phase-configs.js +120 -0
- package/dist/workflow-runner/core/feature-filter.d.ts +16 -0
- package/dist/workflow-runner/core/feature-filter.js +46 -0
- package/dist/workflow-runner/core/index.d.ts +8 -0
- package/dist/workflow-runner/core/index.js +12 -0
- package/dist/workflow-runner/core/pipeline-evaluator.d.ts +24 -0
- package/dist/workflow-runner/core/pipeline-evaluator.js +32 -0
- package/dist/workflow-runner/core/state-manager.d.ts +24 -0
- package/dist/workflow-runner/core/state-manager.js +42 -0
- package/dist/workflow-runner/core/workflow-logger.d.ts +20 -0
- package/dist/workflow-runner/core/workflow-logger.js +65 -0
- package/dist/workflow-runner/executors/phase-executor.d.ts +8 -0
- package/dist/workflow-runner/executors/phase-executor.js +248 -0
- package/dist/workflow-runner/feature-workflow-runner.d.ts +26 -0
- package/dist/workflow-runner/feature-workflow-runner.js +119 -0
- package/dist/workflow-runner/index.d.ts +2 -0
- package/dist/workflow-runner/index.js +2 -0
- package/dist/workflow-runner/pipeline-runner.d.ts +17 -0
- package/dist/workflow-runner/pipeline-runner.js +393 -0
- package/dist/workflow-runner/workflow-processor.d.ts +54 -0
- package/dist/workflow-runner/workflow-processor.js +170 -0
- package/package.json +1 -1
- package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +0 -4
- package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +0 -133
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +0 -4
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +0 -336
- package/dist/services/lifecycle-agent/index.d.ts +0 -24
- package/dist/services/lifecycle-agent/index.js +0 -25
- package/dist/services/lifecycle-agent/phase-criteria.d.ts +0 -57
- package/dist/services/lifecycle-agent/phase-criteria.js +0 -335
- package/dist/services/lifecycle-agent/transition-rules.d.ts +0 -60
- package/dist/services/lifecycle-agent/transition-rules.js +0 -184
- package/dist/services/lifecycle-agent/types.d.ts +0 -190
- 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>;
|