edsger 0.42.0 → 0.43.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/api/web-deploy.d.ts +8 -1
- package/dist/api/web-deploy.js +2 -1
- package/dist/commands/workflow/phase-orchestrator.js +3 -1
- package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.js +1 -0
- package/dist/phases/app-store-generation/index.js +3 -1
- package/dist/phases/app-store-generation/screenshot-composer.js +34 -10
- package/dist/phases/branch-planning/index.js +3 -1
- package/dist/phases/bug-fixing/analyzer.js +3 -1
- package/dist/phases/code-implementation/index.js +3 -1
- package/dist/phases/code-refine/index.js +3 -1
- package/dist/phases/code-review/__tests__/diff-utils.test.js +11 -11
- package/dist/phases/code-review/index.js +3 -1
- package/dist/phases/code-testing/analyzer.js +3 -1
- package/dist/phases/feature-analysis/index.js +3 -1
- package/dist/phases/functional-testing/analyzer.js +3 -1
- package/dist/phases/growth-analysis/index.js +3 -1
- package/dist/phases/intelligence-analysis/__tests__/orchestration.test.js +12 -12
- package/dist/phases/intelligence-analysis/agent.js +2 -0
- package/dist/phases/intelligence-analysis/index.js +1 -0
- package/dist/phases/intelligence-analysis/prompts.js +11 -1
- package/dist/phases/output-contracts.js +1 -0
- package/dist/phases/pr-execution/__tests__/file-assigner.test.js +22 -13
- package/dist/phases/pr-execution/context.js +4 -2
- package/dist/phases/pr-execution/file-assigner.js +1 -0
- package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +11 -11
- package/dist/phases/pr-resolve/__tests__/prompts.test.js +12 -12
- package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.js +6 -6
- package/dist/phases/pr-resolve/__tests__/types.test.js +11 -11
- package/dist/phases/pr-resolve/__tests__/workspace.test.js +13 -13
- package/dist/phases/pr-resolve/checklist-learner.js +34 -9
- package/dist/phases/pr-resolve/index.js +45 -12
- package/dist/phases/pr-resolve/prompts.js +2 -1
- package/dist/phases/pr-resolve/workspace.d.ts +18 -2
- package/dist/phases/pr-resolve/workspace.js +43 -14
- package/dist/phases/pr-review/__tests__/prompts.test.js +9 -9
- package/dist/phases/pr-review/__tests__/review-comments.test.js +6 -6
- package/dist/phases/pr-review/index.js +1 -0
- package/dist/phases/pr-shared/__tests__/agent-utils.test.js +17 -17
- package/dist/phases/pr-shared/__tests__/context.test.js +12 -12
- package/dist/phases/pr-splitting/import-dep-validator.js +14 -6
- package/dist/phases/pr-splitting/index.js +3 -1
- package/dist/phases/technical-design/index.js +3 -1
- package/dist/phases/test-cases-analysis/index.js +3 -1
- package/dist/phases/user-stories-analysis/index.js +3 -1
- package/dist/services/phase-hooks/__tests__/hook-executor.test.js +7 -4
- package/dist/services/phase-hooks/__tests__/hook-runner.test.js +22 -21
- package/dist/services/phase-hooks/hook-executor.js +1 -0
- package/dist/services/phase-hooks/plugin-loader.js +3 -0
- package/dist/services/video/screenshot-generator.js +8 -2
- package/package.json +1 -1
|
@@ -21,16 +21,20 @@ export function buildCredentialArgs(token) {
|
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* Get the workspace path for a PR resolve operation.
|
|
24
|
+
* Includes owner/repo to avoid collisions when resolving PRs from different repos.
|
|
24
25
|
*/
|
|
25
|
-
export function getResolveWorkspacePath(prNumber) {
|
|
26
|
-
return join(homedir(), 'edsger', `pr-resolve-${prNumber}`);
|
|
26
|
+
export function getResolveWorkspacePath(owner, repo, prNumber) {
|
|
27
|
+
return join(homedir(), 'edsger', `pr-resolve-${owner}-${repo}-${prNumber}`);
|
|
27
28
|
}
|
|
28
29
|
/**
|
|
29
30
|
* Clone or reuse a repo for PR resolve.
|
|
31
|
+
* For fork PRs, clones from the fork repo. If the fork branch is unavailable,
|
|
32
|
+
* falls back to fetching the PR ref from the upstream repo.
|
|
30
33
|
* Returns the workspace path.
|
|
31
34
|
*/
|
|
32
|
-
export function prepareWorkspace(
|
|
33
|
-
const
|
|
35
|
+
export function prepareWorkspace(options) {
|
|
36
|
+
const { owner, repo, headRef, prNumber, token, verbose, forkFallback } = options;
|
|
37
|
+
const repoPath = getResolveWorkspacePath(owner, repo, prNumber);
|
|
34
38
|
const repoUrl = `https://github.com/${owner}/${repo}.git`;
|
|
35
39
|
const gitCredentialArgs = buildCredentialArgs(token);
|
|
36
40
|
if (existsSync(join(repoPath, '.git'))) {
|
|
@@ -66,16 +70,41 @@ export function prepareWorkspace(owner, repo, headRef, prNumber, token, verbose)
|
|
|
66
70
|
}
|
|
67
71
|
else {
|
|
68
72
|
logInfo(`Cloning ${owner}/${repo} (branch: ${headRef})...`);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
try {
|
|
74
|
+
execFileSync('git', [
|
|
75
|
+
...gitCredentialArgs,
|
|
76
|
+
'clone',
|
|
77
|
+
'--branch',
|
|
78
|
+
headRef,
|
|
79
|
+
'--single-branch',
|
|
80
|
+
repoUrl,
|
|
81
|
+
repoPath,
|
|
82
|
+
], { stdio: 'pipe' });
|
|
83
|
+
logSuccess(`Cloned to ${repoPath}`);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
// If clone fails and this is a fork PR, fall back to upstream PR ref
|
|
87
|
+
if (forkFallback) {
|
|
88
|
+
logInfo(`Fork branch not available, falling back to PR ref from ${forkFallback.upstreamOwner}/${forkFallback.upstreamRepo}`);
|
|
89
|
+
const upstreamUrl = `https://github.com/${forkFallback.upstreamOwner}/${forkFallback.upstreamRepo}.git`;
|
|
90
|
+
execFileSync('git', [...gitCredentialArgs, 'clone', upstreamUrl, repoPath], { stdio: 'pipe' });
|
|
91
|
+
// Fetch the PR ref and check it out
|
|
92
|
+
execFileSync('git', [
|
|
93
|
+
...gitCredentialArgs,
|
|
94
|
+
'fetch',
|
|
95
|
+
'origin',
|
|
96
|
+
`pull/${prNumber}/head:${headRef}`,
|
|
97
|
+
], { cwd: repoPath, stdio: 'pipe' });
|
|
98
|
+
execSync(`git checkout ${headRef}`, {
|
|
99
|
+
cwd: repoPath,
|
|
100
|
+
stdio: 'pipe',
|
|
101
|
+
});
|
|
102
|
+
logSuccess(`Cloned via PR ref to ${repoPath}`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
79
108
|
}
|
|
80
109
|
// Configure git user for commits
|
|
81
110
|
try {
|
|
@@ -1,46 +1,46 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import { createStandaloneReviewSystemPrompt, createStandaloneReviewUserPrompt, } from '../prompts.js';
|
|
4
|
-
describe('createStandaloneReviewSystemPrompt', () => {
|
|
5
|
-
it('includes review focus areas', () => {
|
|
4
|
+
void describe('createStandaloneReviewSystemPrompt', () => {
|
|
5
|
+
void it('includes review focus areas', () => {
|
|
6
6
|
const prompt = createStandaloneReviewSystemPrompt();
|
|
7
7
|
assert.ok(prompt.includes('Code Quality'));
|
|
8
8
|
assert.ok(prompt.includes('Security'));
|
|
9
9
|
assert.ok(prompt.includes('Performance'));
|
|
10
10
|
});
|
|
11
|
-
it('specifies JSON result format', () => {
|
|
11
|
+
void it('specifies JSON result format', () => {
|
|
12
12
|
const prompt = createStandaloneReviewSystemPrompt();
|
|
13
13
|
assert.ok(prompt.includes('review_result'));
|
|
14
14
|
assert.ok(prompt.includes('"file"'));
|
|
15
15
|
assert.ok(prompt.includes('"line"'));
|
|
16
16
|
assert.ok(prompt.includes('"comment"'));
|
|
17
17
|
});
|
|
18
|
-
it('includes assessment options', () => {
|
|
18
|
+
void it('includes assessment options', () => {
|
|
19
19
|
const prompt = createStandaloneReviewSystemPrompt();
|
|
20
20
|
assert.ok(prompt.includes('APPROVE'));
|
|
21
21
|
assert.ok(prompt.includes('REQUEST_CHANGES'));
|
|
22
22
|
assert.ok(prompt.includes('COMMENT'));
|
|
23
23
|
});
|
|
24
|
-
it('does not include feature/checklist instructions', () => {
|
|
24
|
+
void it('does not include feature/checklist instructions', () => {
|
|
25
25
|
const prompt = createStandaloneReviewSystemPrompt();
|
|
26
26
|
assert.ok(!prompt.includes('checklist'));
|
|
27
27
|
assert.ok(!prompt.includes('feature_id'));
|
|
28
28
|
assert.ok(!prompt.includes('user stories'));
|
|
29
29
|
});
|
|
30
30
|
});
|
|
31
|
-
describe('createStandaloneReviewUserPrompt', () => {
|
|
32
|
-
it('includes context info in the prompt', () => {
|
|
31
|
+
void describe('createStandaloneReviewUserPrompt', () => {
|
|
32
|
+
void it('includes context info in the prompt', () => {
|
|
33
33
|
const contextInfo = '# Pull Request\n**Title**: Fix auth bug\n';
|
|
34
34
|
const prompt = createStandaloneReviewUserPrompt(contextInfo);
|
|
35
35
|
assert.ok(prompt.includes('Fix auth bug'));
|
|
36
36
|
});
|
|
37
|
-
it('includes review instructions', () => {
|
|
37
|
+
void it('includes review instructions', () => {
|
|
38
38
|
const prompt = createStandaloneReviewUserPrompt('some context');
|
|
39
39
|
assert.ok(prompt.includes('Analyze Each File'));
|
|
40
40
|
assert.ok(prompt.includes('Identify Issues'));
|
|
41
41
|
assert.ok(prompt.includes('Actionable Feedback'));
|
|
42
42
|
});
|
|
43
|
-
it('mentions severity categories', () => {
|
|
43
|
+
void it('mentions severity categories', () => {
|
|
44
44
|
const prompt = createStandaloneReviewUserPrompt('context');
|
|
45
45
|
assert.ok(prompt.includes('Critical'));
|
|
46
46
|
assert.ok(prompt.includes('Major'));
|
|
@@ -36,7 +36,7 @@ function mapCommentsToReviewPayload(agentComments, files) {
|
|
|
36
36
|
}
|
|
37
37
|
return result;
|
|
38
38
|
}
|
|
39
|
-
describe('review comment mapping (integration)', () => {
|
|
39
|
+
void describe('review comment mapping (integration)', () => {
|
|
40
40
|
const samplePatch = `@@ -1,5 +1,7 @@
|
|
41
41
|
import { useState } from 'react'
|
|
42
42
|
|
|
@@ -47,7 +47,7 @@ describe('review comment mapping (integration)', () => {
|
|
|
47
47
|
+ const [count, setCount] = useState<number>(0)
|
|
48
48
|
return <div>{count}</div>`;
|
|
49
49
|
const files = [{ filename: 'src/App.tsx', patch: samplePatch }];
|
|
50
|
-
it('maps exact line comments to correct diff positions', () => {
|
|
50
|
+
void it('maps exact line comments to correct diff positions', () => {
|
|
51
51
|
const comments = [
|
|
52
52
|
{
|
|
53
53
|
file: 'src/App.tsx',
|
|
@@ -63,7 +63,7 @@ describe('review comment mapping (integration)', () => {
|
|
|
63
63
|
// No "Note" prefix since line was exact
|
|
64
64
|
assert.ok(!result[0].body.includes('**Note**'));
|
|
65
65
|
});
|
|
66
|
-
it('adjusts line numbers when exact line not in diff', () => {
|
|
66
|
+
void it('adjusts line numbers when exact line not in diff', () => {
|
|
67
67
|
// Line 2 is blank (in diff) but line 100 is way outside
|
|
68
68
|
const comments = [{ file: 'src/App.tsx', line: 8, comment: 'Fix this' }];
|
|
69
69
|
const result = mapCommentsToReviewPayload(comments, files);
|
|
@@ -73,7 +73,7 @@ describe('review comment mapping (integration)', () => {
|
|
|
73
73
|
}
|
|
74
74
|
// If no match within range, it's filtered out - also valid
|
|
75
75
|
});
|
|
76
|
-
it('filters out comments for files not in diff', () => {
|
|
76
|
+
void it('filters out comments for files not in diff', () => {
|
|
77
77
|
const comments = [
|
|
78
78
|
{
|
|
79
79
|
file: 'src/other.ts',
|
|
@@ -84,7 +84,7 @@ describe('review comment mapping (integration)', () => {
|
|
|
84
84
|
const result = mapCommentsToReviewPayload(comments, files);
|
|
85
85
|
assert.strictEqual(result.length, 0);
|
|
86
86
|
});
|
|
87
|
-
it('handles multiple comments on same file', () => {
|
|
87
|
+
void it('handles multiple comments on same file', () => {
|
|
88
88
|
const comments = [
|
|
89
89
|
{ file: 'src/App.tsx', line: 3, comment: 'Comment A' },
|
|
90
90
|
{ file: 'src/App.tsx', line: 6, comment: 'Comment B' },
|
|
@@ -94,7 +94,7 @@ describe('review comment mapping (integration)', () => {
|
|
|
94
94
|
assert.ok(result[0].body.includes('Comment A'));
|
|
95
95
|
assert.ok(result[1].body.includes('Comment B'));
|
|
96
96
|
});
|
|
97
|
-
it('handles files with no patch (binary files)', () => {
|
|
97
|
+
void it('handles files with no patch (binary files)', () => {
|
|
98
98
|
const binaryFiles = [
|
|
99
99
|
{ filename: 'image.png' }, // no patch
|
|
100
100
|
{ filename: 'src/App.tsx', patch: samplePatch },
|
|
@@ -14,6 +14,7 @@ import { createStandaloneReviewSystemPrompt, createStandaloneReviewUserPrompt, }
|
|
|
14
14
|
/**
|
|
15
15
|
* Review a standalone PR and post comments to GitHub.
|
|
16
16
|
*/
|
|
17
|
+
// eslint-disable-next-line complexity
|
|
17
18
|
export async function reviewStandalonePR(options) {
|
|
18
19
|
const { pullRequestUrl, githubToken, verbose, prId } = options;
|
|
19
20
|
logInfo(`Starting standalone PR review: ${pullRequestUrl}`);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import { extractTextFromContent, tryExtractResult, tryParseJsonFromResponse, userMessage, } from '../agent-utils.js';
|
|
4
|
-
describe('userMessage', () => {
|
|
5
|
-
it('creates a user message object', () => {
|
|
4
|
+
void describe('userMessage', () => {
|
|
5
|
+
void it('creates a user message object', () => {
|
|
6
6
|
const msg = userMessage('hello');
|
|
7
7
|
assert.deepStrictEqual(msg, {
|
|
8
8
|
type: 'user',
|
|
@@ -10,8 +10,8 @@ describe('userMessage', () => {
|
|
|
10
10
|
});
|
|
11
11
|
});
|
|
12
12
|
});
|
|
13
|
-
describe('extractTextFromContent', () => {
|
|
14
|
-
it('extracts text items from content array', () => {
|
|
13
|
+
void describe('extractTextFromContent', () => {
|
|
14
|
+
void it('extracts text items from content array', () => {
|
|
15
15
|
const content = [
|
|
16
16
|
{ type: 'text', text: 'hello' },
|
|
17
17
|
{ type: 'tool_use', id: '123', name: 'read', input: {} },
|
|
@@ -20,35 +20,35 @@ describe('extractTextFromContent', () => {
|
|
|
20
20
|
const result = extractTextFromContent(content);
|
|
21
21
|
assert.strictEqual(result, 'hello\nworld\n');
|
|
22
22
|
});
|
|
23
|
-
it('returns empty string for no text items', () => {
|
|
23
|
+
void it('returns empty string for no text items', () => {
|
|
24
24
|
const content = [{ type: 'tool_use', id: '123', name: 'read', input: {} }];
|
|
25
25
|
const result = extractTextFromContent(content);
|
|
26
26
|
assert.strictEqual(result, '');
|
|
27
27
|
});
|
|
28
|
-
it('returns empty string for empty array', () => {
|
|
28
|
+
void it('returns empty string for empty array', () => {
|
|
29
29
|
assert.strictEqual(extractTextFromContent([]), '');
|
|
30
30
|
});
|
|
31
31
|
});
|
|
32
|
-
describe('tryParseJsonFromResponse', () => {
|
|
33
|
-
it('parses JSON from code block', () => {
|
|
32
|
+
void describe('tryParseJsonFromResponse', () => {
|
|
33
|
+
void it('parses JSON from code block', () => {
|
|
34
34
|
const text = 'Here is the result:\n```json\n{"key": "value"}\n```\nDone.';
|
|
35
35
|
const result = tryParseJsonFromResponse(text);
|
|
36
36
|
assert.deepStrictEqual(result, { key: 'value' });
|
|
37
37
|
});
|
|
38
|
-
it('parses raw JSON', () => {
|
|
38
|
+
void it('parses raw JSON', () => {
|
|
39
39
|
const text = '{"key": "value"}';
|
|
40
40
|
const result = tryParseJsonFromResponse(text);
|
|
41
41
|
assert.deepStrictEqual(result, { key: 'value' });
|
|
42
42
|
});
|
|
43
|
-
it('returns null for invalid JSON', () => {
|
|
43
|
+
void it('returns null for invalid JSON', () => {
|
|
44
44
|
const result = tryParseJsonFromResponse('not json at all');
|
|
45
45
|
assert.strictEqual(result, null);
|
|
46
46
|
});
|
|
47
|
-
it('returns null for empty string', () => {
|
|
47
|
+
void it('returns null for empty string', () => {
|
|
48
48
|
const result = tryParseJsonFromResponse('');
|
|
49
49
|
assert.strictEqual(result, null);
|
|
50
50
|
});
|
|
51
|
-
it('parses JSON block with surrounding text', () => {
|
|
51
|
+
void it('parses JSON block with surrounding text', () => {
|
|
52
52
|
const text = `I analyzed the code and here are my findings:
|
|
53
53
|
|
|
54
54
|
\`\`\`json
|
|
@@ -66,22 +66,22 @@ That's my review.`;
|
|
|
66
66
|
assert.ok(result.review_result);
|
|
67
67
|
});
|
|
68
68
|
});
|
|
69
|
-
describe('tryExtractResult', () => {
|
|
70
|
-
it('extracts keyed result from JSON code block', () => {
|
|
69
|
+
void describe('tryExtractResult', () => {
|
|
70
|
+
void it('extracts keyed result from JSON code block', () => {
|
|
71
71
|
const text = '```json\n{"review_result": {"summary": "good"}}\n```';
|
|
72
72
|
const result = tryExtractResult(text, 'review_result');
|
|
73
73
|
assert.deepStrictEqual(result, { summary: 'good' });
|
|
74
74
|
});
|
|
75
|
-
it('returns whole object if key not found but JSON valid', () => {
|
|
75
|
+
void it('returns whole object if key not found but JSON valid', () => {
|
|
76
76
|
const text = '{"summary": "good", "comments": []}';
|
|
77
77
|
const result = tryExtractResult(text, 'review_result');
|
|
78
78
|
assert.deepStrictEqual(result, { summary: 'good', comments: [] });
|
|
79
79
|
});
|
|
80
|
-
it('returns null for unparseable text', () => {
|
|
80
|
+
void it('returns null for unparseable text', () => {
|
|
81
81
|
const result = tryExtractResult('no json here', 'review_result');
|
|
82
82
|
assert.strictEqual(result, null);
|
|
83
83
|
});
|
|
84
|
-
it('extracts resolve_result key', () => {
|
|
84
|
+
void it('extracts resolve_result key', () => {
|
|
85
85
|
const text = '```json\n{"resolve_result": {"comments": [{"comment_id": "comment_1", "action": "changed", "reply": "fixed"}]}}\n```';
|
|
86
86
|
const result = tryExtractResult(text, 'resolve_result');
|
|
87
87
|
assert.ok(result);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import { formatStandalonePRContextForPrompt, parsePullRequestUrl, } from '../context.js';
|
|
4
|
-
describe('parsePullRequestUrl (re-exported)', () => {
|
|
5
|
-
it('parses a standard GitHub PR URL', () => {
|
|
4
|
+
void describe('parsePullRequestUrl (re-exported)', () => {
|
|
5
|
+
void it('parses a standard GitHub PR URL', () => {
|
|
6
6
|
const result = parsePullRequestUrl('https://github.com/owner/repo/pull/123');
|
|
7
7
|
assert.deepStrictEqual(result, {
|
|
8
8
|
owner: 'owner',
|
|
@@ -10,7 +10,7 @@ describe('parsePullRequestUrl (re-exported)', () => {
|
|
|
10
10
|
prNumber: 123,
|
|
11
11
|
});
|
|
12
12
|
});
|
|
13
|
-
it('parses URL with trailing path segments', () => {
|
|
13
|
+
void it('parses URL with trailing path segments', () => {
|
|
14
14
|
const result = parsePullRequestUrl('https://github.com/my-org/my-repo/pull/456/files');
|
|
15
15
|
assert.deepStrictEqual(result, {
|
|
16
16
|
owner: 'my-org',
|
|
@@ -18,14 +18,14 @@ describe('parsePullRequestUrl (re-exported)', () => {
|
|
|
18
18
|
prNumber: 456,
|
|
19
19
|
});
|
|
20
20
|
});
|
|
21
|
-
it('returns null for non-PR URL', () => {
|
|
21
|
+
void it('returns null for non-PR URL', () => {
|
|
22
22
|
assert.strictEqual(parsePullRequestUrl('https://github.com/owner/repo/issues/1'), null);
|
|
23
23
|
});
|
|
24
|
-
it('returns null for non-GitHub URL', () => {
|
|
24
|
+
void it('returns null for non-GitHub URL', () => {
|
|
25
25
|
assert.strictEqual(parsePullRequestUrl('https://example.com/pull/1'), null);
|
|
26
26
|
});
|
|
27
27
|
});
|
|
28
|
-
describe('formatStandalonePRContextForPrompt', () => {
|
|
28
|
+
void describe('formatStandalonePRContextForPrompt', () => {
|
|
29
29
|
const context = {
|
|
30
30
|
pullRequestUrl: 'https://github.com/owner/repo/pull/42',
|
|
31
31
|
pullRequestNumber: 42,
|
|
@@ -61,33 +61,33 @@ describe('formatStandalonePRContextForPrompt', () => {
|
|
|
61
61
|
},
|
|
62
62
|
],
|
|
63
63
|
};
|
|
64
|
-
it('includes PR URL and number', () => {
|
|
64
|
+
void it('includes PR URL and number', () => {
|
|
65
65
|
const output = formatStandalonePRContextForPrompt(context);
|
|
66
66
|
assert.ok(output.includes('#42'));
|
|
67
67
|
assert.ok(output.includes('github.com/owner/repo/pull/42'));
|
|
68
68
|
});
|
|
69
|
-
it('includes PR title and author', () => {
|
|
69
|
+
void it('includes PR title and author', () => {
|
|
70
70
|
const output = formatStandalonePRContextForPrompt(context);
|
|
71
71
|
assert.ok(output.includes('Fix bug in auth'));
|
|
72
72
|
assert.ok(output.includes('@testuser'));
|
|
73
73
|
});
|
|
74
|
-
it('includes branch info', () => {
|
|
74
|
+
void it('includes branch info', () => {
|
|
75
75
|
const output = formatStandalonePRContextForPrompt(context);
|
|
76
76
|
assert.ok(output.includes('fix/auth'));
|
|
77
77
|
assert.ok(output.includes('main'));
|
|
78
78
|
});
|
|
79
|
-
it('includes file diff', () => {
|
|
79
|
+
void it('includes file diff', () => {
|
|
80
80
|
const output = formatStandalonePRContextForPrompt(context);
|
|
81
81
|
assert.ok(output.includes('src/auth.ts'));
|
|
82
82
|
assert.ok(output.includes('+added'));
|
|
83
83
|
assert.ok(output.includes('+5 -2'));
|
|
84
84
|
});
|
|
85
|
-
it('includes commit info', () => {
|
|
85
|
+
void it('includes commit info', () => {
|
|
86
86
|
const output = formatStandalonePRContextForPrompt(context);
|
|
87
87
|
assert.ok(output.includes('abc1234'));
|
|
88
88
|
assert.ok(output.includes('fix: auth bug'));
|
|
89
89
|
});
|
|
90
|
-
it('includes PR body/description', () => {
|
|
90
|
+
void it('includes PR body/description', () => {
|
|
91
91
|
const output = formatStandalonePRContextForPrompt(context);
|
|
92
92
|
assert.ok(output.includes('This fixes the login issue'));
|
|
93
93
|
});
|
|
@@ -155,6 +155,9 @@ export function getTransitiveDependencies(file, graph) {
|
|
|
155
155
|
const stack = [file];
|
|
156
156
|
while (stack.length > 0) {
|
|
157
157
|
const current = stack.pop();
|
|
158
|
+
if (current === undefined) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
158
161
|
const deps = graph.get(current);
|
|
159
162
|
if (!deps) {
|
|
160
163
|
continue;
|
|
@@ -169,11 +172,8 @@ export function getTransitiveDependencies(file, graph) {
|
|
|
169
172
|
return result;
|
|
170
173
|
}
|
|
171
174
|
const MAX_FIX_ITERATIONS = 100;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
* Returns true if a file was moved.
|
|
175
|
-
*/
|
|
176
|
-
function moveDepToEarlierPR(dep, prIdx, sorted, fileToPRIndex, movedFiles, reason) {
|
|
175
|
+
function moveDepToEarlierPR(options) {
|
|
176
|
+
const { dep, prIdx, sorted, fileToPRIndex, movedFiles, reason } = options;
|
|
177
177
|
const depPRIdx = fileToPRIndex.get(dep);
|
|
178
178
|
if (depPRIdx === undefined || depPRIdx <= prIdx) {
|
|
179
179
|
return false;
|
|
@@ -229,7 +229,15 @@ export function autoFixPROrdering(pullRequests, dependencyGraph) {
|
|
|
229
229
|
for (const file of sorted[prIdx].files ?? []) {
|
|
230
230
|
const transitiveDeps = getTransitiveDependencies(file.path, dependencyGraph);
|
|
231
231
|
for (const dep of transitiveDeps) {
|
|
232
|
-
const moved = moveDepToEarlierPR(
|
|
232
|
+
const moved = moveDepToEarlierPR({
|
|
233
|
+
dep,
|
|
234
|
+
prIdx,
|
|
235
|
+
sorted,
|
|
236
|
+
fileToPRIndex,
|
|
237
|
+
movedFiles,
|
|
238
|
+
reason: `imported by ${file.path}`,
|
|
239
|
+
});
|
|
240
|
+
// eslint-disable-next-line max-depth
|
|
233
241
|
if (moved) {
|
|
234
242
|
changed = true;
|
|
235
243
|
}
|
|
@@ -27,7 +27,9 @@ async function* prompt(analysisPrompt) {
|
|
|
27
27
|
* then uses AI to produce a PR split plan saved to the database.
|
|
28
28
|
* Human review is expected before running the pr-execution phase.
|
|
29
29
|
*/
|
|
30
|
-
export const splitFeatureIntoPRs = async (options, config
|
|
30
|
+
export const splitFeatureIntoPRs = async (options, config
|
|
31
|
+
// eslint-disable-next-line complexity
|
|
32
|
+
) => {
|
|
31
33
|
const { featureId, verbose, replaceExisting } = options;
|
|
32
34
|
if (verbose) {
|
|
33
35
|
logInfo(`Starting PR splitting for feature ID: ${featureId}`);
|
|
@@ -24,7 +24,9 @@ async function* prompt(analysisPrompt) {
|
|
|
24
24
|
setTimeout(res, 10000);
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
|
-
export const generateTechnicalDesign = async (options, config, checklistContext
|
|
27
|
+
export const generateTechnicalDesign = async (options, config, checklistContext
|
|
28
|
+
// eslint-disable-next-line complexity
|
|
29
|
+
) => {
|
|
28
30
|
const { featureId, verbose } = options;
|
|
29
31
|
if (verbose) {
|
|
30
32
|
logInfo(`Starting technical design generation for feature ID: ${featureId}`);
|
|
@@ -6,7 +6,9 @@ import { executeTestCasesAnalysisQuery, parseAnalysisResult } from './agent.js';
|
|
|
6
6
|
import { prepareTestCasesAnalysisContext } from './context.js';
|
|
7
7
|
import { buildTestCasesAnalysisResult, deleteSpecificTestCases, deleteTestCaseArtifacts, getAllDraftTestCaseIds, resetReadyTestCasesToDraft, saveTestCasesAsDraft, updateTestCasesToReady, } from './outcome.js';
|
|
8
8
|
import { createTestCasesAnalysisSystemPrompt } from './prompts.js';
|
|
9
|
-
export const analyseTestCases = async (options, config, checklistContext
|
|
9
|
+
export const analyseTestCases = async (options, config, checklistContext
|
|
10
|
+
// eslint-disable-next-line complexity
|
|
11
|
+
) => {
|
|
10
12
|
const { featureId, verbose } = options;
|
|
11
13
|
if (verbose) {
|
|
12
14
|
logInfo(`Starting test cases analysis for feature ID: ${featureId}`);
|
|
@@ -6,7 +6,9 @@ import { executeUserStoriesAnalysisQuery, parseAnalysisResult, } from './agent.j
|
|
|
6
6
|
import { prepareUserStoriesAnalysisContext } from './context.js';
|
|
7
7
|
import { buildUserStoriesAnalysisResult, deleteSpecificUserStories, deleteUserStoryArtifacts, getAllDraftUserStoryIds, resetReadyUserStoriesToDraft, saveUserStoriesAsDraft, updateUserStoriesToReady, } from './outcome.js';
|
|
8
8
|
import { createUserStoriesAnalysisSystemPrompt } from './prompts.js';
|
|
9
|
-
export const analyseUserStories = async (options, config, checklistContext
|
|
9
|
+
export const analyseUserStories = async (options, config, checklistContext
|
|
10
|
+
// eslint-disable-next-line complexity
|
|
11
|
+
) => {
|
|
10
12
|
const { featureId, verbose } = options;
|
|
11
13
|
if (verbose) {
|
|
12
14
|
logInfo(`Starting user stories analysis for feature ID: ${featureId}`);
|
|
@@ -163,6 +163,7 @@ void describe('executeHook', () => {
|
|
|
163
163
|
) {
|
|
164
164
|
// Return a function that returns an async iterable
|
|
165
165
|
return () => ({
|
|
166
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
166
167
|
async *[Symbol.asyncIterator]() {
|
|
167
168
|
for (const msg of messages) {
|
|
168
169
|
yield msg;
|
|
@@ -172,7 +173,7 @@ void describe('executeHook', () => {
|
|
|
172
173
|
}
|
|
173
174
|
function makeDeps(overrides = {}) {
|
|
174
175
|
return {
|
|
175
|
-
loadSkillFile:
|
|
176
|
+
loadSkillFile: () => Promise.resolve(defaultSkillFile),
|
|
176
177
|
queryFn: mockQuery([
|
|
177
178
|
{
|
|
178
179
|
type: 'result',
|
|
@@ -184,7 +185,7 @@ void describe('executeHook', () => {
|
|
|
184
185
|
};
|
|
185
186
|
}
|
|
186
187
|
void it('returns skipped when skill file not found', async () => {
|
|
187
|
-
const deps = makeDeps({ loadSkillFile:
|
|
188
|
+
const deps = makeDeps({ loadSkillFile: () => Promise.resolve(null) });
|
|
188
189
|
const result = await executeHook(makeBinding(), makeContext(), false, deps);
|
|
189
190
|
assert.strictEqual(result.status, 'skipped');
|
|
190
191
|
assert.ok(result.message.includes('not found'));
|
|
@@ -266,7 +267,7 @@ void describe('executeHook', () => {
|
|
|
266
267
|
void it('passes correct model and maxTurns from frontmatter', async () => {
|
|
267
268
|
let capturedOptions = {};
|
|
268
269
|
const deps = makeDeps({
|
|
269
|
-
loadSkillFile:
|
|
270
|
+
loadSkillFile: () => Promise.resolve({
|
|
270
271
|
frontmatter: { model: 'haiku', maxTurns: 5 },
|
|
271
272
|
body: 'Test prompt',
|
|
272
273
|
}),
|
|
@@ -274,6 +275,7 @@ void describe('executeHook', () => {
|
|
|
274
275
|
queryFn: ((opts) => {
|
|
275
276
|
capturedOptions = opts.options;
|
|
276
277
|
return {
|
|
278
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
277
279
|
async *[Symbol.asyncIterator]() {
|
|
278
280
|
yield {
|
|
279
281
|
type: 'result',
|
|
@@ -292,7 +294,7 @@ void describe('executeHook', () => {
|
|
|
292
294
|
void it('uses DEFAULT_MODEL when frontmatter has no model', async () => {
|
|
293
295
|
let capturedOptions = {};
|
|
294
296
|
const deps = makeDeps({
|
|
295
|
-
loadSkillFile:
|
|
297
|
+
loadSkillFile: () => Promise.resolve({
|
|
296
298
|
frontmatter: {},
|
|
297
299
|
body: 'Test prompt',
|
|
298
300
|
}),
|
|
@@ -300,6 +302,7 @@ void describe('executeHook', () => {
|
|
|
300
302
|
queryFn: ((opts) => {
|
|
301
303
|
capturedOptions = opts.options;
|
|
302
304
|
return {
|
|
305
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
303
306
|
async *[Symbol.asyncIterator]() {
|
|
304
307
|
yield {
|
|
305
308
|
type: 'result',
|
|
@@ -37,10 +37,10 @@ function makeContext(overrides = {}) {
|
|
|
37
37
|
}
|
|
38
38
|
function makeDeps(overrides = {}) {
|
|
39
39
|
return {
|
|
40
|
-
executeHook:
|
|
40
|
+
executeHook: (binding) => Promise.resolve(makeResult(binding)),
|
|
41
41
|
getCachedBindings: () => null,
|
|
42
42
|
getBindingsForPhase: () => [],
|
|
43
|
-
logHookEvent:
|
|
43
|
+
logHookEvent: () => Promise.resolve(),
|
|
44
44
|
...overrides,
|
|
45
45
|
};
|
|
46
46
|
}
|
|
@@ -86,9 +86,9 @@ void describe('runHooksForPhase', () => {
|
|
|
86
86
|
const deps = makeDeps({
|
|
87
87
|
getCachedBindings: () => cached,
|
|
88
88
|
getBindingsForPhase: () => [binding],
|
|
89
|
-
executeHook:
|
|
89
|
+
executeHook: (b) => {
|
|
90
90
|
executeCalls.push(b);
|
|
91
|
-
return makeResult(b);
|
|
91
|
+
return Promise.resolve(makeResult(b));
|
|
92
92
|
},
|
|
93
93
|
});
|
|
94
94
|
const result = await runHooksForPhase(makeContext(), deps);
|
|
@@ -109,9 +109,9 @@ void describe('runHooksForPhase', () => {
|
|
|
109
109
|
const deps = makeDeps({
|
|
110
110
|
getCachedBindings: () => cached,
|
|
111
111
|
getBindingsForPhase: () => [b1, b2],
|
|
112
|
-
executeHook:
|
|
112
|
+
executeHook: (b) => {
|
|
113
113
|
order.push(b.id);
|
|
114
|
-
return makeResult(b);
|
|
114
|
+
return Promise.resolve(makeResult(b));
|
|
115
115
|
},
|
|
116
116
|
});
|
|
117
117
|
await runHooksForPhase(makeContext(), deps);
|
|
@@ -133,15 +133,15 @@ void describe('runHooksForPhase', () => {
|
|
|
133
133
|
const deps = makeDeps({
|
|
134
134
|
getCachedBindings: () => cached,
|
|
135
135
|
getBindingsForPhase: () => [b1, b2],
|
|
136
|
-
executeHook:
|
|
136
|
+
executeHook: (b) => {
|
|
137
137
|
executedIds.push(b.id);
|
|
138
138
|
if (b.id === 'blocker') {
|
|
139
|
-
return makeResult(b, {
|
|
139
|
+
return Promise.resolve(makeResult(b, {
|
|
140
140
|
status: 'error',
|
|
141
141
|
message: 'Validation failed',
|
|
142
|
-
});
|
|
142
|
+
}));
|
|
143
143
|
}
|
|
144
|
-
return makeResult(b);
|
|
144
|
+
return Promise.resolve(makeResult(b));
|
|
145
145
|
},
|
|
146
146
|
});
|
|
147
147
|
const result = await runHooksForPhase(makeContext(), deps);
|
|
@@ -165,14 +165,14 @@ void describe('runHooksForPhase', () => {
|
|
|
165
165
|
const deps = makeDeps({
|
|
166
166
|
getCachedBindings: () => cached,
|
|
167
167
|
getBindingsForPhase: () => [b1, b2],
|
|
168
|
-
executeHook:
|
|
168
|
+
executeHook: (b) => {
|
|
169
169
|
if (b.id === 'warner') {
|
|
170
|
-
return makeResult(b, {
|
|
170
|
+
return Promise.resolve(makeResult(b, {
|
|
171
171
|
status: 'error',
|
|
172
172
|
message: 'Non-critical issue',
|
|
173
|
-
});
|
|
173
|
+
}));
|
|
174
174
|
}
|
|
175
|
-
return makeResult(b);
|
|
175
|
+
return Promise.resolve(makeResult(b));
|
|
176
176
|
},
|
|
177
177
|
});
|
|
178
178
|
const result = await runHooksForPhase(makeContext(), deps);
|
|
@@ -192,11 +192,11 @@ void describe('runHooksForPhase', () => {
|
|
|
192
192
|
const deps = makeDeps({
|
|
193
193
|
getCachedBindings: () => cached,
|
|
194
194
|
getBindingsForPhase: () => [b1, b2],
|
|
195
|
-
executeHook:
|
|
195
|
+
executeHook: (b) => {
|
|
196
196
|
if (b.id === 'skipper') {
|
|
197
|
-
return makeResult(b, { status: 'error', message: 'Ignored' });
|
|
197
|
+
return Promise.resolve(makeResult(b, { status: 'error', message: 'Ignored' }));
|
|
198
198
|
}
|
|
199
|
-
return makeResult(b);
|
|
199
|
+
return Promise.resolve(makeResult(b));
|
|
200
200
|
},
|
|
201
201
|
});
|
|
202
202
|
const result = await runHooksForPhase(makeContext(), deps);
|
|
@@ -214,8 +214,9 @@ void describe('runHooksForPhase', () => {
|
|
|
214
214
|
const deps = makeDeps({
|
|
215
215
|
getCachedBindings: () => cached,
|
|
216
216
|
getBindingsForPhase: () => [binding],
|
|
217
|
-
logHookEvent:
|
|
217
|
+
logHookEvent: ({ result: r }) => {
|
|
218
218
|
logCalls.push(r.hookId);
|
|
219
|
+
return Promise.resolve();
|
|
219
220
|
},
|
|
220
221
|
});
|
|
221
222
|
await runHooksForPhase(makeContext(), deps);
|
|
@@ -231,8 +232,8 @@ void describe('runHooksForPhase', () => {
|
|
|
231
232
|
const deps = makeDeps({
|
|
232
233
|
getCachedBindings: () => cached,
|
|
233
234
|
getBindingsForPhase: () => [binding],
|
|
234
|
-
logHookEvent:
|
|
235
|
-
|
|
235
|
+
logHookEvent: () => {
|
|
236
|
+
return Promise.reject(new Error('Logging failed'));
|
|
236
237
|
},
|
|
237
238
|
});
|
|
238
239
|
const result = await runHooksForPhase(makeContext(), deps);
|
|
@@ -250,7 +251,7 @@ void describe('runHooksForPhase', () => {
|
|
|
250
251
|
const deps = makeDeps({
|
|
251
252
|
getCachedBindings: () => cached,
|
|
252
253
|
getBindingsForPhase: () => [binding],
|
|
253
|
-
executeHook:
|
|
254
|
+
executeHook: (b) => Promise.resolve(makeResult(b, { status: 'skipped', message: 'Not found' })),
|
|
254
255
|
});
|
|
255
256
|
const result = await runHooksForPhase(makeContext(), deps);
|
|
256
257
|
// 'skipped' is not an error, so on_failure policy should not apply
|
|
@@ -94,6 +94,7 @@ export async function executeHook(binding, context, verbose, deps = defaultDeps)
|
|
|
94
94
|
if (message.type === 'assistant') {
|
|
95
95
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
96
96
|
for (const item of (message.message?.content ?? [])) {
|
|
97
|
+
// eslint-disable-next-line max-depth
|
|
97
98
|
if (item.type === 'text') {
|
|
98
99
|
resultText += item.text;
|
|
99
100
|
}
|