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
|
@@ -26,8 +26,8 @@ function makeMap(entries) {
|
|
|
26
26
|
return new Map(entries);
|
|
27
27
|
}
|
|
28
28
|
// ── buildLearnerPrompt ──────────────────────────────────────
|
|
29
|
-
describe('buildLearnerPrompt', () => {
|
|
30
|
-
it('includes addressed comment count in header', () => {
|
|
29
|
+
void describe('buildLearnerPrompt', () => {
|
|
30
|
+
void it('includes addressed comment count in header', () => {
|
|
31
31
|
const comments = [
|
|
32
32
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
33
33
|
{ comment_id: 'comment_2', action: 'changed', reply: 'Done' },
|
|
@@ -40,7 +40,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
40
40
|
const prompt = buildLearnerPrompt(comments, threads, map);
|
|
41
41
|
assert.ok(prompt.includes('2 review comment(s)'));
|
|
42
42
|
});
|
|
43
|
-
it('includes reviewer, file path, line, and comment body', () => {
|
|
43
|
+
void it('includes reviewer, file path, line, and comment body', () => {
|
|
44
44
|
const comments = [
|
|
45
45
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Added null check' },
|
|
46
46
|
];
|
|
@@ -60,7 +60,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
60
60
|
assert.ok(prompt.includes('Missing null check here'));
|
|
61
61
|
assert.ok(prompt.includes('Added null check'));
|
|
62
62
|
});
|
|
63
|
-
it('includes resolution text for each comment', () => {
|
|
63
|
+
void it('includes resolution text for each comment', () => {
|
|
64
64
|
const comments = [
|
|
65
65
|
{
|
|
66
66
|
comment_id: 'comment_1',
|
|
@@ -73,7 +73,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
73
73
|
const prompt = buildLearnerPrompt(comments, threads, map);
|
|
74
74
|
assert.ok(prompt.includes('**Resolution**: Refactored to use try/catch'));
|
|
75
75
|
});
|
|
76
|
-
it('includes summary when provided', () => {
|
|
76
|
+
void it('includes summary when provided', () => {
|
|
77
77
|
const comments = [
|
|
78
78
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
79
79
|
];
|
|
@@ -83,7 +83,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
83
83
|
assert.ok(prompt.includes('## Overall Summary'));
|
|
84
84
|
assert.ok(prompt.includes('Improved error handling across 3 files'));
|
|
85
85
|
});
|
|
86
|
-
it('omits summary section when not provided', () => {
|
|
86
|
+
void it('omits summary section when not provided', () => {
|
|
87
87
|
const comments = [
|
|
88
88
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
89
89
|
];
|
|
@@ -92,7 +92,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
92
92
|
const prompt = buildLearnerPrompt(comments, threads, map);
|
|
93
93
|
assert.ok(!prompt.includes('## Overall Summary'));
|
|
94
94
|
});
|
|
95
|
-
it('handles comment with no matching thread gracefully', () => {
|
|
95
|
+
void it('handles comment with no matching thread gracefully', () => {
|
|
96
96
|
const comments = [
|
|
97
97
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
98
98
|
];
|
|
@@ -105,7 +105,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
105
105
|
// Should NOT include reviewer info since thread was not found
|
|
106
106
|
assert.ok(!prompt.includes('**Reviewer**'));
|
|
107
107
|
});
|
|
108
|
-
it('handles comment_id not in map gracefully', () => {
|
|
108
|
+
void it('handles comment_id not in map gracefully', () => {
|
|
109
109
|
const comments = [
|
|
110
110
|
{ comment_id: 'comment_99', action: 'changed', reply: 'Fixed' },
|
|
111
111
|
];
|
|
@@ -113,7 +113,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
113
113
|
assert.ok(prompt.includes('## comment_99'));
|
|
114
114
|
assert.ok(prompt.includes('**Resolution**: Fixed'));
|
|
115
115
|
});
|
|
116
|
-
it('omits line when null', () => {
|
|
116
|
+
void it('omits line when null', () => {
|
|
117
117
|
const comments = [
|
|
118
118
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
119
119
|
];
|
|
@@ -123,7 +123,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
123
123
|
assert.ok(prompt.includes('**File**: src/index.ts'));
|
|
124
124
|
assert.ok(!prompt.includes('**Line**'));
|
|
125
125
|
});
|
|
126
|
-
it('uses threadById map correctly for multiple comments', () => {
|
|
126
|
+
void it('uses threadById map correctly for multiple comments', () => {
|
|
127
127
|
const comments = [
|
|
128
128
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Fix A' },
|
|
129
129
|
{ comment_id: 'comment_3', action: 'changed', reply: 'Fix C' },
|
|
@@ -146,7 +146,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
146
146
|
// comment_2 was not in addressedComments, should not appear
|
|
147
147
|
assert.ok(!prompt.includes('Issue B'));
|
|
148
148
|
});
|
|
149
|
-
it('includes instructions section', () => {
|
|
149
|
+
void it('includes instructions section', () => {
|
|
150
150
|
const comments = [
|
|
151
151
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
152
152
|
];
|
|
@@ -34,26 +34,26 @@ function makeThread(id, overrides) {
|
|
|
34
34
|
},
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
describe('createResolveSystemPrompt', () => {
|
|
38
|
-
it('includes decision criteria', () => {
|
|
37
|
+
void describe('createResolveSystemPrompt', () => {
|
|
38
|
+
void it('includes decision criteria', () => {
|
|
39
39
|
const prompt = createResolveSystemPrompt();
|
|
40
40
|
assert.ok(prompt.includes('Make the change when'));
|
|
41
41
|
assert.ok(prompt.includes('Skip the change when'));
|
|
42
42
|
});
|
|
43
|
-
it('specifies comment_id format', () => {
|
|
43
|
+
void it('specifies comment_id format', () => {
|
|
44
44
|
const prompt = createResolveSystemPrompt();
|
|
45
45
|
assert.ok(prompt.includes('comment_id'));
|
|
46
46
|
assert.ok(prompt.includes('comment_1'));
|
|
47
47
|
});
|
|
48
|
-
it('includes result format', () => {
|
|
48
|
+
void it('includes result format', () => {
|
|
49
49
|
const prompt = createResolveSystemPrompt();
|
|
50
50
|
assert.ok(prompt.includes('resolve_result'));
|
|
51
51
|
assert.ok(prompt.includes('"action"'));
|
|
52
52
|
assert.ok(prompt.includes('"reply"'));
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
|
-
describe('createResolveUserPrompt', () => {
|
|
56
|
-
it('uses sequential comment IDs not thread IDs', () => {
|
|
55
|
+
void describe('createResolveUserPrompt', () => {
|
|
56
|
+
void it('uses sequential comment IDs not thread IDs', () => {
|
|
57
57
|
const threads = [makeThread('PRRT_kwDOxx_1'), makeThread('PRRT_kwDOxx_2')];
|
|
58
58
|
const { prompt, commentIdToThreadId } = createResolveUserPrompt(threads);
|
|
59
59
|
// Should use comment_1, comment_2 in the prompt
|
|
@@ -67,20 +67,20 @@ describe('createResolveUserPrompt', () => {
|
|
|
67
67
|
assert.strictEqual(commentIdToThreadId.get('comment_2'), 'PRRT_kwDOxx_2');
|
|
68
68
|
assert.strictEqual(commentIdToThreadId.size, 2);
|
|
69
69
|
});
|
|
70
|
-
it('includes file path and line number', () => {
|
|
70
|
+
void it('includes file path and line number', () => {
|
|
71
71
|
const threads = [makeThread('t1', { path: 'src/auth.ts', line: 42 })];
|
|
72
72
|
const { prompt } = createResolveUserPrompt(threads);
|
|
73
73
|
assert.ok(prompt.includes('src/auth.ts'));
|
|
74
74
|
assert.ok(prompt.includes('42'));
|
|
75
75
|
});
|
|
76
|
-
it('includes comment body', () => {
|
|
76
|
+
void it('includes comment body', () => {
|
|
77
77
|
const threads = [
|
|
78
78
|
makeThread('t1', { body: 'This should use a const instead of let' }),
|
|
79
79
|
];
|
|
80
80
|
const { prompt } = createResolveUserPrompt(threads);
|
|
81
81
|
assert.ok(prompt.includes('This should use a const instead of let'));
|
|
82
82
|
});
|
|
83
|
-
it('includes follow-up comments', () => {
|
|
83
|
+
void it('includes follow-up comments', () => {
|
|
84
84
|
const threads = [
|
|
85
85
|
makeThread('t1', {
|
|
86
86
|
body: 'Main comment',
|
|
@@ -92,12 +92,12 @@ describe('createResolveUserPrompt', () => {
|
|
|
92
92
|
assert.ok(prompt.includes('I disagree because...'));
|
|
93
93
|
assert.ok(prompt.includes('@dev'));
|
|
94
94
|
});
|
|
95
|
-
it('includes instruction to use exact comment IDs', () => {
|
|
95
|
+
void it('includes instruction to use exact comment IDs', () => {
|
|
96
96
|
const threads = [makeThread('t1'), makeThread('t2'), makeThread('t3')];
|
|
97
97
|
const { prompt } = createResolveUserPrompt(threads);
|
|
98
98
|
assert.ok(prompt.includes('comment_1, comment_2, comment_3'));
|
|
99
99
|
});
|
|
100
|
-
it('handles threads with no comments gracefully', () => {
|
|
100
|
+
void it('handles threads with no comments gracefully', () => {
|
|
101
101
|
const emptyThread = {
|
|
102
102
|
id: 'empty',
|
|
103
103
|
isResolved: false,
|
|
@@ -108,7 +108,7 @@ describe('createResolveUserPrompt', () => {
|
|
|
108
108
|
// Empty thread should be skipped (no comment nodes to index)
|
|
109
109
|
assert.strictEqual(commentIdToThreadId.size, 0);
|
|
110
110
|
});
|
|
111
|
-
it('returns correct count in header', () => {
|
|
111
|
+
void it('returns correct count in header', () => {
|
|
112
112
|
const threads = [makeThread('t1'), makeThread('t2')];
|
|
113
113
|
const { prompt } = createResolveUserPrompt(threads);
|
|
114
114
|
assert.ok(prompt.includes('2 unresolved review comment(s)'));
|
|
@@ -45,8 +45,8 @@ function makeThread(id, body) {
|
|
|
45
45
|
},
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
describe('resolve comment→thread mapping (integration)', () => {
|
|
49
|
-
it('correctly maps comment_ids to thread IDs', () => {
|
|
48
|
+
void describe('resolve comment→thread mapping (integration)', () => {
|
|
49
|
+
void it('correctly maps comment_ids to thread IDs', () => {
|
|
50
50
|
const threads = [
|
|
51
51
|
makeThread('PRRT_aaa', 'Use const'),
|
|
52
52
|
makeThread('PRRT_bbb', 'Add error handling'),
|
|
@@ -75,7 +75,7 @@ describe('resolve comment→thread mapping (integration)', () => {
|
|
|
75
75
|
assert.strictEqual(result.addressed[1].threadId, 'PRRT_bbb');
|
|
76
76
|
assert.strictEqual(result.skipped[0].threadId, 'PRRT_ccc');
|
|
77
77
|
});
|
|
78
|
-
it('reports errors for unknown comment_ids', () => {
|
|
78
|
+
void it('reports errors for unknown comment_ids', () => {
|
|
79
79
|
const threads = [makeThread('PRRT_aaa', 'Fix bug')];
|
|
80
80
|
const { commentIdToThreadId } = createResolveUserPrompt(threads);
|
|
81
81
|
const agentResult = [
|
|
@@ -87,7 +87,7 @@ describe('resolve comment→thread mapping (integration)', () => {
|
|
|
87
87
|
assert.strictEqual(result.errors.length, 1);
|
|
88
88
|
assert.ok(result.errors[0].includes('comment_99'));
|
|
89
89
|
});
|
|
90
|
-
it('handles agent returning partial results', () => {
|
|
90
|
+
void it('handles agent returning partial results', () => {
|
|
91
91
|
const threads = [
|
|
92
92
|
makeThread('PRRT_aaa', 'Fix A'),
|
|
93
93
|
makeThread('PRRT_bbb', 'Fix B'),
|
|
@@ -105,7 +105,7 @@ describe('resolve comment→thread mapping (integration)', () => {
|
|
|
105
105
|
assert.strictEqual(result.errors.length, 0);
|
|
106
106
|
// comment_2 was not mentioned - no error, just not processed
|
|
107
107
|
});
|
|
108
|
-
it('handles empty agent result', () => {
|
|
108
|
+
void it('handles empty agent result', () => {
|
|
109
109
|
const threads = [makeThread('PRRT_aaa', 'Fix')];
|
|
110
110
|
const { commentIdToThreadId } = createResolveUserPrompt(threads);
|
|
111
111
|
const result = processResolveResult([], commentIdToThreadId);
|
|
@@ -113,7 +113,7 @@ describe('resolve comment→thread mapping (integration)', () => {
|
|
|
113
113
|
assert.strictEqual(result.skipped.length, 0);
|
|
114
114
|
assert.strictEqual(result.errors.length, 0);
|
|
115
115
|
});
|
|
116
|
-
it('preserves reply text in all cases', () => {
|
|
116
|
+
void it('preserves reply text in all cases', () => {
|
|
117
117
|
const threads = [
|
|
118
118
|
makeThread('PRRT_aaa', 'Fix this'),
|
|
119
119
|
makeThread('PRRT_bbb', 'Change that'),
|
|
@@ -1,43 +1,43 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import { isResolveResult } from '../types.js';
|
|
4
|
-
describe('isResolveResult', () => {
|
|
5
|
-
it('returns true for valid ResolveResult', () => {
|
|
4
|
+
void describe('isResolveResult', () => {
|
|
5
|
+
void it('returns true for valid ResolveResult', () => {
|
|
6
6
|
assert.ok(isResolveResult({
|
|
7
7
|
comments: [
|
|
8
8
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
9
9
|
],
|
|
10
10
|
}));
|
|
11
11
|
});
|
|
12
|
-
it('returns true when optional fields are present', () => {
|
|
12
|
+
void it('returns true when optional fields are present', () => {
|
|
13
13
|
assert.ok(isResolveResult({
|
|
14
14
|
comments: [],
|
|
15
15
|
files_modified: ['a.ts'],
|
|
16
16
|
summary: 'Done',
|
|
17
17
|
}));
|
|
18
18
|
});
|
|
19
|
-
it('returns true for empty comments array', () => {
|
|
19
|
+
void it('returns true for empty comments array', () => {
|
|
20
20
|
assert.ok(isResolveResult({ comments: [] }));
|
|
21
21
|
});
|
|
22
|
-
it('returns false for null', () => {
|
|
22
|
+
void it('returns false for null', () => {
|
|
23
23
|
assert.ok(!isResolveResult(null));
|
|
24
24
|
});
|
|
25
|
-
it('returns false for undefined', () => {
|
|
25
|
+
void it('returns false for undefined', () => {
|
|
26
26
|
assert.ok(!isResolveResult(undefined));
|
|
27
27
|
});
|
|
28
|
-
it('returns false for string', () => {
|
|
28
|
+
void it('returns false for string', () => {
|
|
29
29
|
assert.ok(!isResolveResult('not an object'));
|
|
30
30
|
});
|
|
31
|
-
it('returns false for number', () => {
|
|
31
|
+
void it('returns false for number', () => {
|
|
32
32
|
assert.ok(!isResolveResult(42));
|
|
33
33
|
});
|
|
34
|
-
it('returns false when comments is missing', () => {
|
|
34
|
+
void it('returns false when comments is missing', () => {
|
|
35
35
|
assert.ok(!isResolveResult({ files_modified: ['a.ts'] }));
|
|
36
36
|
});
|
|
37
|
-
it('returns false when comments is not an array', () => {
|
|
37
|
+
void it('returns false when comments is not an array', () => {
|
|
38
38
|
assert.ok(!isResolveResult({ comments: 'not-array' }));
|
|
39
39
|
});
|
|
40
|
-
it('returns false for empty object', () => {
|
|
40
|
+
void it('returns false for empty object', () => {
|
|
41
41
|
assert.ok(!isResolveResult({}));
|
|
42
42
|
});
|
|
43
43
|
});
|
|
@@ -16,8 +16,8 @@ function createTempRepo() {
|
|
|
16
16
|
execSync('git commit -m "init"', { cwd: dir, stdio: 'pipe' });
|
|
17
17
|
return dir;
|
|
18
18
|
}
|
|
19
|
-
describe('buildCredentialArgs', () => {
|
|
20
|
-
it('returns 4 args with credential helper config', () => {
|
|
19
|
+
void describe('buildCredentialArgs', () => {
|
|
20
|
+
void it('returns 4 args with credential helper config', () => {
|
|
21
21
|
const args = buildCredentialArgs('my-token');
|
|
22
22
|
assert.strictEqual(args.length, 4);
|
|
23
23
|
assert.strictEqual(args[0], '-c');
|
|
@@ -26,12 +26,12 @@ describe('buildCredentialArgs', () => {
|
|
|
26
26
|
assert.ok(args[3].includes('my-token'));
|
|
27
27
|
assert.ok(args[3].includes('x-access-token'));
|
|
28
28
|
});
|
|
29
|
-
it('escapes token in credential helper', () => {
|
|
29
|
+
void it('escapes token in credential helper', () => {
|
|
30
30
|
const args = buildCredentialArgs('token-with-special-chars!@#');
|
|
31
31
|
assert.ok(args[3].includes('token-with-special-chars!@#'));
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
|
-
describe('hasUncommittedChanges', () => {
|
|
34
|
+
void describe('hasUncommittedChanges', () => {
|
|
35
35
|
let repoPath;
|
|
36
36
|
beforeEach(() => {
|
|
37
37
|
repoPath = createTempRepo();
|
|
@@ -39,18 +39,18 @@ describe('hasUncommittedChanges', () => {
|
|
|
39
39
|
afterEach(() => {
|
|
40
40
|
rmSync(repoPath, { recursive: true, force: true });
|
|
41
41
|
});
|
|
42
|
-
it('returns false for clean repo', () => {
|
|
42
|
+
void it('returns false for clean repo', () => {
|
|
43
43
|
assert.strictEqual(hasUncommittedChanges(repoPath), false);
|
|
44
44
|
});
|
|
45
|
-
it('returns true after modifying a file', () => {
|
|
45
|
+
void it('returns true after modifying a file', () => {
|
|
46
46
|
writeFileSync(join(repoPath, 'README.md'), '# Modified');
|
|
47
47
|
assert.strictEqual(hasUncommittedChanges(repoPath), true);
|
|
48
48
|
});
|
|
49
|
-
it('returns true for new untracked file', () => {
|
|
49
|
+
void it('returns true for new untracked file', () => {
|
|
50
50
|
writeFileSync(join(repoPath, 'new-file.txt'), 'content');
|
|
51
51
|
assert.strictEqual(hasUncommittedChanges(repoPath), true);
|
|
52
52
|
});
|
|
53
|
-
it('returns false after staging and committing', () => {
|
|
53
|
+
void it('returns false after staging and committing', () => {
|
|
54
54
|
writeFileSync(join(repoPath, 'new.txt'), 'x');
|
|
55
55
|
execSync('git add . && git commit -m "add"', {
|
|
56
56
|
cwd: repoPath,
|
|
@@ -58,11 +58,11 @@ describe('hasUncommittedChanges', () => {
|
|
|
58
58
|
});
|
|
59
59
|
assert.strictEqual(hasUncommittedChanges(repoPath), false);
|
|
60
60
|
});
|
|
61
|
-
it('returns false for non-existent path', () => {
|
|
61
|
+
void it('returns false for non-existent path', () => {
|
|
62
62
|
assert.strictEqual(hasUncommittedChanges('/tmp/nonexistent-repo-xyz'), false);
|
|
63
63
|
});
|
|
64
64
|
});
|
|
65
|
-
describe('hasNewCommits', () => {
|
|
65
|
+
void describe('hasNewCommits', () => {
|
|
66
66
|
let repoPath;
|
|
67
67
|
let bareRemote;
|
|
68
68
|
beforeEach(() => {
|
|
@@ -89,10 +89,10 @@ describe('hasNewCommits', () => {
|
|
|
89
89
|
rmSync(repoPath, { recursive: true, force: true });
|
|
90
90
|
rmSync(bareRemote, { recursive: true, force: true });
|
|
91
91
|
});
|
|
92
|
-
it('returns false when HEAD matches remote', () => {
|
|
92
|
+
void it('returns false when HEAD matches remote', () => {
|
|
93
93
|
assert.strictEqual(hasNewCommits(repoPath, 'main'), false);
|
|
94
94
|
});
|
|
95
|
-
it('returns true after a local commit not pushed', () => {
|
|
95
|
+
void it('returns true after a local commit not pushed', () => {
|
|
96
96
|
writeFileSync(join(repoPath, 'new.txt'), 'hello');
|
|
97
97
|
execSync('git add . && git commit -m "local"', {
|
|
98
98
|
cwd: repoPath,
|
|
@@ -100,7 +100,7 @@ describe('hasNewCommits', () => {
|
|
|
100
100
|
});
|
|
101
101
|
assert.strictEqual(hasNewCommits(repoPath, 'main'), true);
|
|
102
102
|
});
|
|
103
|
-
it('returns false after pushing local commit', () => {
|
|
103
|
+
void it('returns false after pushing local commit', () => {
|
|
104
104
|
writeFileSync(join(repoPath, 'new.txt'), 'hello');
|
|
105
105
|
execSync('git add . && git commit -m "local" && git push origin main', {
|
|
106
106
|
cwd: repoPath,
|
|
@@ -9,24 +9,48 @@
|
|
|
9
9
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
10
10
|
import { createChecklistsMcpServer } from '../../commands/checklists/tools.js';
|
|
11
11
|
import { logInfo, logWarning } from '../../utils/logger.js';
|
|
12
|
-
const LEARNER_SYSTEM_PROMPT = `You are a software quality engineer. Your task is to analyse review comments that were addressed during a PR resolve and distil them into actionable checklist items
|
|
12
|
+
const LEARNER_SYSTEM_PROMPT = `You are a software quality engineer. Your task is to analyse review comments that were addressed during a PR resolve and distil them into actionable checklist items — assigned to the most appropriate phase(s) so issues are caught as early as possible.
|
|
13
|
+
|
|
14
|
+
## Available Phases (earliest → latest)
|
|
15
|
+
|
|
16
|
+
| Phase | When it runs | Good for |
|
|
17
|
+
|-------|-------------|----------|
|
|
18
|
+
| \`feature_analysis\` | Requirements gathering | Requirement gaps, scope issues, edge cases |
|
|
19
|
+
| \`user_stories_analysis\` | User story refinement | Acceptance criteria gaps, missing scenarios |
|
|
20
|
+
| \`test_cases_analysis\` | Test planning | Missing test coverage, untested edge cases |
|
|
21
|
+
| \`technical_design\` | Architecture & design | Design flaws, API contract issues, data modelling |
|
|
22
|
+
| \`branch_planning\` | Work breakdown | Dependency issues, ordering problems |
|
|
23
|
+
| \`code_implementation\` | Writing code | Coding patterns, error handling, security, performance |
|
|
24
|
+
| \`code_testing\` | Unit/integration tests | Test quality, assertion coverage, mocking issues |
|
|
25
|
+
| \`code_refine\` | Code polish | Readability, naming, documentation |
|
|
26
|
+
| \`code_review\` | Final review | Cross-cutting concerns not caught earlier |
|
|
13
27
|
|
|
14
28
|
## Workflow
|
|
15
29
|
|
|
16
30
|
1. **Read** the addressed review comments provided below.
|
|
17
|
-
2. **Query existing checklists** using \`list_checklists\`
|
|
31
|
+
2. **Query existing checklists** using \`list_checklists\` (without a phase filter) to see what already exists across all phases.
|
|
18
32
|
3. **Identify patterns** — group related comments into categories (e.g., error handling, naming, security, performance, testing).
|
|
19
|
-
4. **
|
|
20
|
-
|
|
21
|
-
- If
|
|
22
|
-
|
|
33
|
+
4. **Decide the best phase(s)** for each pattern — prefer earlier phases where the issue could have been prevented. A checklist item can belong to multiple phases if appropriate.
|
|
34
|
+
5. **Update or create checklist items**:
|
|
35
|
+
- If an existing checklist covers the category and phase, add new items to it (skip if a similar item already exists).
|
|
36
|
+
- If no suitable checklist exists, create a new one with the appropriate phases.
|
|
37
|
+
6. **Summarise** what you added or updated, and explain why you chose each phase.
|
|
38
|
+
|
|
39
|
+
## Phase Selection Guidelines
|
|
40
|
+
|
|
41
|
+
- **Shift left**: If an issue could have been prevented in an earlier phase, assign it there. For example, a missing null check found in code review should become a \`code_implementation\` checklist item.
|
|
42
|
+
- **Security & error handling** → typically \`code_implementation\`
|
|
43
|
+
- **Missing test cases or edge cases** → \`test_cases_analysis\` or \`code_testing\`
|
|
44
|
+
- **Design or architecture issues** → \`technical_design\`
|
|
45
|
+
- **Missing requirements or acceptance criteria** → \`feature_analysis\` or \`user_stories_analysis\`
|
|
46
|
+
- **Code style, readability, naming** → \`code_refine\`
|
|
47
|
+
- **Only use \`code_review\`** for cross-cutting concerns that genuinely can't be checked earlier.
|
|
23
48
|
|
|
24
49
|
## Rules
|
|
25
50
|
|
|
26
|
-
- Only create items for **genuine quality patterns** — things that should be checked
|
|
51
|
+
- Only create items for **genuine quality patterns** — things that should be checked repeatedly.
|
|
27
52
|
- Skip one-off nits, purely stylistic preferences, or context-specific fixes that won't generalise.
|
|
28
53
|
- Role: \`developer\`
|
|
29
|
-
- Phases: \`["code_review"]\`
|
|
30
54
|
- Item type: \`boolean\` (yes/no checkable)
|
|
31
55
|
- Keep item titles concise (< 80 chars). Use the description for details.
|
|
32
56
|
- Do NOT duplicate items that already exist in the checklists.
|
|
@@ -72,7 +96,7 @@ export function buildLearnerPrompt(addressedComments, unresolvedThreads, comment
|
|
|
72
96
|
sections.push('');
|
|
73
97
|
}
|
|
74
98
|
sections.push('## Instructions');
|
|
75
|
-
sections.push('Based on the patterns above, query existing
|
|
99
|
+
sections.push('Based on the patterns above, query existing checklists across all phases and create or update items in the most appropriate phase(s) to prevent these issues from recurring. Prefer earlier phases where the issue could have been caught sooner.');
|
|
76
100
|
return sections.join('\n');
|
|
77
101
|
}
|
|
78
102
|
/**
|
|
@@ -112,6 +136,7 @@ export async function learnFromReviewFeedback(input) {
|
|
|
112
136
|
if (message.type === 'result') {
|
|
113
137
|
if (message.subtype === 'success') {
|
|
114
138
|
logInfo('Checklist learning completed.');
|
|
139
|
+
// eslint-disable-next-line max-depth
|
|
115
140
|
if (verbose && message.result) {
|
|
116
141
|
logInfo(message.result);
|
|
117
142
|
}
|
|
@@ -22,6 +22,7 @@ export { isResolveResult } from './types.js';
|
|
|
22
22
|
/**
|
|
23
23
|
* Resolve PR change requests: evaluate each comment, fix or explain.
|
|
24
24
|
*/
|
|
25
|
+
// eslint-disable-next-line complexity
|
|
25
26
|
export async function resolveStandalonePR(options) {
|
|
26
27
|
const { pullRequestUrl, githubToken, owner, repo, verbose, prId } = options;
|
|
27
28
|
logInfo(`Starting PR resolve: ${pullRequestUrl}`);
|
|
@@ -53,8 +54,33 @@ export async function resolveStandalonePR(options) {
|
|
|
53
54
|
pull_number: prInfo.prNumber,
|
|
54
55
|
});
|
|
55
56
|
const headRef = prData.head.ref;
|
|
56
|
-
//
|
|
57
|
-
|
|
57
|
+
// Determine clone source: fork PRs need to clone from the fork repo.
|
|
58
|
+
// head.repo is null when the fork has been deleted — treat as fork with deleted source.
|
|
59
|
+
const isFork = !prData.head.repo ||
|
|
60
|
+
prData.head.repo.full_name !== prData.base.repo.full_name;
|
|
61
|
+
const forkDeleted = !prData.head.repo;
|
|
62
|
+
const cloneOwner = isFork && !forkDeleted ? prData.head.repo.owner.login : owner;
|
|
63
|
+
const cloneRepo = isFork && !forkDeleted ? prData.head.repo.name : repo;
|
|
64
|
+
if (isFork && verbose) {
|
|
65
|
+
logInfo(forkDeleted
|
|
66
|
+
? `Fork PR detected but fork repo has been deleted, will use PR ref from ${owner}/${repo}`
|
|
67
|
+
: `Fork PR detected: cloning from ${cloneOwner}/${cloneRepo} instead of ${owner}/${repo}`);
|
|
68
|
+
}
|
|
69
|
+
// Clone repo and checkout PR branch.
|
|
70
|
+
// For fork PRs, pass fallback info so prepareWorkspace can fetch the PR ref
|
|
71
|
+
// from upstream if the fork branch is unavailable.
|
|
72
|
+
// For deleted forks, clone upstream directly and always use PR ref fallback.
|
|
73
|
+
const repoPath = prepareWorkspace({
|
|
74
|
+
owner: cloneOwner,
|
|
75
|
+
repo: cloneRepo,
|
|
76
|
+
headRef,
|
|
77
|
+
prNumber: prInfo.prNumber,
|
|
78
|
+
token: githubToken,
|
|
79
|
+
verbose,
|
|
80
|
+
forkFallback: isFork
|
|
81
|
+
? { upstreamOwner: owner, upstreamRepo: repo }
|
|
82
|
+
: undefined,
|
|
83
|
+
});
|
|
58
84
|
try {
|
|
59
85
|
// Run Claude Agent SDK to evaluate and fix comments
|
|
60
86
|
const systemPrompt = createResolveSystemPrompt();
|
|
@@ -87,6 +113,7 @@ export async function resolveStandalonePR(options) {
|
|
|
87
113
|
logInfo('Agent completed, parsing results...');
|
|
88
114
|
const responseText = message.result || lastAssistantResponse;
|
|
89
115
|
const parsed = tryExtractResult(responseText, 'resolve_result');
|
|
116
|
+
// eslint-disable-next-line max-depth
|
|
90
117
|
if (isResolveResult(parsed)) {
|
|
91
118
|
resolveResult = parsed;
|
|
92
119
|
}
|
|
@@ -97,17 +124,19 @@ export async function resolveStandalonePR(options) {
|
|
|
97
124
|
else {
|
|
98
125
|
logError(`Agent incomplete: ${message.subtype}`);
|
|
99
126
|
// Try to salvage partial results from last response
|
|
127
|
+
// eslint-disable-next-line max-depth
|
|
100
128
|
if (lastAssistantResponse) {
|
|
101
129
|
const salvaged = tryExtractResult(lastAssistantResponse, 'resolve_result');
|
|
130
|
+
// eslint-disable-next-line max-depth
|
|
102
131
|
if (isResolveResult(salvaged)) {
|
|
103
132
|
resolveResult = salvaged;
|
|
104
133
|
}
|
|
105
134
|
}
|
|
106
135
|
}
|
|
107
136
|
}
|
|
108
|
-
//
|
|
137
|
+
// Fallback: commit any leftover uncommitted changes the agent didn't commit itself
|
|
109
138
|
if (hasUncommittedChanges(repoPath)) {
|
|
110
|
-
logInfo('Committing changes...');
|
|
139
|
+
logInfo('Committing remaining uncommitted changes...');
|
|
111
140
|
execSync('git add -A', { cwd: repoPath, stdio: 'pipe' });
|
|
112
141
|
execSync('git commit -m "Resolve PR review comments\n\nAutomated resolution by Edsger AI"', { cwd: repoPath, stdio: 'pipe' });
|
|
113
142
|
}
|
|
@@ -125,13 +154,16 @@ export async function resolveStandalonePR(options) {
|
|
|
125
154
|
for (const comment of comments) {
|
|
126
155
|
// Map comment_id back to real GraphQL thread ID
|
|
127
156
|
const threadId = commentIdToThreadId.get(comment.comment_id);
|
|
157
|
+
// eslint-disable-next-line max-depth
|
|
128
158
|
if (!threadId) {
|
|
129
159
|
logError(`Unknown comment_id "${comment.comment_id}", skipping reply`);
|
|
130
160
|
threadsErrored++;
|
|
131
161
|
continue;
|
|
132
162
|
}
|
|
163
|
+
// eslint-disable-next-line max-depth
|
|
133
164
|
try {
|
|
134
165
|
const replied = await replyToReviewThread(octokit, threadId, comment.reply, verbose);
|
|
166
|
+
// eslint-disable-next-line max-depth
|
|
135
167
|
if (replied && comment.action === 'changed') {
|
|
136
168
|
// Resolve the thread since the change was made
|
|
137
169
|
await resolveReviewThread(octokit, threadId, verbose);
|
|
@@ -161,6 +193,7 @@ export async function resolveStandalonePR(options) {
|
|
|
161
193
|
? 'Changes were made to address review feedback. Please re-review.'
|
|
162
194
|
: 'Reviewed this comment. No changes were made at this time.';
|
|
163
195
|
const replied = await replyToReviewThread(octokit, thread.id, genericReply, verbose);
|
|
196
|
+
// eslint-disable-next-line max-depth
|
|
164
197
|
if (replied) {
|
|
165
198
|
threadsSkipped++;
|
|
166
199
|
}
|
|
@@ -195,6 +228,14 @@ export async function resolveStandalonePR(options) {
|
|
|
195
228
|
// Non-critical
|
|
196
229
|
}
|
|
197
230
|
}
|
|
231
|
+
// Clean up workspace on success
|
|
232
|
+
try {
|
|
233
|
+
rmSync(repoPath, { recursive: true, force: true });
|
|
234
|
+
logInfo(`Cleaned up workspace: ${repoPath}`);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// ignore cleanup errors
|
|
238
|
+
}
|
|
198
239
|
return {
|
|
199
240
|
status: 'success',
|
|
200
241
|
message: `Resolved ${threadsAddressed} threads, skipped ${threadsSkipped}, ${threadsErrored} errors`,
|
|
@@ -211,14 +252,6 @@ export async function resolveStandalonePR(options) {
|
|
|
211
252
|
logInfo(`Workspace preserved for inspection: ${repoPath}`);
|
|
212
253
|
throw innerError;
|
|
213
254
|
}
|
|
214
|
-
// Only clean up on success
|
|
215
|
-
try {
|
|
216
|
-
rmSync(repoPath, { recursive: true, force: true });
|
|
217
|
-
logInfo(`Cleaned up workspace: ${repoPath}`);
|
|
218
|
-
}
|
|
219
|
-
catch {
|
|
220
|
-
// ignore cleanup errors
|
|
221
|
-
}
|
|
222
255
|
}
|
|
223
256
|
catch (error) {
|
|
224
257
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -27,7 +27,8 @@ export function createResolveSystemPrompt() {
|
|
|
27
27
|
2. For each comment, examine the relevant code
|
|
28
28
|
3. If you agree: make the change in the file
|
|
29
29
|
4. If you disagree: skip it (do NOT modify the file for that comment)
|
|
30
|
-
5. After
|
|
30
|
+
5. After making all changes, commit them with a descriptive message summarizing what was resolved (do NOT push)
|
|
31
|
+
6. After committing, output a JSON summary
|
|
31
32
|
|
|
32
33
|
**CRITICAL - Result Format**:
|
|
33
34
|
After making all changes, you MUST output a JSON result. Use the exact comment_id from each comment (comment_1, comment_2, etc.):
|
|
@@ -8,13 +8,29 @@
|
|
|
8
8
|
export declare function buildCredentialArgs(token: string): string[];
|
|
9
9
|
/**
|
|
10
10
|
* Get the workspace path for a PR resolve operation.
|
|
11
|
+
* Includes owner/repo to avoid collisions when resolving PRs from different repos.
|
|
11
12
|
*/
|
|
12
|
-
export declare function getResolveWorkspacePath(prNumber: number): string;
|
|
13
|
+
export declare function getResolveWorkspacePath(owner: string, repo: string, prNumber: number): string;
|
|
14
|
+
export interface ForkFallbackInfo {
|
|
15
|
+
upstreamOwner: string;
|
|
16
|
+
upstreamRepo: string;
|
|
17
|
+
}
|
|
18
|
+
export interface PrepareWorkspaceOptions {
|
|
19
|
+
owner: string;
|
|
20
|
+
repo: string;
|
|
21
|
+
headRef: string;
|
|
22
|
+
prNumber: number;
|
|
23
|
+
token: string;
|
|
24
|
+
verbose?: boolean;
|
|
25
|
+
forkFallback?: ForkFallbackInfo;
|
|
26
|
+
}
|
|
13
27
|
/**
|
|
14
28
|
* Clone or reuse a repo for PR resolve.
|
|
29
|
+
* For fork PRs, clones from the fork repo. If the fork branch is unavailable,
|
|
30
|
+
* falls back to fetching the PR ref from the upstream repo.
|
|
15
31
|
* Returns the workspace path.
|
|
16
32
|
*/
|
|
17
|
-
export declare function prepareWorkspace(
|
|
33
|
+
export declare function prepareWorkspace(options: PrepareWorkspaceOptions): string;
|
|
18
34
|
/**
|
|
19
35
|
* Push changes from workspace back to remote.
|
|
20
36
|
*/
|