edsger 0.42.1 → 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 +29 -13
- package/dist/phases/pr-resolve/prompts.js +2 -1
- package/dist/phases/pr-resolve/workspace.d.ts +12 -2
- package/dist/phases/pr-resolve/workspace.js +6 -4
- 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}`);
|
|
@@ -69,9 +70,17 @@ export async function resolveStandalonePR(options) {
|
|
|
69
70
|
// For fork PRs, pass fallback info so prepareWorkspace can fetch the PR ref
|
|
70
71
|
// from upstream if the fork branch is unavailable.
|
|
71
72
|
// For deleted forks, clone upstream directly and always use PR ref fallback.
|
|
72
|
-
const repoPath = prepareWorkspace(
|
|
73
|
-
|
|
74
|
-
:
|
|
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
|
+
});
|
|
75
84
|
try {
|
|
76
85
|
// Run Claude Agent SDK to evaluate and fix comments
|
|
77
86
|
const systemPrompt = createResolveSystemPrompt();
|
|
@@ -104,6 +113,7 @@ export async function resolveStandalonePR(options) {
|
|
|
104
113
|
logInfo('Agent completed, parsing results...');
|
|
105
114
|
const responseText = message.result || lastAssistantResponse;
|
|
106
115
|
const parsed = tryExtractResult(responseText, 'resolve_result');
|
|
116
|
+
// eslint-disable-next-line max-depth
|
|
107
117
|
if (isResolveResult(parsed)) {
|
|
108
118
|
resolveResult = parsed;
|
|
109
119
|
}
|
|
@@ -114,17 +124,19 @@ export async function resolveStandalonePR(options) {
|
|
|
114
124
|
else {
|
|
115
125
|
logError(`Agent incomplete: ${message.subtype}`);
|
|
116
126
|
// Try to salvage partial results from last response
|
|
127
|
+
// eslint-disable-next-line max-depth
|
|
117
128
|
if (lastAssistantResponse) {
|
|
118
129
|
const salvaged = tryExtractResult(lastAssistantResponse, 'resolve_result');
|
|
130
|
+
// eslint-disable-next-line max-depth
|
|
119
131
|
if (isResolveResult(salvaged)) {
|
|
120
132
|
resolveResult = salvaged;
|
|
121
133
|
}
|
|
122
134
|
}
|
|
123
135
|
}
|
|
124
136
|
}
|
|
125
|
-
//
|
|
137
|
+
// Fallback: commit any leftover uncommitted changes the agent didn't commit itself
|
|
126
138
|
if (hasUncommittedChanges(repoPath)) {
|
|
127
|
-
logInfo('Committing changes...');
|
|
139
|
+
logInfo('Committing remaining uncommitted changes...');
|
|
128
140
|
execSync('git add -A', { cwd: repoPath, stdio: 'pipe' });
|
|
129
141
|
execSync('git commit -m "Resolve PR review comments\n\nAutomated resolution by Edsger AI"', { cwd: repoPath, stdio: 'pipe' });
|
|
130
142
|
}
|
|
@@ -142,13 +154,16 @@ export async function resolveStandalonePR(options) {
|
|
|
142
154
|
for (const comment of comments) {
|
|
143
155
|
// Map comment_id back to real GraphQL thread ID
|
|
144
156
|
const threadId = commentIdToThreadId.get(comment.comment_id);
|
|
157
|
+
// eslint-disable-next-line max-depth
|
|
145
158
|
if (!threadId) {
|
|
146
159
|
logError(`Unknown comment_id "${comment.comment_id}", skipping reply`);
|
|
147
160
|
threadsErrored++;
|
|
148
161
|
continue;
|
|
149
162
|
}
|
|
163
|
+
// eslint-disable-next-line max-depth
|
|
150
164
|
try {
|
|
151
165
|
const replied = await replyToReviewThread(octokit, threadId, comment.reply, verbose);
|
|
166
|
+
// eslint-disable-next-line max-depth
|
|
152
167
|
if (replied && comment.action === 'changed') {
|
|
153
168
|
// Resolve the thread since the change was made
|
|
154
169
|
await resolveReviewThread(octokit, threadId, verbose);
|
|
@@ -178,6 +193,7 @@ export async function resolveStandalonePR(options) {
|
|
|
178
193
|
? 'Changes were made to address review feedback. Please re-review.'
|
|
179
194
|
: 'Reviewed this comment. No changes were made at this time.';
|
|
180
195
|
const replied = await replyToReviewThread(octokit, thread.id, genericReply, verbose);
|
|
196
|
+
// eslint-disable-next-line max-depth
|
|
181
197
|
if (replied) {
|
|
182
198
|
threadsSkipped++;
|
|
183
199
|
}
|
|
@@ -212,6 +228,14 @@ export async function resolveStandalonePR(options) {
|
|
|
212
228
|
// Non-critical
|
|
213
229
|
}
|
|
214
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
|
+
}
|
|
215
239
|
return {
|
|
216
240
|
status: 'success',
|
|
217
241
|
message: `Resolved ${threadsAddressed} threads, skipped ${threadsSkipped}, ${threadsErrored} errors`,
|
|
@@ -228,14 +252,6 @@ export async function resolveStandalonePR(options) {
|
|
|
228
252
|
logInfo(`Workspace preserved for inspection: ${repoPath}`);
|
|
229
253
|
throw innerError;
|
|
230
254
|
}
|
|
231
|
-
// Only clean up 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
|
-
}
|
|
239
255
|
}
|
|
240
256
|
catch (error) {
|
|
241
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,19 +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;
|
|
13
14
|
export interface ForkFallbackInfo {
|
|
14
15
|
upstreamOwner: string;
|
|
15
16
|
upstreamRepo: string;
|
|
16
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
|
+
}
|
|
17
27
|
/**
|
|
18
28
|
* Clone or reuse a repo for PR resolve.
|
|
19
29
|
* For fork PRs, clones from the fork repo. If the fork branch is unavailable,
|
|
20
30
|
* falls back to fetching the PR ref from the upstream repo.
|
|
21
31
|
* Returns the workspace path.
|
|
22
32
|
*/
|
|
23
|
-
export declare function prepareWorkspace(
|
|
33
|
+
export declare function prepareWorkspace(options: PrepareWorkspaceOptions): string;
|
|
24
34
|
/**
|
|
25
35
|
* Push changes from workspace back to remote.
|
|
26
36
|
*/
|
|
@@ -21,9 +21,10 @@ 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,8 +32,9 @@ export function getResolveWorkspacePath(prNumber) {
|
|
|
31
32
|
* falls back to fetching the PR ref from the upstream repo.
|
|
32
33
|
* Returns the workspace path.
|
|
33
34
|
*/
|
|
34
|
-
export function prepareWorkspace(
|
|
35
|
-
const
|
|
35
|
+
export function prepareWorkspace(options) {
|
|
36
|
+
const { owner, repo, headRef, prNumber, token, verbose, forkFallback } = options;
|
|
37
|
+
const repoPath = getResolveWorkspacePath(owner, repo, prNumber);
|
|
36
38
|
const repoUrl = `https://github.com/${owner}/${repo}.git`;
|
|
37
39
|
const gitCredentialArgs = buildCredentialArgs(token);
|
|
38
40
|
if (existsSync(join(repoPath, '.git'))) {
|