edsger 0.41.3 → 0.42.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/.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 +23 -5
- package/dist/phases/pr-resolve/types.d.ts +18 -0
- package/dist/phases/pr-resolve/types.js +14 -0
- 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
|
*/
|
|
@@ -57,7 +60,6 @@ export async function resolveStandalonePR(options) {
|
|
|
57
60
|
const systemPrompt = createResolveSystemPrompt();
|
|
58
61
|
const { prompt: resolvePrompt, commentIdToThreadId } = createResolveUserPrompt(unresolvedThreads);
|
|
59
62
|
let lastAssistantResponse = '';
|
|
60
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
63
|
let resolveResult = null;
|
|
62
64
|
logInfo('Starting Claude agent to evaluate and resolve comments...');
|
|
63
65
|
for await (const message of query({
|
|
@@ -84,8 +86,11 @@ export async function resolveStandalonePR(options) {
|
|
|
84
86
|
if (message.subtype === 'success') {
|
|
85
87
|
logInfo('Agent completed, parsing results...');
|
|
86
88
|
const responseText = message.result || lastAssistantResponse;
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
+
const parsed = tryExtractResult(responseText, 'resolve_result');
|
|
90
|
+
if (isResolveResult(parsed)) {
|
|
91
|
+
resolveResult = parsed;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
89
94
|
logError('Failed to parse resolve result JSON');
|
|
90
95
|
}
|
|
91
96
|
}
|
|
@@ -93,7 +98,10 @@ export async function resolveStandalonePR(options) {
|
|
|
93
98
|
logError(`Agent incomplete: ${message.subtype}`);
|
|
94
99
|
// Try to salvage partial results from last response
|
|
95
100
|
if (lastAssistantResponse) {
|
|
96
|
-
|
|
101
|
+
const salvaged = tryExtractResult(lastAssistantResponse, 'resolve_result');
|
|
102
|
+
if (isResolveResult(salvaged)) {
|
|
103
|
+
resolveResult = salvaged;
|
|
104
|
+
}
|
|
97
105
|
}
|
|
98
106
|
}
|
|
99
107
|
}
|
|
@@ -113,7 +121,7 @@ export async function resolveStandalonePR(options) {
|
|
|
113
121
|
let threadsSkipped = 0;
|
|
114
122
|
let threadsErrored = 0;
|
|
115
123
|
if (resolveResult?.comments) {
|
|
116
|
-
const comments = resolveResult
|
|
124
|
+
const { comments } = resolveResult;
|
|
117
125
|
for (const comment of comments) {
|
|
118
126
|
// Map comment_id back to real GraphQL thread ID
|
|
119
127
|
const threadId = commentIdToThreadId.get(comment.comment_id);
|
|
@@ -166,6 +174,16 @@ export async function resolveStandalonePR(options) {
|
|
|
166
174
|
}
|
|
167
175
|
}
|
|
168
176
|
logSuccess(`PR resolve completed: ${threadsAddressed} addressed, ${threadsSkipped} skipped, ${threadsErrored} errors`);
|
|
177
|
+
// Learn from addressed comments to update code-review checklists
|
|
178
|
+
if (options.learn !== false && threadsAddressed > 0 && resolveResult) {
|
|
179
|
+
await learnFromReviewFeedback({
|
|
180
|
+
productId: options.productId,
|
|
181
|
+
unresolvedThreads,
|
|
182
|
+
resolveResult,
|
|
183
|
+
commentIdToThreadId,
|
|
184
|
+
verbose,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
169
187
|
if (prId) {
|
|
170
188
|
try {
|
|
171
189
|
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
|
+
}
|
|
@@ -53,18 +53,18 @@ function getChangedFiles(baseRef, headRef) {
|
|
|
53
53
|
/**
|
|
54
54
|
* Determine the diff base ref for incremental re-runs
|
|
55
55
|
* If existing PRs have last_synced_commit, use the earliest one
|
|
56
|
-
* Otherwise use main
|
|
56
|
+
* Otherwise use origin/main (remote-tracking ref, always up-to-date after fetch)
|
|
57
57
|
*/
|
|
58
58
|
function determineDiffBaseRef(existingPRs, replaceExisting) {
|
|
59
59
|
if (replaceExisting || existingPRs.length === 0) {
|
|
60
|
-
return 'main';
|
|
60
|
+
return 'origin/main';
|
|
61
61
|
}
|
|
62
62
|
// Find the minimum last_synced_commit (earliest sync point)
|
|
63
63
|
const syncedCommits = existingPRs
|
|
64
64
|
.map((pr) => pr.last_synced_commit)
|
|
65
65
|
.filter((c) => c !== null);
|
|
66
66
|
if (syncedCommits.length === 0) {
|
|
67
|
-
return 'main';
|
|
67
|
+
return 'origin/main';
|
|
68
68
|
}
|
|
69
69
|
// All PRs should have been synced to the same commit
|
|
70
70
|
// Use the first one (they should all be equal after a successful sync)
|
|
@@ -87,6 +87,22 @@ export async function fetchPRSplittingContext(featureId, verbose, replaceExistin
|
|
|
87
87
|
getPullRequests({ featureId, verbose }).catch(() => []),
|
|
88
88
|
getGitHubConfig(featureId, verbose),
|
|
89
89
|
]);
|
|
90
|
+
// Fetch latest remote refs (updates origin/main and all remote-tracking branches)
|
|
91
|
+
try {
|
|
92
|
+
const credArgs = buildCredentialArgs(githubConfig.token);
|
|
93
|
+
execFileSync('git', [...credArgs, 'fetch', 'origin'], {
|
|
94
|
+
encoding: 'utf-8',
|
|
95
|
+
stdio: 'pipe',
|
|
96
|
+
});
|
|
97
|
+
if (verbose) {
|
|
98
|
+
logInfo('✅ Fetched latest remote refs');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (verbose) {
|
|
103
|
+
logInfo(`⚠️ Could not fetch from origin: ${error instanceof Error ? error.message : String(error)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
90
106
|
// Verify dev branch exists
|
|
91
107
|
const localExists = branchExists(devBranchName);
|
|
92
108
|
const remoteExists = !localExists && remoteBranchExists(devBranchName, githubConfig.token);
|
|
@@ -94,17 +110,6 @@ export async function fetchPRSplittingContext(featureId, verbose, replaceExistin
|
|
|
94
110
|
throw new Error(`Development branch '${devBranchName}' does not exist. ` +
|
|
95
111
|
`The feature must have code on the dev branch before PR splitting.`);
|
|
96
112
|
}
|
|
97
|
-
// If branch only exists on remote, fetch it (using credential helper)
|
|
98
|
-
if (!localExists && remoteExists) {
|
|
99
|
-
if (verbose) {
|
|
100
|
-
logInfo(`Fetching remote branch ${devBranchName}...`);
|
|
101
|
-
}
|
|
102
|
-
const credArgs = buildCredentialArgs(githubConfig.token);
|
|
103
|
-
execFileSync('git', [...credArgs, 'fetch', 'origin', devBranchName], {
|
|
104
|
-
encoding: 'utf-8',
|
|
105
|
-
stdio: 'pipe',
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
113
|
const product = await getProduct(feature.product_id, verbose);
|
|
109
114
|
// Detect fork status
|
|
110
115
|
let forkInfo = { isFork: false };
|
|
@@ -131,7 +136,7 @@ export async function fetchPRSplittingContext(featureId, verbose, replaceExistin
|
|
|
131
136
|
const baseRef = determineDiffBaseRef(existingPullRequests, replaceExisting);
|
|
132
137
|
const devBranchHeadSha = getBranchHeadSha(devRef);
|
|
133
138
|
// Check if there are new changes since last sync
|
|
134
|
-
if (baseRef !== 'main' && baseRef === devBranchHeadSha) {
|
|
139
|
+
if (baseRef !== 'origin/main' && baseRef === devBranchHeadSha) {
|
|
135
140
|
if (verbose) {
|
|
136
141
|
logInfo(`No new changes since last sync (HEAD: ${devBranchHeadSha})`);
|
|
137
142
|
}
|