edsger 0.41.3 → 0.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +3 -23
- package/dist/commands/pr-resolve/index.d.ts +1 -0
- package/dist/commands/pr-resolve/index.js +1 -0
- package/dist/index.js +1 -0
- package/dist/phases/pr-resolve/__tests__/checklist-learner.test.d.ts +1 -0
- package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +157 -0
- package/dist/phases/pr-resolve/__tests__/types.test.d.ts +1 -0
- package/dist/phases/pr-resolve/__tests__/types.test.js +43 -0
- package/dist/phases/pr-resolve/checklist-learner.d.ts +28 -0
- package/dist/phases/pr-resolve/checklist-learner.js +128 -0
- package/dist/phases/pr-resolve/index.d.ts +4 -0
- package/dist/phases/pr-resolve/index.js +42 -7
- package/dist/phases/pr-resolve/types.d.ts +18 -0
- package/dist/phases/pr-resolve/types.js +14 -0
- package/dist/phases/pr-resolve/workspace.d.ts +7 -1
- package/dist/phases/pr-resolve/workspace.js +38 -11
- package/dist/phases/pr-splitting/context.js +20 -15
- package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +4 -0
- package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +133 -0
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +4 -0
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +336 -0
- package/dist/services/lifecycle-agent/index.d.ts +24 -0
- package/dist/services/lifecycle-agent/index.js +25 -0
- package/dist/services/lifecycle-agent/phase-criteria.d.ts +57 -0
- package/dist/services/lifecycle-agent/phase-criteria.js +335 -0
- package/dist/services/lifecycle-agent/transition-rules.d.ts +60 -0
- package/dist/services/lifecycle-agent/transition-rules.js +184 -0
- package/dist/services/lifecycle-agent/types.d.ts +190 -0
- package/dist/services/lifecycle-agent/types.js +12 -0
- package/package.json +1 -1
- package/.env.local +0 -12
- package/dist/api/features/__tests__/regression-prevention.test.d.ts +0 -5
- package/dist/api/features/__tests__/regression-prevention.test.js +0 -338
- package/dist/api/features/__tests__/status-updater.integration.test.d.ts +0 -5
- package/dist/api/features/__tests__/status-updater.integration.test.js +0 -497
- package/dist/commands/workflow/pipeline-runner.d.ts +0 -17
- package/dist/commands/workflow/pipeline-runner.js +0 -393
- package/dist/commands/workflow/runner.d.ts +0 -26
- package/dist/commands/workflow/runner.js +0 -119
- package/dist/commands/workflow/workflow-runner.d.ts +0 -26
- package/dist/commands/workflow/workflow-runner.js +0 -119
- package/dist/phases/code-implementation/analyzer-helpers.d.ts +0 -28
- package/dist/phases/code-implementation/analyzer-helpers.js +0 -177
- package/dist/phases/code-implementation/analyzer.d.ts +0 -32
- package/dist/phases/code-implementation/analyzer.js +0 -629
- package/dist/phases/code-implementation/context-fetcher.d.ts +0 -17
- package/dist/phases/code-implementation/context-fetcher.js +0 -86
- package/dist/phases/code-implementation/mcp-server.d.ts +0 -1
- package/dist/phases/code-implementation/mcp-server.js +0 -93
- package/dist/phases/code-implementation/prompts-improvement.d.ts +0 -5
- package/dist/phases/code-implementation/prompts-improvement.js +0 -108
- package/dist/phases/code-implementation-verification/verifier.d.ts +0 -31
- package/dist/phases/code-implementation-verification/verifier.js +0 -196
- package/dist/phases/code-refine/analyzer.d.ts +0 -41
- package/dist/phases/code-refine/analyzer.js +0 -561
- package/dist/phases/code-refine/context-fetcher.d.ts +0 -94
- package/dist/phases/code-refine/context-fetcher.js +0 -423
- package/dist/phases/code-refine-verification/analysis/llm-analyzer.d.ts +0 -22
- package/dist/phases/code-refine-verification/analysis/llm-analyzer.js +0 -134
- package/dist/phases/code-refine-verification/verifier.d.ts +0 -47
- package/dist/phases/code-refine-verification/verifier.js +0 -597
- package/dist/phases/code-review/analyzer.d.ts +0 -29
- package/dist/phases/code-review/analyzer.js +0 -363
- package/dist/phases/code-review/context-fetcher.d.ts +0 -92
- package/dist/phases/code-review/context-fetcher.js +0 -296
- package/dist/phases/feature-analysis/analyzer-helpers.d.ts +0 -10
- package/dist/phases/feature-analysis/analyzer-helpers.js +0 -47
- package/dist/phases/feature-analysis/analyzer.d.ts +0 -11
- package/dist/phases/feature-analysis/analyzer.js +0 -208
- package/dist/phases/feature-analysis/context-fetcher.d.ts +0 -26
- package/dist/phases/feature-analysis/context-fetcher.js +0 -134
- package/dist/phases/feature-analysis/http-fallback.d.ts +0 -20
- package/dist/phases/feature-analysis/http-fallback.js +0 -95
- package/dist/phases/feature-analysis/mcp-server.d.ts +0 -1
- package/dist/phases/feature-analysis/mcp-server.js +0 -144
- package/dist/phases/feature-analysis/prompts-improvement.d.ts +0 -8
- package/dist/phases/feature-analysis/prompts-improvement.js +0 -109
- package/dist/phases/feature-analysis-verification/verifier.d.ts +0 -37
- package/dist/phases/feature-analysis-verification/verifier.js +0 -147
- package/dist/phases/technical-design/analyzer-helpers.d.ts +0 -25
- package/dist/phases/technical-design/analyzer-helpers.js +0 -39
- package/dist/phases/technical-design/analyzer.d.ts +0 -21
- package/dist/phases/technical-design/analyzer.js +0 -461
- package/dist/phases/technical-design/context-fetcher.d.ts +0 -12
- package/dist/phases/technical-design/context-fetcher.js +0 -39
- package/dist/phases/technical-design/http-fallback.d.ts +0 -17
- package/dist/phases/technical-design/http-fallback.js +0 -151
- package/dist/phases/technical-design/mcp-server.d.ts +0 -1
- package/dist/phases/technical-design/mcp-server.js +0 -157
- package/dist/phases/technical-design/prompts-improvement.d.ts +0 -5
- package/dist/phases/technical-design/prompts-improvement.js +0 -93
- package/dist/phases/technical-design-verification/verifier.d.ts +0 -53
- package/dist/phases/technical-design-verification/verifier.js +0 -170
- package/dist/services/feature-branches.d.ts +0 -77
- package/dist/services/feature-branches.js +0 -205
- package/dist/workflow-runner/config/phase-configs.d.ts +0 -5
- package/dist/workflow-runner/config/phase-configs.js +0 -120
- package/dist/workflow-runner/core/feature-filter.d.ts +0 -16
- package/dist/workflow-runner/core/feature-filter.js +0 -46
- package/dist/workflow-runner/core/index.d.ts +0 -8
- package/dist/workflow-runner/core/index.js +0 -12
- package/dist/workflow-runner/core/pipeline-evaluator.d.ts +0 -24
- package/dist/workflow-runner/core/pipeline-evaluator.js +0 -32
- package/dist/workflow-runner/core/state-manager.d.ts +0 -24
- package/dist/workflow-runner/core/state-manager.js +0 -42
- package/dist/workflow-runner/core/workflow-logger.d.ts +0 -20
- package/dist/workflow-runner/core/workflow-logger.js +0 -65
- package/dist/workflow-runner/executors/phase-executor.d.ts +0 -8
- package/dist/workflow-runner/executors/phase-executor.js +0 -248
- package/dist/workflow-runner/feature-workflow-runner.d.ts +0 -26
- package/dist/workflow-runner/feature-workflow-runner.js +0 -119
- package/dist/workflow-runner/index.d.ts +0 -2
- package/dist/workflow-runner/index.js +0 -2
- package/dist/workflow-runner/pipeline-runner.d.ts +0 -17
- package/dist/workflow-runner/pipeline-runner.js +0 -393
- package/dist/workflow-runner/workflow-processor.d.ts +0 -54
- package/dist/workflow-runner/workflow-processor.js +0 -170
|
@@ -1,28 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
|
-
"
|
|
5
|
-
"Bash(npm run
|
|
6
|
-
|
|
7
|
-
"Bash(git add:*)",
|
|
8
|
-
"Bash(git commit:*)",
|
|
9
|
-
"Bash(ls:*)",
|
|
10
|
-
"Bash(cat:*)",
|
|
11
|
-
"Bash(npm run typecheck:*)",
|
|
12
|
-
"Bash(git diff:*)",
|
|
13
|
-
"WebSearch",
|
|
14
|
-
"WebFetch(domain:supabase.com)",
|
|
15
|
-
"Bash(npm install:*)",
|
|
16
|
-
"Bash(grep:*)",
|
|
17
|
-
"Bash(npx supabase gen types typescript --help:*)",
|
|
18
|
-
"Bash(git -C /Users/steven/development/edsger status)",
|
|
19
|
-
"Bash(git -C /Users/steven/development/edsger diff)",
|
|
20
|
-
"Bash(git -C /Users/steven/development/edsger log --oneline -5)",
|
|
21
|
-
"Bash(git -C /Users/steven/development/edsger add supabase/migrations/20251231000000_drop_unused_views.sql)",
|
|
22
|
-
"Bash(git -C /Users/steven/development/edsger commit -m \"$\\(cat <<''EOF''\nchore: drop unused database views\n\nRemove test_report_summary and user_stories_with_context views that are defined but never used in the application.\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
|
23
|
-
"Bash(git -C /Users/steven/development/edsger commit -m \"$\\(cat <<''EOF''\nchore: drop unused database views\n\nRemove test_report_summary and user_stories_with_context views\nthat are defined but never used in the application.\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")"
|
|
24
|
-
],
|
|
25
|
-
"deny": [],
|
|
26
|
-
"ask": []
|
|
4
|
+
"Bash(npx tsc:*)",
|
|
5
|
+
"Bash(npm run:*)"
|
|
6
|
+
]
|
|
27
7
|
}
|
|
28
8
|
}
|
package/dist/index.js
CHANGED
|
@@ -299,6 +299,7 @@ program
|
|
|
299
299
|
.description('AI-resolve change requests on a GitHub PR')
|
|
300
300
|
.requiredOption('--pr-url <url>', 'GitHub PR URL')
|
|
301
301
|
.option('--pr-id <id>', 'Pull request record ID in database')
|
|
302
|
+
.option('--no-learn', 'Skip checklist learning after resolve')
|
|
302
303
|
.option('-v, --verbose', 'Verbose output')
|
|
303
304
|
.action(async (productId, opts) => {
|
|
304
305
|
try {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { buildLearnerPrompt } from '../checklist-learner.js';
|
|
4
|
+
// ── helpers ──────────────────────────────────────────────────
|
|
5
|
+
function makeThread(id, overrides) {
|
|
6
|
+
return {
|
|
7
|
+
id,
|
|
8
|
+
isResolved: false,
|
|
9
|
+
isOutdated: false,
|
|
10
|
+
comments: {
|
|
11
|
+
totalCount: 1,
|
|
12
|
+
nodes: [
|
|
13
|
+
{
|
|
14
|
+
id: `${id}-c1`,
|
|
15
|
+
author: { login: overrides?.author || 'reviewer' },
|
|
16
|
+
body: overrides?.body || 'Please fix this',
|
|
17
|
+
path: overrides?.path || 'src/index.ts',
|
|
18
|
+
line: overrides && 'line' in overrides ? (overrides.line ?? null) : 10,
|
|
19
|
+
url: `https://github.com/o/r/pull/1#${id}`,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function makeMap(entries) {
|
|
26
|
+
return new Map(entries);
|
|
27
|
+
}
|
|
28
|
+
// ── buildLearnerPrompt ──────────────────────────────────────
|
|
29
|
+
describe('buildLearnerPrompt', () => {
|
|
30
|
+
it('includes addressed comment count in header', () => {
|
|
31
|
+
const comments = [
|
|
32
|
+
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
33
|
+
{ comment_id: 'comment_2', action: 'changed', reply: 'Done' },
|
|
34
|
+
];
|
|
35
|
+
const threads = [makeThread('t1'), makeThread('t2')];
|
|
36
|
+
const map = makeMap([
|
|
37
|
+
['comment_1', 't1'],
|
|
38
|
+
['comment_2', 't2'],
|
|
39
|
+
]);
|
|
40
|
+
const prompt = buildLearnerPrompt(comments, threads, map);
|
|
41
|
+
assert.ok(prompt.includes('2 review comment(s)'));
|
|
42
|
+
});
|
|
43
|
+
it('includes reviewer, file path, line, and comment body', () => {
|
|
44
|
+
const comments = [
|
|
45
|
+
{ comment_id: 'comment_1', action: 'changed', reply: 'Added null check' },
|
|
46
|
+
];
|
|
47
|
+
const threads = [
|
|
48
|
+
makeThread('t1', {
|
|
49
|
+
path: 'src/auth.ts',
|
|
50
|
+
line: 42,
|
|
51
|
+
body: 'Missing null check here',
|
|
52
|
+
author: 'alice',
|
|
53
|
+
}),
|
|
54
|
+
];
|
|
55
|
+
const map = makeMap([['comment_1', 't1']]);
|
|
56
|
+
const prompt = buildLearnerPrompt(comments, threads, map);
|
|
57
|
+
assert.ok(prompt.includes('src/auth.ts'));
|
|
58
|
+
assert.ok(prompt.includes('42'));
|
|
59
|
+
assert.ok(prompt.includes('@alice'));
|
|
60
|
+
assert.ok(prompt.includes('Missing null check here'));
|
|
61
|
+
assert.ok(prompt.includes('Added null check'));
|
|
62
|
+
});
|
|
63
|
+
it('includes resolution text for each comment', () => {
|
|
64
|
+
const comments = [
|
|
65
|
+
{
|
|
66
|
+
comment_id: 'comment_1',
|
|
67
|
+
action: 'changed',
|
|
68
|
+
reply: 'Refactored to use try/catch',
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
const threads = [makeThread('t1')];
|
|
72
|
+
const map = makeMap([['comment_1', 't1']]);
|
|
73
|
+
const prompt = buildLearnerPrompt(comments, threads, map);
|
|
74
|
+
assert.ok(prompt.includes('**Resolution**: Refactored to use try/catch'));
|
|
75
|
+
});
|
|
76
|
+
it('includes summary when provided', () => {
|
|
77
|
+
const comments = [
|
|
78
|
+
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
79
|
+
];
|
|
80
|
+
const threads = [makeThread('t1')];
|
|
81
|
+
const map = makeMap([['comment_1', 't1']]);
|
|
82
|
+
const prompt = buildLearnerPrompt(comments, threads, map, 'Improved error handling across 3 files');
|
|
83
|
+
assert.ok(prompt.includes('## Overall Summary'));
|
|
84
|
+
assert.ok(prompt.includes('Improved error handling across 3 files'));
|
|
85
|
+
});
|
|
86
|
+
it('omits summary section when not provided', () => {
|
|
87
|
+
const comments = [
|
|
88
|
+
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
89
|
+
];
|
|
90
|
+
const threads = [makeThread('t1')];
|
|
91
|
+
const map = makeMap([['comment_1', 't1']]);
|
|
92
|
+
const prompt = buildLearnerPrompt(comments, threads, map);
|
|
93
|
+
assert.ok(!prompt.includes('## Overall Summary'));
|
|
94
|
+
});
|
|
95
|
+
it('handles comment with no matching thread gracefully', () => {
|
|
96
|
+
const comments = [
|
|
97
|
+
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
98
|
+
];
|
|
99
|
+
// Empty threads — no match for comment_1
|
|
100
|
+
const map = makeMap([['comment_1', 'nonexistent']]);
|
|
101
|
+
const prompt = buildLearnerPrompt(comments, [], map);
|
|
102
|
+
// Should still include the comment section with resolution
|
|
103
|
+
assert.ok(prompt.includes('## comment_1'));
|
|
104
|
+
assert.ok(prompt.includes('**Resolution**: Fixed'));
|
|
105
|
+
// Should NOT include reviewer info since thread was not found
|
|
106
|
+
assert.ok(!prompt.includes('**Reviewer**'));
|
|
107
|
+
});
|
|
108
|
+
it('handles comment_id not in map gracefully', () => {
|
|
109
|
+
const comments = [
|
|
110
|
+
{ comment_id: 'comment_99', action: 'changed', reply: 'Fixed' },
|
|
111
|
+
];
|
|
112
|
+
const prompt = buildLearnerPrompt(comments, [], new Map());
|
|
113
|
+
assert.ok(prompt.includes('## comment_99'));
|
|
114
|
+
assert.ok(prompt.includes('**Resolution**: Fixed'));
|
|
115
|
+
});
|
|
116
|
+
it('omits line when null', () => {
|
|
117
|
+
const comments = [
|
|
118
|
+
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
119
|
+
];
|
|
120
|
+
const threads = [makeThread('t1', { line: null })];
|
|
121
|
+
const map = makeMap([['comment_1', 't1']]);
|
|
122
|
+
const prompt = buildLearnerPrompt(comments, threads, map);
|
|
123
|
+
assert.ok(prompt.includes('**File**: src/index.ts'));
|
|
124
|
+
assert.ok(!prompt.includes('**Line**'));
|
|
125
|
+
});
|
|
126
|
+
it('uses threadById map correctly for multiple comments', () => {
|
|
127
|
+
const comments = [
|
|
128
|
+
{ comment_id: 'comment_1', action: 'changed', reply: 'Fix A' },
|
|
129
|
+
{ comment_id: 'comment_3', action: 'changed', reply: 'Fix C' },
|
|
130
|
+
];
|
|
131
|
+
const threads = [
|
|
132
|
+
makeThread('t1', { body: 'Issue A', path: 'a.ts' }),
|
|
133
|
+
makeThread('t2', { body: 'Issue B', path: 'b.ts' }),
|
|
134
|
+
makeThread('t3', { body: 'Issue C', path: 'c.ts' }),
|
|
135
|
+
];
|
|
136
|
+
const map = makeMap([
|
|
137
|
+
['comment_1', 't1'],
|
|
138
|
+
['comment_2', 't2'],
|
|
139
|
+
['comment_3', 't3'],
|
|
140
|
+
]);
|
|
141
|
+
const prompt = buildLearnerPrompt(comments, threads, map);
|
|
142
|
+
assert.ok(prompt.includes('Issue A'));
|
|
143
|
+
assert.ok(prompt.includes('a.ts'));
|
|
144
|
+
assert.ok(prompt.includes('Issue C'));
|
|
145
|
+
assert.ok(prompt.includes('c.ts'));
|
|
146
|
+
// comment_2 was not in addressedComments, should not appear
|
|
147
|
+
assert.ok(!prompt.includes('Issue B'));
|
|
148
|
+
});
|
|
149
|
+
it('includes instructions section', () => {
|
|
150
|
+
const comments = [
|
|
151
|
+
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
152
|
+
];
|
|
153
|
+
const prompt = buildLearnerPrompt(comments, [makeThread('t1')], makeMap([['comment_1', 't1']]));
|
|
154
|
+
assert.ok(prompt.includes('## Instructions'));
|
|
155
|
+
assert.ok(prompt.includes('code_review checklists'));
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { isResolveResult } from '../types.js';
|
|
4
|
+
describe('isResolveResult', () => {
|
|
5
|
+
it('returns true for valid ResolveResult', () => {
|
|
6
|
+
assert.ok(isResolveResult({
|
|
7
|
+
comments: [
|
|
8
|
+
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
9
|
+
],
|
|
10
|
+
}));
|
|
11
|
+
});
|
|
12
|
+
it('returns true when optional fields are present', () => {
|
|
13
|
+
assert.ok(isResolveResult({
|
|
14
|
+
comments: [],
|
|
15
|
+
files_modified: ['a.ts'],
|
|
16
|
+
summary: 'Done',
|
|
17
|
+
}));
|
|
18
|
+
});
|
|
19
|
+
it('returns true for empty comments array', () => {
|
|
20
|
+
assert.ok(isResolveResult({ comments: [] }));
|
|
21
|
+
});
|
|
22
|
+
it('returns false for null', () => {
|
|
23
|
+
assert.ok(!isResolveResult(null));
|
|
24
|
+
});
|
|
25
|
+
it('returns false for undefined', () => {
|
|
26
|
+
assert.ok(!isResolveResult(undefined));
|
|
27
|
+
});
|
|
28
|
+
it('returns false for string', () => {
|
|
29
|
+
assert.ok(!isResolveResult('not an object'));
|
|
30
|
+
});
|
|
31
|
+
it('returns false for number', () => {
|
|
32
|
+
assert.ok(!isResolveResult(42));
|
|
33
|
+
});
|
|
34
|
+
it('returns false when comments is missing', () => {
|
|
35
|
+
assert.ok(!isResolveResult({ files_modified: ['a.ts'] }));
|
|
36
|
+
});
|
|
37
|
+
it('returns false when comments is not an array', () => {
|
|
38
|
+
assert.ok(!isResolveResult({ comments: 'not-array' }));
|
|
39
|
+
});
|
|
40
|
+
it('returns false for empty object', () => {
|
|
41
|
+
assert.ok(!isResolveResult({}));
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checklist Learner — runs after PR resolve to analyse addressed review
|
|
3
|
+
* comments and create / update code-review checklists so the same issues
|
|
4
|
+
* don't recur.
|
|
5
|
+
*
|
|
6
|
+
* Uses the Claude Agent SDK with the existing checklist MCP tools.
|
|
7
|
+
* Strictly non-blocking: failures only log a warning.
|
|
8
|
+
*/
|
|
9
|
+
import { type ReviewThread } from '../code-refine-verification/types.js';
|
|
10
|
+
import { type ResolveComment, type ResolveResult } from './types.js';
|
|
11
|
+
export interface ChecklistLearnerInput {
|
|
12
|
+
productId: string;
|
|
13
|
+
unresolvedThreads: ReviewThread[];
|
|
14
|
+
resolveResult: ResolveResult;
|
|
15
|
+
/** Maps comment_id (e.g. "comment_1") → GraphQL thread ID */
|
|
16
|
+
commentIdToThreadId: Map<string, string>;
|
|
17
|
+
verbose?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build a user prompt from the addressed review comments.
|
|
21
|
+
* @param addressedComments - pre-filtered list of comments with action === 'changed'
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildLearnerPrompt(addressedComments: ResolveComment[], unresolvedThreads: ReviewThread[], commentIdToThreadId: Map<string, string>, summary?: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Analyse addressed review comments and update code-review checklists.
|
|
26
|
+
* Non-blocking — catches all errors and logs a warning.
|
|
27
|
+
*/
|
|
28
|
+
export declare function learnFromReviewFeedback(input: ChecklistLearnerInput): Promise<void>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checklist Learner — runs after PR resolve to analyse addressed review
|
|
3
|
+
* comments and create / update code-review checklists so the same issues
|
|
4
|
+
* don't recur.
|
|
5
|
+
*
|
|
6
|
+
* Uses the Claude Agent SDK with the existing checklist MCP tools.
|
|
7
|
+
* Strictly non-blocking: failures only log a warning.
|
|
8
|
+
*/
|
|
9
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
10
|
+
import { createChecklistsMcpServer } from '../../commands/checklists/tools.js';
|
|
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 for the code_review phase.
|
|
13
|
+
|
|
14
|
+
## Workflow
|
|
15
|
+
|
|
16
|
+
1. **Read** the addressed review comments provided below.
|
|
17
|
+
2. **Query existing checklists** using \`list_checklists\` with phase "code_review" to see what already exists.
|
|
18
|
+
3. **Identify patterns** — group related comments into categories (e.g., error handling, naming, security, performance, testing).
|
|
19
|
+
4. **Update or create checklist items**:
|
|
20
|
+
- If an existing checklist covers the category, add new items to it (skip if a similar item already exists).
|
|
21
|
+
- If no suitable checklist exists, create a new one.
|
|
22
|
+
5. **Summarise** what you added or updated.
|
|
23
|
+
|
|
24
|
+
## Rules
|
|
25
|
+
|
|
26
|
+
- Only create items for **genuine quality patterns** — things that should be checked in every code review.
|
|
27
|
+
- Skip one-off nits, purely stylistic preferences, or context-specific fixes that won't generalise.
|
|
28
|
+
- Role: \`developer\`
|
|
29
|
+
- Phases: \`["code_review"]\`
|
|
30
|
+
- Item type: \`boolean\` (yes/no checkable)
|
|
31
|
+
- Keep item titles concise (< 80 chars). Use the description for details.
|
|
32
|
+
- Do NOT duplicate items that already exist in the checklists.
|
|
33
|
+
- If all comments are too specific to generalise, it's fine to add nothing — just say so.
|
|
34
|
+
`;
|
|
35
|
+
/**
|
|
36
|
+
* Build a user prompt from the addressed review comments.
|
|
37
|
+
* @param addressedComments - pre-filtered list of comments with action === 'changed'
|
|
38
|
+
*/
|
|
39
|
+
export function buildLearnerPrompt(addressedComments, unresolvedThreads, commentIdToThreadId, summary) {
|
|
40
|
+
// Build a reverse lookup: threadId → thread for O(1) access
|
|
41
|
+
const threadById = new Map();
|
|
42
|
+
for (const thread of unresolvedThreads) {
|
|
43
|
+
threadById.set(thread.id, thread);
|
|
44
|
+
}
|
|
45
|
+
const sections = [
|
|
46
|
+
'# Addressed PR Review Comments',
|
|
47
|
+
'',
|
|
48
|
+
`${addressedComments.length} review comment(s) were accepted and fixed during PR resolution.`,
|
|
49
|
+
`Analyse these to identify patterns that should become checklist items for future code reviews.`,
|
|
50
|
+
'',
|
|
51
|
+
];
|
|
52
|
+
for (const comment of addressedComments) {
|
|
53
|
+
const threadId = commentIdToThreadId.get(comment.comment_id);
|
|
54
|
+
const thread = threadId ? threadById.get(threadId) : undefined;
|
|
55
|
+
const firstNode = thread?.comments.nodes[0];
|
|
56
|
+
sections.push(`## ${comment.comment_id}`);
|
|
57
|
+
if (firstNode) {
|
|
58
|
+
sections.push(`**File**: ${firstNode.path}`);
|
|
59
|
+
if (firstNode.line) {
|
|
60
|
+
sections.push(`**Line**: ${firstNode.line}`);
|
|
61
|
+
}
|
|
62
|
+
sections.push(`**Reviewer**: @${firstNode.author.login}`);
|
|
63
|
+
sections.push(`**Review comment**:`);
|
|
64
|
+
sections.push(firstNode.body);
|
|
65
|
+
}
|
|
66
|
+
sections.push(`**Resolution**: ${comment.reply}`);
|
|
67
|
+
sections.push('');
|
|
68
|
+
}
|
|
69
|
+
if (summary) {
|
|
70
|
+
sections.push('## Overall Summary');
|
|
71
|
+
sections.push(summary);
|
|
72
|
+
sections.push('');
|
|
73
|
+
}
|
|
74
|
+
sections.push('## Instructions');
|
|
75
|
+
sections.push('Based on the patterns above, query existing code_review checklists and create or update items to prevent these issues from recurring.');
|
|
76
|
+
return sections.join('\n');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Analyse addressed review comments and update code-review checklists.
|
|
80
|
+
* Non-blocking — catches all errors and logs a warning.
|
|
81
|
+
*/
|
|
82
|
+
export async function learnFromReviewFeedback(input) {
|
|
83
|
+
try {
|
|
84
|
+
const { resolveResult, unresolvedThreads, commentIdToThreadId, verbose, productId, } = input;
|
|
85
|
+
// Filter once — only learn from accepted changes
|
|
86
|
+
const addressedComments = resolveResult.comments.filter((c) => c.action === 'changed');
|
|
87
|
+
if (addressedComments.length === 0) {
|
|
88
|
+
if (verbose) {
|
|
89
|
+
logInfo('No addressed comments to learn from, skipping checklist sync.');
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
logInfo(`Learning from ${addressedComments.length} addressed review comment(s) to update checklists...`);
|
|
94
|
+
const userPrompt = buildLearnerPrompt(addressedComments, unresolvedThreads, commentIdToThreadId, resolveResult.summary);
|
|
95
|
+
const mcpServer = createChecklistsMcpServer(productId);
|
|
96
|
+
for await (const message of query({
|
|
97
|
+
prompt: userPrompt,
|
|
98
|
+
options: {
|
|
99
|
+
systemPrompt: {
|
|
100
|
+
type: 'preset',
|
|
101
|
+
preset: 'claude_code',
|
|
102
|
+
append: LEARNER_SYSTEM_PROMPT,
|
|
103
|
+
},
|
|
104
|
+
model: 'sonnet',
|
|
105
|
+
maxTurns: 15,
|
|
106
|
+
permissionMode: 'bypassPermissions',
|
|
107
|
+
mcpServers: {
|
|
108
|
+
'edsger-checklists': mcpServer,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
})) {
|
|
112
|
+
if (message.type === 'result') {
|
|
113
|
+
if (message.subtype === 'success') {
|
|
114
|
+
logInfo('Checklist learning completed.');
|
|
115
|
+
if (verbose && message.result) {
|
|
116
|
+
logInfo(message.result);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (verbose) {
|
|
120
|
+
logWarning(`Checklist learning incomplete: ${message.subtype}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
logWarning(`Checklist learning failed (non-blocking): ${error instanceof Error ? error.message : String(error)}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -11,6 +11,8 @@ export interface StandalonePRResolveOptions {
|
|
|
11
11
|
repo: string;
|
|
12
12
|
prId?: string;
|
|
13
13
|
verbose?: boolean;
|
|
14
|
+
/** Set to false to skip checklist learning after resolve (default: true) */
|
|
15
|
+
learn?: boolean;
|
|
14
16
|
}
|
|
15
17
|
export interface PRResolveResult {
|
|
16
18
|
status: 'success' | 'error';
|
|
@@ -21,6 +23,8 @@ export interface PRResolveResult {
|
|
|
21
23
|
filesModified?: string[];
|
|
22
24
|
summary?: string;
|
|
23
25
|
}
|
|
26
|
+
export type { ResolveComment, ResolveResult } from './types.js';
|
|
27
|
+
export { isResolveResult } from './types.js';
|
|
24
28
|
/**
|
|
25
29
|
* Resolve PR change requests: evaluate each comment, fix or explain.
|
|
26
30
|
*/
|
|
@@ -13,9 +13,12 @@ import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
|
13
13
|
import { fetchUnresolvedReviewThreads } from '../code-refine-verification/github.js';
|
|
14
14
|
import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
|
|
15
15
|
import { parsePullRequestUrl } from '../pr-shared/context.js';
|
|
16
|
+
import { learnFromReviewFeedback } from './checklist-learner.js';
|
|
16
17
|
import { replyToReviewThread, resolveReviewThread } from './github-reply.js';
|
|
17
18
|
import { createResolveSystemPrompt, createResolveUserPrompt, } from './prompts.js';
|
|
19
|
+
import { isResolveResult } from './types.js';
|
|
18
20
|
import { hasNewCommits, hasUncommittedChanges, prepareWorkspace, pushChanges, } from './workspace.js';
|
|
21
|
+
export { isResolveResult } from './types.js';
|
|
19
22
|
/**
|
|
20
23
|
* Resolve PR change requests: evaluate each comment, fix or explain.
|
|
21
24
|
*/
|
|
@@ -50,14 +53,30 @@ export async function resolveStandalonePR(options) {
|
|
|
50
53
|
pull_number: prInfo.prNumber,
|
|
51
54
|
});
|
|
52
55
|
const headRef = prData.head.ref;
|
|
53
|
-
//
|
|
54
|
-
|
|
56
|
+
// Determine clone source: fork PRs need to clone from the fork repo.
|
|
57
|
+
// head.repo is null when the fork has been deleted — treat as fork with deleted source.
|
|
58
|
+
const isFork = !prData.head.repo ||
|
|
59
|
+
prData.head.repo.full_name !== prData.base.repo.full_name;
|
|
60
|
+
const forkDeleted = !prData.head.repo;
|
|
61
|
+
const cloneOwner = isFork && !forkDeleted ? prData.head.repo.owner.login : owner;
|
|
62
|
+
const cloneRepo = isFork && !forkDeleted ? prData.head.repo.name : repo;
|
|
63
|
+
if (isFork && verbose) {
|
|
64
|
+
logInfo(forkDeleted
|
|
65
|
+
? `Fork PR detected but fork repo has been deleted, will use PR ref from ${owner}/${repo}`
|
|
66
|
+
: `Fork PR detected: cloning from ${cloneOwner}/${cloneRepo} instead of ${owner}/${repo}`);
|
|
67
|
+
}
|
|
68
|
+
// Clone repo and checkout PR branch.
|
|
69
|
+
// For fork PRs, pass fallback info so prepareWorkspace can fetch the PR ref
|
|
70
|
+
// from upstream if the fork branch is unavailable.
|
|
71
|
+
// For deleted forks, clone upstream directly and always use PR ref fallback.
|
|
72
|
+
const repoPath = prepareWorkspace(cloneOwner, cloneRepo, headRef, prInfo.prNumber, githubToken, verbose, isFork
|
|
73
|
+
? { upstreamOwner: owner, upstreamRepo: repo }
|
|
74
|
+
: undefined);
|
|
55
75
|
try {
|
|
56
76
|
// Run Claude Agent SDK to evaluate and fix comments
|
|
57
77
|
const systemPrompt = createResolveSystemPrompt();
|
|
58
78
|
const { prompt: resolvePrompt, commentIdToThreadId } = createResolveUserPrompt(unresolvedThreads);
|
|
59
79
|
let lastAssistantResponse = '';
|
|
60
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
80
|
let resolveResult = null;
|
|
62
81
|
logInfo('Starting Claude agent to evaluate and resolve comments...');
|
|
63
82
|
for await (const message of query({
|
|
@@ -84,8 +103,11 @@ export async function resolveStandalonePR(options) {
|
|
|
84
103
|
if (message.subtype === 'success') {
|
|
85
104
|
logInfo('Agent completed, parsing results...');
|
|
86
105
|
const responseText = message.result || lastAssistantResponse;
|
|
87
|
-
|
|
88
|
-
if (
|
|
106
|
+
const parsed = tryExtractResult(responseText, 'resolve_result');
|
|
107
|
+
if (isResolveResult(parsed)) {
|
|
108
|
+
resolveResult = parsed;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
89
111
|
logError('Failed to parse resolve result JSON');
|
|
90
112
|
}
|
|
91
113
|
}
|
|
@@ -93,7 +115,10 @@ export async function resolveStandalonePR(options) {
|
|
|
93
115
|
logError(`Agent incomplete: ${message.subtype}`);
|
|
94
116
|
// Try to salvage partial results from last response
|
|
95
117
|
if (lastAssistantResponse) {
|
|
96
|
-
|
|
118
|
+
const salvaged = tryExtractResult(lastAssistantResponse, 'resolve_result');
|
|
119
|
+
if (isResolveResult(salvaged)) {
|
|
120
|
+
resolveResult = salvaged;
|
|
121
|
+
}
|
|
97
122
|
}
|
|
98
123
|
}
|
|
99
124
|
}
|
|
@@ -113,7 +138,7 @@ export async function resolveStandalonePR(options) {
|
|
|
113
138
|
let threadsSkipped = 0;
|
|
114
139
|
let threadsErrored = 0;
|
|
115
140
|
if (resolveResult?.comments) {
|
|
116
|
-
const comments = resolveResult
|
|
141
|
+
const { comments } = resolveResult;
|
|
117
142
|
for (const comment of comments) {
|
|
118
143
|
// Map comment_id back to real GraphQL thread ID
|
|
119
144
|
const threadId = commentIdToThreadId.get(comment.comment_id);
|
|
@@ -166,6 +191,16 @@ export async function resolveStandalonePR(options) {
|
|
|
166
191
|
}
|
|
167
192
|
}
|
|
168
193
|
logSuccess(`PR resolve completed: ${threadsAddressed} addressed, ${threadsSkipped} skipped, ${threadsErrored} errors`);
|
|
194
|
+
// Learn from addressed comments to update code-review checklists
|
|
195
|
+
if (options.learn !== false && threadsAddressed > 0 && resolveResult) {
|
|
196
|
+
await learnFromReviewFeedback({
|
|
197
|
+
productId: options.productId,
|
|
198
|
+
unresolvedThreads,
|
|
199
|
+
resolveResult,
|
|
200
|
+
commentIdToThreadId,
|
|
201
|
+
verbose,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
169
204
|
if (prId) {
|
|
170
205
|
try {
|
|
171
206
|
await callMcpEndpoint('pull_requests/update', {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared type definitions for PR resolve phase.
|
|
3
|
+
*/
|
|
4
|
+
export interface ResolveComment {
|
|
5
|
+
comment_id: string;
|
|
6
|
+
action: 'changed' | 'skipped';
|
|
7
|
+
reply: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ResolveResult {
|
|
10
|
+
comments: ResolveComment[];
|
|
11
|
+
files_modified?: string[];
|
|
12
|
+
summary?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Runtime type guard — validates that an unknown value from tryExtractResult
|
|
16
|
+
* has the shape of a ResolveResult.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isResolveResult(value: unknown): value is ResolveResult;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared type definitions for PR resolve phase.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Runtime type guard — validates that an unknown value from tryExtractResult
|
|
6
|
+
* has the shape of a ResolveResult.
|
|
7
|
+
*/
|
|
8
|
+
export function isResolveResult(value) {
|
|
9
|
+
if (!value || typeof value !== 'object') {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const obj = value;
|
|
13
|
+
return Array.isArray(obj.comments);
|
|
14
|
+
}
|
|
@@ -10,11 +10,17 @@ export declare function buildCredentialArgs(token: string): string[];
|
|
|
10
10
|
* Get the workspace path for a PR resolve operation.
|
|
11
11
|
*/
|
|
12
12
|
export declare function getResolveWorkspacePath(prNumber: number): string;
|
|
13
|
+
export interface ForkFallbackInfo {
|
|
14
|
+
upstreamOwner: string;
|
|
15
|
+
upstreamRepo: string;
|
|
16
|
+
}
|
|
13
17
|
/**
|
|
14
18
|
* Clone or reuse a repo for PR resolve.
|
|
19
|
+
* For fork PRs, clones from the fork repo. If the fork branch is unavailable,
|
|
20
|
+
* falls back to fetching the PR ref from the upstream repo.
|
|
15
21
|
* Returns the workspace path.
|
|
16
22
|
*/
|
|
17
|
-
export declare function prepareWorkspace(owner: string, repo: string, headRef: string, prNumber: number, token: string, verbose?: boolean): string;
|
|
23
|
+
export declare function prepareWorkspace(owner: string, repo: string, headRef: string, prNumber: number, token: string, verbose?: boolean, forkFallback?: ForkFallbackInfo): string;
|
|
18
24
|
/**
|
|
19
25
|
* Push changes from workspace back to remote.
|
|
20
26
|
*/
|
|
@@ -27,9 +27,11 @@ export function getResolveWorkspacePath(prNumber) {
|
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
29
|
* Clone or reuse a repo for PR resolve.
|
|
30
|
+
* For fork PRs, clones from the fork repo. If the fork branch is unavailable,
|
|
31
|
+
* falls back to fetching the PR ref from the upstream repo.
|
|
30
32
|
* Returns the workspace path.
|
|
31
33
|
*/
|
|
32
|
-
export function prepareWorkspace(owner, repo, headRef, prNumber, token, verbose) {
|
|
34
|
+
export function prepareWorkspace(owner, repo, headRef, prNumber, token, verbose, forkFallback) {
|
|
33
35
|
const repoPath = getResolveWorkspacePath(prNumber);
|
|
34
36
|
const repoUrl = `https://github.com/${owner}/${repo}.git`;
|
|
35
37
|
const gitCredentialArgs = buildCredentialArgs(token);
|
|
@@ -66,16 +68,41 @@ export function prepareWorkspace(owner, repo, headRef, prNumber, token, verbose)
|
|
|
66
68
|
}
|
|
67
69
|
else {
|
|
68
70
|
logInfo(`Cloning ${owner}/${repo} (branch: ${headRef})...`);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
try {
|
|
72
|
+
execFileSync('git', [
|
|
73
|
+
...gitCredentialArgs,
|
|
74
|
+
'clone',
|
|
75
|
+
'--branch',
|
|
76
|
+
headRef,
|
|
77
|
+
'--single-branch',
|
|
78
|
+
repoUrl,
|
|
79
|
+
repoPath,
|
|
80
|
+
], { stdio: 'pipe' });
|
|
81
|
+
logSuccess(`Cloned to ${repoPath}`);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
// If clone fails and this is a fork PR, fall back to upstream PR ref
|
|
85
|
+
if (forkFallback) {
|
|
86
|
+
logInfo(`Fork branch not available, falling back to PR ref from ${forkFallback.upstreamOwner}/${forkFallback.upstreamRepo}`);
|
|
87
|
+
const upstreamUrl = `https://github.com/${forkFallback.upstreamOwner}/${forkFallback.upstreamRepo}.git`;
|
|
88
|
+
execFileSync('git', [...gitCredentialArgs, 'clone', upstreamUrl, repoPath], { stdio: 'pipe' });
|
|
89
|
+
// Fetch the PR ref and check it out
|
|
90
|
+
execFileSync('git', [
|
|
91
|
+
...gitCredentialArgs,
|
|
92
|
+
'fetch',
|
|
93
|
+
'origin',
|
|
94
|
+
`pull/${prNumber}/head:${headRef}`,
|
|
95
|
+
], { cwd: repoPath, stdio: 'pipe' });
|
|
96
|
+
execSync(`git checkout ${headRef}`, {
|
|
97
|
+
cwd: repoPath,
|
|
98
|
+
stdio: 'pipe',
|
|
99
|
+
});
|
|
100
|
+
logSuccess(`Cloned via PR ref to ${repoPath}`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
79
106
|
}
|
|
80
107
|
// Configure git user for commits
|
|
81
108
|
try {
|