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.
- package/dist/commands/pr-resolve/index.d.ts +10 -0
- package/dist/commands/pr-resolve/index.js +44 -0
- package/dist/commands/pr-review/index.d.ts +10 -0
- package/dist/commands/pr-review/index.js +43 -0
- package/dist/index.js +38 -0
- package/dist/phases/code-review/__tests__/diff-utils.test.d.ts +1 -0
- package/dist/phases/code-review/__tests__/diff-utils.test.js +101 -0
- package/dist/phases/code-review/diff-utils.d.ts +36 -0
- package/dist/phases/code-review/diff-utils.js +100 -0
- package/dist/phases/code-review/index.js +17 -153
- package/dist/phases/pr-execution/file-assigner.d.ts +12 -0
- package/dist/phases/pr-execution/file-assigner.js +34 -0
- package/dist/phases/pr-execution/index.js +25 -8
- package/dist/phases/pr-resolve/__tests__/prompts.test.d.ts +1 -0
- package/dist/phases/pr-resolve/__tests__/prompts.test.js +116 -0
- package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.d.ts +1 -0
- package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.js +138 -0
- package/dist/phases/pr-resolve/__tests__/workspace.test.d.ts +1 -0
- package/dist/phases/pr-resolve/__tests__/workspace.test.js +111 -0
- package/dist/phases/pr-resolve/github-reply.d.ts +13 -0
- package/dist/phases/pr-resolve/github-reply.js +59 -0
- package/dist/phases/pr-resolve/index.d.ts +27 -0
- package/dist/phases/pr-resolve/index.js +213 -0
- package/dist/phases/pr-resolve/prompts.d.ts +17 -0
- package/dist/phases/pr-resolve/prompts.js +106 -0
- package/dist/phases/pr-resolve/workspace.d.ts +29 -0
- package/dist/phases/pr-resolve/workspace.js +145 -0
- package/dist/phases/pr-review/__tests__/prompts.test.d.ts +1 -0
- package/dist/phases/pr-review/__tests__/prompts.test.js +49 -0
- package/dist/phases/pr-review/__tests__/review-comments.test.d.ts +1 -0
- package/dist/phases/pr-review/__tests__/review-comments.test.js +108 -0
- package/dist/phases/pr-review/index.d.ts +25 -0
- package/dist/phases/pr-review/index.js +213 -0
- package/dist/phases/pr-review/prompts.d.ts +5 -0
- package/dist/phases/pr-review/prompts.js +87 -0
- package/dist/phases/pr-shared/__tests__/agent-utils.test.d.ts +1 -0
- package/dist/phases/pr-shared/__tests__/agent-utils.test.js +91 -0
- package/dist/phases/pr-shared/__tests__/context.test.d.ts +1 -0
- package/dist/phases/pr-shared/__tests__/context.test.js +94 -0
- package/dist/phases/pr-shared/agent-utils.d.ts +39 -0
- package/dist/phases/pr-shared/agent-utils.js +69 -0
- package/dist/phases/pr-shared/context.d.ts +24 -0
- package/dist/phases/pr-shared/context.js +78 -0
- package/package.json +1 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone PR Review
|
|
3
|
+
* Reviews a GitHub PR and posts review comments, without feature lifecycle dependency.
|
|
4
|
+
*/
|
|
5
|
+
export interface StandalonePRReviewOptions {
|
|
6
|
+
productId: string;
|
|
7
|
+
pullRequestUrl: string;
|
|
8
|
+
githubToken: string;
|
|
9
|
+
owner: string;
|
|
10
|
+
repo: string;
|
|
11
|
+
prId?: string;
|
|
12
|
+
verbose?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface StandalonePRReviewResult {
|
|
15
|
+
status: 'success' | 'error';
|
|
16
|
+
message: string;
|
|
17
|
+
reviewId?: number;
|
|
18
|
+
reviewUrl?: string;
|
|
19
|
+
commentsCount?: number;
|
|
20
|
+
summary?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Review a standalone PR and post comments to GitHub.
|
|
24
|
+
*/
|
|
25
|
+
export declare function reviewStandalonePR(options: StandalonePRReviewOptions): Promise<StandalonePRReviewResult>;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone PR Review
|
|
3
|
+
* Reviews a GitHub PR and posts review comments, without feature lifecycle dependency.
|
|
4
|
+
*/
|
|
5
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
6
|
+
import { Octokit } from '@octokit/rest';
|
|
7
|
+
import { callMcpEndpoint } from '../../api/mcp-client.js';
|
|
8
|
+
import { DEFAULT_MODEL } from '../../constants.js';
|
|
9
|
+
import { logError, logInfo } from '../../utils/logger.js';
|
|
10
|
+
import { buildLineToPositionMap, findClosestPosition, } from '../code-review/diff-utils.js';
|
|
11
|
+
import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
|
|
12
|
+
import { fetchStandalonePRContext, formatStandalonePRContextForPrompt, } from '../pr-shared/context.js';
|
|
13
|
+
import { createStandaloneReviewSystemPrompt, createStandaloneReviewUserPrompt, } from './prompts.js';
|
|
14
|
+
/**
|
|
15
|
+
* Review a standalone PR and post comments to GitHub.
|
|
16
|
+
*/
|
|
17
|
+
export async function reviewStandalonePR(options
|
|
18
|
+
// eslint-disable-next-line complexity
|
|
19
|
+
) {
|
|
20
|
+
const { pullRequestUrl, githubToken, verbose, prId } = options;
|
|
21
|
+
logInfo(`Starting standalone PR review: ${pullRequestUrl}`);
|
|
22
|
+
try {
|
|
23
|
+
// Fetch PR context
|
|
24
|
+
const context = await fetchStandalonePRContext(pullRequestUrl, githubToken, verbose);
|
|
25
|
+
if (context.files.length === 0) {
|
|
26
|
+
logInfo('No files to review in the PR.');
|
|
27
|
+
return {
|
|
28
|
+
status: 'success',
|
|
29
|
+
message: 'No files to review',
|
|
30
|
+
summary: 'No changed files found in the PR',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
logInfo(`Found ${context.files.length} files to review across ${context.commits.length} commits`);
|
|
34
|
+
// Build prompts
|
|
35
|
+
const systemPrompt = createStandaloneReviewSystemPrompt();
|
|
36
|
+
const contextInfo = formatStandalonePRContextForPrompt(context);
|
|
37
|
+
const reviewPrompt = createStandaloneReviewUserPrompt(contextInfo);
|
|
38
|
+
let lastAssistantResponse = '';
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
let structuredReviewResult = null;
|
|
41
|
+
logInfo('Starting Claude analysis for code review...');
|
|
42
|
+
for await (const message of query({
|
|
43
|
+
prompt: createPromptGenerator(reviewPrompt),
|
|
44
|
+
options: {
|
|
45
|
+
systemPrompt: {
|
|
46
|
+
type: 'preset',
|
|
47
|
+
preset: 'claude_code',
|
|
48
|
+
append: systemPrompt,
|
|
49
|
+
},
|
|
50
|
+
model: DEFAULT_MODEL,
|
|
51
|
+
maxTurns: 50,
|
|
52
|
+
permissionMode: 'bypassPermissions',
|
|
53
|
+
},
|
|
54
|
+
})) {
|
|
55
|
+
if (message.type === 'assistant') {
|
|
56
|
+
lastAssistantResponse += extractTextFromContent(message.message?.content ?? [], verbose);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (message.type !== 'result') {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (message.subtype === 'success') {
|
|
63
|
+
logInfo('Code review analysis completed, parsing results...');
|
|
64
|
+
const responseText = message.result || lastAssistantResponse;
|
|
65
|
+
structuredReviewResult = tryExtractResult(responseText, 'review_result') || {
|
|
66
|
+
summary: 'Code review completed',
|
|
67
|
+
comments: [],
|
|
68
|
+
overall_assessment: 'COMMENT',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
logError(`Code review incomplete: ${message.subtype}`);
|
|
73
|
+
if (lastAssistantResponse) {
|
|
74
|
+
structuredReviewResult = tryExtractResult(lastAssistantResponse, 'review_result');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!structuredReviewResult) {
|
|
79
|
+
return {
|
|
80
|
+
status: 'error',
|
|
81
|
+
message: 'Code review analysis failed or incomplete',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// Post review to GitHub
|
|
85
|
+
const { summary, comments, overall_assessment } = structuredReviewResult;
|
|
86
|
+
const octokit = new Octokit({ auth: githubToken });
|
|
87
|
+
if (!comments || comments.length === 0) {
|
|
88
|
+
logInfo('No issues found. PR looks good!');
|
|
89
|
+
const review = await octokit.pulls.createReview({
|
|
90
|
+
owner: context.owner,
|
|
91
|
+
repo: context.repo,
|
|
92
|
+
pull_number: context.pullRequestNumber,
|
|
93
|
+
event: 'COMMENT',
|
|
94
|
+
body: summary ||
|
|
95
|
+
overall_assessment ||
|
|
96
|
+
'Code review completed. No issues found.',
|
|
97
|
+
});
|
|
98
|
+
if (prId) {
|
|
99
|
+
await updatePRStatus(prId, 'in_review');
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
status: 'success',
|
|
103
|
+
message: 'Code review completed - no issues found',
|
|
104
|
+
reviewId: review.data.id,
|
|
105
|
+
reviewUrl: review.data.html_url,
|
|
106
|
+
commentsCount: 0,
|
|
107
|
+
summary: summary || 'No issues found',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
logInfo(`Creating GitHub review with ${comments.length} comments...`);
|
|
111
|
+
// Build line-to-position maps for all files
|
|
112
|
+
const fileLineToPosition = new Map();
|
|
113
|
+
for (const file of context.files) {
|
|
114
|
+
if (file.patch) {
|
|
115
|
+
fileLineToPosition.set(file.filename, buildLineToPositionMap(file.patch));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const commitId = context.prData.head.sha;
|
|
119
|
+
// Map comments to diff positions
|
|
120
|
+
const reviewComments = [];
|
|
121
|
+
for (const comment of comments) {
|
|
122
|
+
const filePath = comment.file || comment.path;
|
|
123
|
+
const requestedLine = comment.line;
|
|
124
|
+
const lineToPosition = fileLineToPosition.get(filePath);
|
|
125
|
+
if (!lineToPosition) {
|
|
126
|
+
if (verbose) {
|
|
127
|
+
logInfo(`Skipping comment for ${filePath}:${requestedLine} - file has no diff`);
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const positionResult = findClosestPosition(requestedLine, lineToPosition);
|
|
132
|
+
if (positionResult === null) {
|
|
133
|
+
if (verbose) {
|
|
134
|
+
logInfo(`Skipping comment for ${filePath}:${requestedLine} - line not in diff range`);
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
let commentBody = comment.comment || comment.body;
|
|
139
|
+
if (positionResult.actualLine !== requestedLine) {
|
|
140
|
+
commentBody = `**Note**: Comment originally for line ${requestedLine}, adjusted to line ${positionResult.actualLine} (nearest line in diff).\n\n${commentBody}`;
|
|
141
|
+
}
|
|
142
|
+
reviewComments.push({
|
|
143
|
+
path: filePath,
|
|
144
|
+
position: positionResult.position,
|
|
145
|
+
body: commentBody,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (reviewComments.length === 0) {
|
|
149
|
+
logInfo('All comments were filtered out (invalid line numbers)');
|
|
150
|
+
const review = await octokit.pulls.createReview({
|
|
151
|
+
owner: context.owner,
|
|
152
|
+
repo: context.repo,
|
|
153
|
+
pull_number: context.pullRequestNumber,
|
|
154
|
+
event: 'COMMENT',
|
|
155
|
+
body: `${summary || 'Code review completed.'}\n\n**Note**: Some review comments could not be posted because they referenced lines not present in the diff.`,
|
|
156
|
+
});
|
|
157
|
+
if (prId) {
|
|
158
|
+
await updatePRStatus(prId, 'in_review');
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
status: 'success',
|
|
162
|
+
message: 'Code review completed - comments filtered',
|
|
163
|
+
reviewId: review.data.id,
|
|
164
|
+
reviewUrl: review.data.html_url,
|
|
165
|
+
commentsCount: 0,
|
|
166
|
+
summary: summary || 'Code review completed (comments filtered)',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
logInfo(`Posting ${reviewComments.length} validated comments (${comments.length - reviewComments.length} filtered out)`);
|
|
170
|
+
const review = await octokit.pulls.createReview({
|
|
171
|
+
owner: context.owner,
|
|
172
|
+
repo: context.repo,
|
|
173
|
+
pull_number: context.pullRequestNumber,
|
|
174
|
+
commit_id: commitId,
|
|
175
|
+
event: 'COMMENT',
|
|
176
|
+
body: summary ||
|
|
177
|
+
overall_assessment ||
|
|
178
|
+
'Please address the following review comments.',
|
|
179
|
+
comments: reviewComments,
|
|
180
|
+
});
|
|
181
|
+
logInfo(`GitHub review created: ${review.data.html_url}`);
|
|
182
|
+
if (prId) {
|
|
183
|
+
await updatePRStatus(prId, 'in_review');
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
status: 'success',
|
|
187
|
+
message: 'Code review completed and posted to GitHub',
|
|
188
|
+
reviewId: review.data.id,
|
|
189
|
+
reviewUrl: review.data.html_url,
|
|
190
|
+
commentsCount: reviewComments.length,
|
|
191
|
+
summary: summary || 'Code review completed',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
196
|
+
logError(`PR review failed: ${errorMessage}`);
|
|
197
|
+
return {
|
|
198
|
+
status: 'error',
|
|
199
|
+
message: `PR review failed: ${errorMessage}`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function updatePRStatus(prId, status) {
|
|
204
|
+
try {
|
|
205
|
+
await callMcpEndpoint('pull_requests/update', {
|
|
206
|
+
pull_request_id: prId,
|
|
207
|
+
status,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// Non-critical, don't fail the review
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompts for standalone PR review (no feature/design context).
|
|
3
|
+
*/
|
|
4
|
+
export function createStandaloneReviewSystemPrompt() {
|
|
5
|
+
return `You are an expert code reviewer specializing in thorough, constructive code reviews. Your goal is to analyze pull request code and identify issues, potential bugs, code quality concerns, and areas for improvement.
|
|
6
|
+
|
|
7
|
+
**Review Focus Areas**:
|
|
8
|
+
1. **Code Quality**: Clean code principles, readability, maintainability
|
|
9
|
+
2. **Best Practices**: Language-specific conventions, design patterns
|
|
10
|
+
3. **Potential Bugs**: Logic errors, edge cases, error handling
|
|
11
|
+
4. **Security**: Security vulnerabilities, input validation
|
|
12
|
+
5. **Performance**: Performance issues, optimization opportunities
|
|
13
|
+
6. **Architecture**: Design decisions, coupling, cohesion
|
|
14
|
+
|
|
15
|
+
**Important Guidelines**:
|
|
16
|
+
- Be thorough but focus on significant issues
|
|
17
|
+
- Provide constructive, actionable feedback
|
|
18
|
+
- Suggest specific improvements
|
|
19
|
+
- Be respectful and professional
|
|
20
|
+
- Focus on issues that truly matter, not nitpicks
|
|
21
|
+
- If code looks good overall, provide positive feedback
|
|
22
|
+
|
|
23
|
+
**CRITICAL - Result Format**:
|
|
24
|
+
You MUST end your response with a JSON object containing the review results in this EXACT format:
|
|
25
|
+
|
|
26
|
+
\`\`\`json
|
|
27
|
+
{
|
|
28
|
+
"review_result": {
|
|
29
|
+
"summary": "Overall review summary and assessment",
|
|
30
|
+
"overall_assessment": "APPROVE | REQUEST_CHANGES | COMMENT",
|
|
31
|
+
"comments": [
|
|
32
|
+
{
|
|
33
|
+
"file": "path/to/file.ts",
|
|
34
|
+
"line": 42,
|
|
35
|
+
"comment": "Detailed comment about the issue and suggested fix"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"issues_found": {
|
|
39
|
+
"critical": 0,
|
|
40
|
+
"major": 0,
|
|
41
|
+
"minor": 0
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
\`\`\`
|
|
46
|
+
|
|
47
|
+
**Comment Guidelines**:
|
|
48
|
+
- Each comment should reference a specific file and line number
|
|
49
|
+
- Be specific about what the issue is
|
|
50
|
+
- Provide a suggested fix or improvement
|
|
51
|
+
|
|
52
|
+
**Assessment Options**:
|
|
53
|
+
- **APPROVE**: Code looks good, no significant issues
|
|
54
|
+
- **REQUEST_CHANGES**: Issues found that should be addressed
|
|
55
|
+
- **COMMENT**: Minor suggestions or questions, not blocking`;
|
|
56
|
+
}
|
|
57
|
+
export function createStandaloneReviewUserPrompt(contextInfo) {
|
|
58
|
+
return `Review the following pull request code:
|
|
59
|
+
|
|
60
|
+
${contextInfo}
|
|
61
|
+
|
|
62
|
+
## Code Review Instructions
|
|
63
|
+
|
|
64
|
+
Follow this systematic approach:
|
|
65
|
+
|
|
66
|
+
1. **Analyze Each File**: For each changed file:
|
|
67
|
+
- Review the code changes in the diff
|
|
68
|
+
- Look for code quality issues
|
|
69
|
+
- Check for potential bugs or logic errors
|
|
70
|
+
- Verify error handling
|
|
71
|
+
- Consider security implications
|
|
72
|
+
- Evaluate performance considerations
|
|
73
|
+
|
|
74
|
+
2. **Identify Issues**: Categorize by severity:
|
|
75
|
+
- **Critical**: Security vulnerabilities, data loss risks, major bugs
|
|
76
|
+
- **Major**: Logic errors, incorrect implementations, missing error handling
|
|
77
|
+
- **Minor**: Code quality improvements, style issues, optimization suggestions
|
|
78
|
+
|
|
79
|
+
3. **Provide Actionable Feedback**: For each issue:
|
|
80
|
+
- Reference the specific file and line number
|
|
81
|
+
- Explain what the issue is
|
|
82
|
+
- Suggest how to fix it
|
|
83
|
+
|
|
84
|
+
4. **Generate Review Result**: Create a structured JSON response with summary, assessment, and comments.
|
|
85
|
+
|
|
86
|
+
Begin by analyzing the changed files and identifying any issues or improvements.`;
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { extractTextFromContent, tryExtractResult, tryParseJsonFromResponse, userMessage, } from '../agent-utils.js';
|
|
4
|
+
describe('userMessage', () => {
|
|
5
|
+
it('creates a user message object', () => {
|
|
6
|
+
const msg = userMessage('hello');
|
|
7
|
+
assert.deepStrictEqual(msg, {
|
|
8
|
+
type: 'user',
|
|
9
|
+
message: { role: 'user', content: 'hello' },
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
describe('extractTextFromContent', () => {
|
|
14
|
+
it('extracts text items from content array', () => {
|
|
15
|
+
const content = [
|
|
16
|
+
{ type: 'text', text: 'hello' },
|
|
17
|
+
{ type: 'tool_use', id: '123', name: 'read', input: {} },
|
|
18
|
+
{ type: 'text', text: 'world' },
|
|
19
|
+
];
|
|
20
|
+
const result = extractTextFromContent(content);
|
|
21
|
+
assert.strictEqual(result, 'hello\nworld\n');
|
|
22
|
+
});
|
|
23
|
+
it('returns empty string for no text items', () => {
|
|
24
|
+
const content = [{ type: 'tool_use', id: '123', name: 'read', input: {} }];
|
|
25
|
+
const result = extractTextFromContent(content);
|
|
26
|
+
assert.strictEqual(result, '');
|
|
27
|
+
});
|
|
28
|
+
it('returns empty string for empty array', () => {
|
|
29
|
+
assert.strictEqual(extractTextFromContent([]), '');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('tryParseJsonFromResponse', () => {
|
|
33
|
+
it('parses JSON from code block', () => {
|
|
34
|
+
const text = 'Here is the result:\n```json\n{"key": "value"}\n```\nDone.';
|
|
35
|
+
const result = tryParseJsonFromResponse(text);
|
|
36
|
+
assert.deepStrictEqual(result, { key: 'value' });
|
|
37
|
+
});
|
|
38
|
+
it('parses raw JSON', () => {
|
|
39
|
+
const text = '{"key": "value"}';
|
|
40
|
+
const result = tryParseJsonFromResponse(text);
|
|
41
|
+
assert.deepStrictEqual(result, { key: 'value' });
|
|
42
|
+
});
|
|
43
|
+
it('returns null for invalid JSON', () => {
|
|
44
|
+
const result = tryParseJsonFromResponse('not json at all');
|
|
45
|
+
assert.strictEqual(result, null);
|
|
46
|
+
});
|
|
47
|
+
it('returns null for empty string', () => {
|
|
48
|
+
const result = tryParseJsonFromResponse('');
|
|
49
|
+
assert.strictEqual(result, null);
|
|
50
|
+
});
|
|
51
|
+
it('parses JSON block with surrounding text', () => {
|
|
52
|
+
const text = `I analyzed the code and here are my findings:
|
|
53
|
+
|
|
54
|
+
\`\`\`json
|
|
55
|
+
{
|
|
56
|
+
"review_result": {
|
|
57
|
+
"summary": "looks good",
|
|
58
|
+
"comments": []
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
\`\`\`
|
|
62
|
+
|
|
63
|
+
That's my review.`;
|
|
64
|
+
const result = tryParseJsonFromResponse(text);
|
|
65
|
+
assert.ok(result);
|
|
66
|
+
assert.ok(result.review_result);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('tryExtractResult', () => {
|
|
70
|
+
it('extracts keyed result from JSON code block', () => {
|
|
71
|
+
const text = '```json\n{"review_result": {"summary": "good"}}\n```';
|
|
72
|
+
const result = tryExtractResult(text, 'review_result');
|
|
73
|
+
assert.deepStrictEqual(result, { summary: 'good' });
|
|
74
|
+
});
|
|
75
|
+
it('returns whole object if key not found but JSON valid', () => {
|
|
76
|
+
const text = '{"summary": "good", "comments": []}';
|
|
77
|
+
const result = tryExtractResult(text, 'review_result');
|
|
78
|
+
assert.deepStrictEqual(result, { summary: 'good', comments: [] });
|
|
79
|
+
});
|
|
80
|
+
it('returns null for unparseable text', () => {
|
|
81
|
+
const result = tryExtractResult('no json here', 'review_result');
|
|
82
|
+
assert.strictEqual(result, null);
|
|
83
|
+
});
|
|
84
|
+
it('extracts resolve_result key', () => {
|
|
85
|
+
const text = '```json\n{"resolve_result": {"comments": [{"comment_id": "comment_1", "action": "changed", "reply": "fixed"}]}}\n```';
|
|
86
|
+
const result = tryExtractResult(text, 'resolve_result');
|
|
87
|
+
assert.ok(result);
|
|
88
|
+
assert.ok(Array.isArray(result.comments));
|
|
89
|
+
assert.strictEqual(result.comments.length, 1);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { formatStandalonePRContextForPrompt, parsePullRequestUrl, } from '../context.js';
|
|
4
|
+
describe('parsePullRequestUrl (re-exported)', () => {
|
|
5
|
+
it('parses a standard GitHub PR URL', () => {
|
|
6
|
+
const result = parsePullRequestUrl('https://github.com/owner/repo/pull/123');
|
|
7
|
+
assert.deepStrictEqual(result, {
|
|
8
|
+
owner: 'owner',
|
|
9
|
+
repo: 'repo',
|
|
10
|
+
prNumber: 123,
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
it('parses URL with trailing path segments', () => {
|
|
14
|
+
const result = parsePullRequestUrl('https://github.com/my-org/my-repo/pull/456/files');
|
|
15
|
+
assert.deepStrictEqual(result, {
|
|
16
|
+
owner: 'my-org',
|
|
17
|
+
repo: 'my-repo',
|
|
18
|
+
prNumber: 456,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
it('returns null for non-PR URL', () => {
|
|
22
|
+
assert.strictEqual(parsePullRequestUrl('https://github.com/owner/repo/issues/1'), null);
|
|
23
|
+
});
|
|
24
|
+
it('returns null for non-GitHub URL', () => {
|
|
25
|
+
assert.strictEqual(parsePullRequestUrl('https://example.com/pull/1'), null);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe('formatStandalonePRContextForPrompt', () => {
|
|
29
|
+
const context = {
|
|
30
|
+
pullRequestUrl: 'https://github.com/owner/repo/pull/42',
|
|
31
|
+
pullRequestNumber: 42,
|
|
32
|
+
owner: 'owner',
|
|
33
|
+
repo: 'repo',
|
|
34
|
+
prData: {
|
|
35
|
+
number: 42,
|
|
36
|
+
title: 'Fix bug in auth',
|
|
37
|
+
body: 'This fixes the login issue',
|
|
38
|
+
state: 'open',
|
|
39
|
+
head: { ref: 'fix/auth', sha: 'abc123' },
|
|
40
|
+
base: { ref: 'main', sha: 'def456' },
|
|
41
|
+
user: { login: 'testuser' },
|
|
42
|
+
},
|
|
43
|
+
files: [
|
|
44
|
+
{
|
|
45
|
+
filename: 'src/auth.ts',
|
|
46
|
+
status: 'modified',
|
|
47
|
+
additions: 5,
|
|
48
|
+
deletions: 2,
|
|
49
|
+
changes: 7,
|
|
50
|
+
patch: '@@ -1,3 +1,6 @@\n line1\n+added\n line3',
|
|
51
|
+
blob_url: '',
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
commits: [
|
|
55
|
+
{
|
|
56
|
+
sha: 'abc1234567890',
|
|
57
|
+
commit: {
|
|
58
|
+
message: 'fix: auth bug',
|
|
59
|
+
author: { name: 'Test', date: '2026-01-01' },
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
it('includes PR URL and number', () => {
|
|
65
|
+
const output = formatStandalonePRContextForPrompt(context);
|
|
66
|
+
assert.ok(output.includes('#42'));
|
|
67
|
+
assert.ok(output.includes('github.com/owner/repo/pull/42'));
|
|
68
|
+
});
|
|
69
|
+
it('includes PR title and author', () => {
|
|
70
|
+
const output = formatStandalonePRContextForPrompt(context);
|
|
71
|
+
assert.ok(output.includes('Fix bug in auth'));
|
|
72
|
+
assert.ok(output.includes('@testuser'));
|
|
73
|
+
});
|
|
74
|
+
it('includes branch info', () => {
|
|
75
|
+
const output = formatStandalonePRContextForPrompt(context);
|
|
76
|
+
assert.ok(output.includes('fix/auth'));
|
|
77
|
+
assert.ok(output.includes('main'));
|
|
78
|
+
});
|
|
79
|
+
it('includes file diff', () => {
|
|
80
|
+
const output = formatStandalonePRContextForPrompt(context);
|
|
81
|
+
assert.ok(output.includes('src/auth.ts'));
|
|
82
|
+
assert.ok(output.includes('+added'));
|
|
83
|
+
assert.ok(output.includes('+5 -2'));
|
|
84
|
+
});
|
|
85
|
+
it('includes commit info', () => {
|
|
86
|
+
const output = formatStandalonePRContextForPrompt(context);
|
|
87
|
+
assert.ok(output.includes('abc1234'));
|
|
88
|
+
assert.ok(output.includes('fix: auth bug'));
|
|
89
|
+
});
|
|
90
|
+
it('includes PR body/description', () => {
|
|
91
|
+
const output = formatStandalonePRContextForPrompt(context);
|
|
92
|
+
assert.ok(output.includes('This fixes the login issue'));
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for Agent SDK phases.
|
|
3
|
+
* Eliminates duplication of prompt generators, text extractors, and JSON parsers.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Create a user message object for the Agent SDK prompt generator.
|
|
7
|
+
*/
|
|
8
|
+
export declare function userMessage(content: string): {
|
|
9
|
+
type: string;
|
|
10
|
+
message: {
|
|
11
|
+
role: string;
|
|
12
|
+
content: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Create an async generator that yields a single user prompt for Agent SDK query().
|
|
17
|
+
*/
|
|
18
|
+
export declare function createPromptGenerator(prompt: string): AsyncGenerator<{
|
|
19
|
+
type: string;
|
|
20
|
+
message: {
|
|
21
|
+
role: string;
|
|
22
|
+
content: string;
|
|
23
|
+
};
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Extract text content from assistant message content array.
|
|
27
|
+
*/
|
|
28
|
+
export declare function extractTextFromContent(content: any[], verbose?: boolean): string;
|
|
29
|
+
/**
|
|
30
|
+
* Try to parse a JSON result from agent response text.
|
|
31
|
+
* Looks for ```json code blocks first, then falls back to raw JSON parsing.
|
|
32
|
+
* Returns the parsed object or null on failure.
|
|
33
|
+
*/
|
|
34
|
+
export declare function tryParseJsonFromResponse(responseText: string): unknown | null;
|
|
35
|
+
/**
|
|
36
|
+
* Extract a specific keyed result from agent response.
|
|
37
|
+
* e.g., tryExtractResult(text, 'review_result') extracts the review_result key.
|
|
38
|
+
*/
|
|
39
|
+
export declare function tryExtractResult(responseText: string, key: string): unknown | null;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for Agent SDK phases.
|
|
3
|
+
* Eliminates duplication of prompt generators, text extractors, and JSON parsers.
|
|
4
|
+
*/
|
|
5
|
+
import { logDebug } from '../../utils/logger.js';
|
|
6
|
+
/**
|
|
7
|
+
* Create a user message object for the Agent SDK prompt generator.
|
|
8
|
+
*/
|
|
9
|
+
export function userMessage(content) {
|
|
10
|
+
return {
|
|
11
|
+
type: 'user',
|
|
12
|
+
message: { role: 'user', content },
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create an async generator that yields a single user prompt for Agent SDK query().
|
|
17
|
+
*/
|
|
18
|
+
export async function* createPromptGenerator(prompt) {
|
|
19
|
+
yield userMessage(prompt);
|
|
20
|
+
await new Promise((res) => {
|
|
21
|
+
setTimeout(res, 10000);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Extract text content from assistant message content array.
|
|
26
|
+
*/
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
export function extractTextFromContent(
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
content, verbose) {
|
|
31
|
+
let text = '';
|
|
32
|
+
for (const item of content) {
|
|
33
|
+
if (item.type === 'text') {
|
|
34
|
+
text += `${item.text}\n`;
|
|
35
|
+
logDebug(item.text, verbose);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return text;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Try to parse a JSON result from agent response text.
|
|
42
|
+
* Looks for ```json code blocks first, then falls back to raw JSON parsing.
|
|
43
|
+
* Returns the parsed object or null on failure.
|
|
44
|
+
*/
|
|
45
|
+
export function tryParseJsonFromResponse(responseText) {
|
|
46
|
+
try {
|
|
47
|
+
const jsonBlockMatch = responseText.match(/```json\s*\n([\s\S]*?)\n\s*```/);
|
|
48
|
+
return jsonBlockMatch
|
|
49
|
+
? JSON.parse(jsonBlockMatch[1])
|
|
50
|
+
: JSON.parse(responseText);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Extract a specific keyed result from agent response.
|
|
58
|
+
* e.g., tryExtractResult(text, 'review_result') extracts the review_result key.
|
|
59
|
+
*/
|
|
60
|
+
export function tryExtractResult(responseText, key) {
|
|
61
|
+
const parsed = tryParseJsonFromResponse(responseText);
|
|
62
|
+
if (parsed &&
|
|
63
|
+
typeof parsed === 'object' &&
|
|
64
|
+
key in parsed) {
|
|
65
|
+
return parsed[key];
|
|
66
|
+
}
|
|
67
|
+
// If top-level has the expected shape, return the whole thing
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared context builder for standalone PR operations.
|
|
3
|
+
* Reuses GitHub API utilities from code-review/context.ts without feature dependencies.
|
|
4
|
+
*/
|
|
5
|
+
import { type PRCommit, type PRData, type PRFile } from '../code-review/context.js';
|
|
6
|
+
export { parsePullRequestUrl } from '../code-review/context.js';
|
|
7
|
+
export type { PRCommit, PRData, PRFile } from '../code-review/context.js';
|
|
8
|
+
export interface StandalonePRContext {
|
|
9
|
+
pullRequestUrl: string;
|
|
10
|
+
pullRequestNumber: number;
|
|
11
|
+
owner: string;
|
|
12
|
+
repo: string;
|
|
13
|
+
prData: PRData;
|
|
14
|
+
files: PRFile[];
|
|
15
|
+
commits: PRCommit[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Fetch PR context for standalone review/resolve (no feature dependency).
|
|
19
|
+
*/
|
|
20
|
+
export declare function fetchStandalonePRContext(pullRequestUrl: string, githubToken: string, verbose?: boolean): Promise<StandalonePRContext>;
|
|
21
|
+
/**
|
|
22
|
+
* Format standalone PR context for prompts (no feature/design context).
|
|
23
|
+
*/
|
|
24
|
+
export declare function formatStandalonePRContextForPrompt(context: StandalonePRContext): string;
|