edsger 0.42.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/api/web-deploy.d.ts +8 -1
  2. package/dist/api/web-deploy.js +2 -1
  3. package/dist/commands/workflow/phase-orchestrator.js +3 -1
  4. package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.js +1 -0
  5. package/dist/phases/app-store-generation/index.js +3 -1
  6. package/dist/phases/app-store-generation/screenshot-composer.js +34 -10
  7. package/dist/phases/branch-planning/index.js +3 -1
  8. package/dist/phases/bug-fixing/analyzer.js +3 -1
  9. package/dist/phases/code-implementation/index.js +3 -1
  10. package/dist/phases/code-refine/index.js +3 -1
  11. package/dist/phases/code-review/__tests__/diff-utils.test.js +11 -11
  12. package/dist/phases/code-review/index.js +3 -1
  13. package/dist/phases/code-testing/analyzer.js +3 -1
  14. package/dist/phases/feature-analysis/index.js +3 -1
  15. package/dist/phases/functional-testing/analyzer.js +3 -1
  16. package/dist/phases/growth-analysis/index.js +3 -1
  17. package/dist/phases/intelligence-analysis/__tests__/orchestration.test.js +12 -12
  18. package/dist/phases/intelligence-analysis/agent.js +2 -0
  19. package/dist/phases/intelligence-analysis/index.js +1 -0
  20. package/dist/phases/intelligence-analysis/prompts.js +11 -1
  21. package/dist/phases/output-contracts.js +1 -0
  22. package/dist/phases/pr-execution/__tests__/file-assigner.test.js +22 -13
  23. package/dist/phases/pr-execution/context.js +4 -2
  24. package/dist/phases/pr-execution/file-assigner.js +1 -0
  25. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +11 -11
  26. package/dist/phases/pr-resolve/__tests__/prompts.test.js +12 -12
  27. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.js +6 -6
  28. package/dist/phases/pr-resolve/__tests__/types.test.js +11 -11
  29. package/dist/phases/pr-resolve/__tests__/workspace.test.js +13 -13
  30. package/dist/phases/pr-resolve/checklist-learner.js +34 -9
  31. package/dist/phases/pr-resolve/index.js +45 -12
  32. package/dist/phases/pr-resolve/prompts.js +2 -1
  33. package/dist/phases/pr-resolve/workspace.d.ts +18 -2
  34. package/dist/phases/pr-resolve/workspace.js +43 -14
  35. package/dist/phases/pr-review/__tests__/prompts.test.js +9 -9
  36. package/dist/phases/pr-review/__tests__/review-comments.test.js +6 -6
  37. package/dist/phases/pr-review/index.js +1 -0
  38. package/dist/phases/pr-shared/__tests__/agent-utils.test.js +17 -17
  39. package/dist/phases/pr-shared/__tests__/context.test.js +12 -12
  40. package/dist/phases/pr-splitting/import-dep-validator.js +14 -6
  41. package/dist/phases/pr-splitting/index.js +3 -1
  42. package/dist/phases/technical-design/index.js +3 -1
  43. package/dist/phases/test-cases-analysis/index.js +3 -1
  44. package/dist/phases/user-stories-analysis/index.js +3 -1
  45. package/dist/services/phase-hooks/__tests__/hook-executor.test.js +7 -4
  46. package/dist/services/phase-hooks/__tests__/hook-runner.test.js +22 -21
  47. package/dist/services/phase-hooks/hook-executor.js +1 -0
  48. package/dist/services/phase-hooks/plugin-loader.js +3 -0
  49. package/dist/services/video/screenshot-generator.js +8 -2
  50. 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 for the code_review phase.
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\` with phase "code_review" to see what already exists.
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. **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.
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 in every code review.
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 code_review checklists and create or update items to prevent these issues from recurring.');
99
+ sections.push('Based on the patterns above, query existing checklists across all phases and create or update items in the most appropriate phase(s) to prevent these issues from recurring. Prefer earlier phases where the issue could have been caught sooner.');
76
100
  return sections.join('\n');
77
101
  }
78
102
  /**
@@ -112,6 +136,7 @@ export async function learnFromReviewFeedback(input) {
112
136
  if (message.type === 'result') {
113
137
  if (message.subtype === 'success') {
114
138
  logInfo('Checklist learning completed.');
139
+ // eslint-disable-next-line max-depth
115
140
  if (verbose && message.result) {
116
141
  logInfo(message.result);
117
142
  }
@@ -22,6 +22,7 @@ export { isResolveResult } from './types.js';
22
22
  /**
23
23
  * Resolve PR change requests: evaluate each comment, fix or explain.
24
24
  */
25
+ // eslint-disable-next-line complexity
25
26
  export async function resolveStandalonePR(options) {
26
27
  const { pullRequestUrl, githubToken, owner, repo, verbose, prId } = options;
27
28
  logInfo(`Starting PR resolve: ${pullRequestUrl}`);
@@ -53,8 +54,33 @@ export async function resolveStandalonePR(options) {
53
54
  pull_number: prInfo.prNumber,
54
55
  });
55
56
  const headRef = prData.head.ref;
56
- // Clone repo and checkout PR branch
57
- const repoPath = prepareWorkspace(owner, repo, headRef, prInfo.prNumber, githubToken, verbose);
57
+ // Determine clone source: fork PRs need to clone from the fork repo.
58
+ // head.repo is null when the fork has been deleted — treat as fork with deleted source.
59
+ const isFork = !prData.head.repo ||
60
+ prData.head.repo.full_name !== prData.base.repo.full_name;
61
+ const forkDeleted = !prData.head.repo;
62
+ const cloneOwner = isFork && !forkDeleted ? prData.head.repo.owner.login : owner;
63
+ const cloneRepo = isFork && !forkDeleted ? prData.head.repo.name : repo;
64
+ if (isFork && verbose) {
65
+ logInfo(forkDeleted
66
+ ? `Fork PR detected but fork repo has been deleted, will use PR ref from ${owner}/${repo}`
67
+ : `Fork PR detected: cloning from ${cloneOwner}/${cloneRepo} instead of ${owner}/${repo}`);
68
+ }
69
+ // Clone repo and checkout PR branch.
70
+ // For fork PRs, pass fallback info so prepareWorkspace can fetch the PR ref
71
+ // from upstream if the fork branch is unavailable.
72
+ // For deleted forks, clone upstream directly and always use PR ref fallback.
73
+ const repoPath = prepareWorkspace({
74
+ owner: cloneOwner,
75
+ repo: cloneRepo,
76
+ headRef,
77
+ prNumber: prInfo.prNumber,
78
+ token: githubToken,
79
+ verbose,
80
+ forkFallback: isFork
81
+ ? { upstreamOwner: owner, upstreamRepo: repo }
82
+ : undefined,
83
+ });
58
84
  try {
59
85
  // Run Claude Agent SDK to evaluate and fix comments
60
86
  const systemPrompt = createResolveSystemPrompt();
@@ -87,6 +113,7 @@ export async function resolveStandalonePR(options) {
87
113
  logInfo('Agent completed, parsing results...');
88
114
  const responseText = message.result || lastAssistantResponse;
89
115
  const parsed = tryExtractResult(responseText, 'resolve_result');
116
+ // eslint-disable-next-line max-depth
90
117
  if (isResolveResult(parsed)) {
91
118
  resolveResult = parsed;
92
119
  }
@@ -97,17 +124,19 @@ export async function resolveStandalonePR(options) {
97
124
  else {
98
125
  logError(`Agent incomplete: ${message.subtype}`);
99
126
  // Try to salvage partial results from last response
127
+ // eslint-disable-next-line max-depth
100
128
  if (lastAssistantResponse) {
101
129
  const salvaged = tryExtractResult(lastAssistantResponse, 'resolve_result');
130
+ // eslint-disable-next-line max-depth
102
131
  if (isResolveResult(salvaged)) {
103
132
  resolveResult = salvaged;
104
133
  }
105
134
  }
106
135
  }
107
136
  }
108
- // Commit and push any changes the agent made
137
+ // Fallback: commit any leftover uncommitted changes the agent didn't commit itself
109
138
  if (hasUncommittedChanges(repoPath)) {
110
- logInfo('Committing changes...');
139
+ logInfo('Committing remaining uncommitted changes...');
111
140
  execSync('git add -A', { cwd: repoPath, stdio: 'pipe' });
112
141
  execSync('git commit -m "Resolve PR review comments\n\nAutomated resolution by Edsger AI"', { cwd: repoPath, stdio: 'pipe' });
113
142
  }
@@ -125,13 +154,16 @@ export async function resolveStandalonePR(options) {
125
154
  for (const comment of comments) {
126
155
  // Map comment_id back to real GraphQL thread ID
127
156
  const threadId = commentIdToThreadId.get(comment.comment_id);
157
+ // eslint-disable-next-line max-depth
128
158
  if (!threadId) {
129
159
  logError(`Unknown comment_id "${comment.comment_id}", skipping reply`);
130
160
  threadsErrored++;
131
161
  continue;
132
162
  }
163
+ // eslint-disable-next-line max-depth
133
164
  try {
134
165
  const replied = await replyToReviewThread(octokit, threadId, comment.reply, verbose);
166
+ // eslint-disable-next-line max-depth
135
167
  if (replied && comment.action === 'changed') {
136
168
  // Resolve the thread since the change was made
137
169
  await resolveReviewThread(octokit, threadId, verbose);
@@ -161,6 +193,7 @@ export async function resolveStandalonePR(options) {
161
193
  ? 'Changes were made to address review feedback. Please re-review.'
162
194
  : 'Reviewed this comment. No changes were made at this time.';
163
195
  const replied = await replyToReviewThread(octokit, thread.id, genericReply, verbose);
196
+ // eslint-disable-next-line max-depth
164
197
  if (replied) {
165
198
  threadsSkipped++;
166
199
  }
@@ -195,6 +228,14 @@ export async function resolveStandalonePR(options) {
195
228
  // Non-critical
196
229
  }
197
230
  }
231
+ // Clean up workspace on success
232
+ try {
233
+ rmSync(repoPath, { recursive: true, force: true });
234
+ logInfo(`Cleaned up workspace: ${repoPath}`);
235
+ }
236
+ catch {
237
+ // ignore cleanup errors
238
+ }
198
239
  return {
199
240
  status: 'success',
200
241
  message: `Resolved ${threadsAddressed} threads, skipped ${threadsSkipped}, ${threadsErrored} errors`,
@@ -211,14 +252,6 @@ export async function resolveStandalonePR(options) {
211
252
  logInfo(`Workspace preserved for inspection: ${repoPath}`);
212
253
  throw innerError;
213
254
  }
214
- // Only clean up on success
215
- try {
216
- rmSync(repoPath, { recursive: true, force: true });
217
- logInfo(`Cleaned up workspace: ${repoPath}`);
218
- }
219
- catch {
220
- // ignore cleanup errors
221
- }
222
255
  }
223
256
  catch (error) {
224
257
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -27,7 +27,8 @@ export function createResolveSystemPrompt() {
27
27
  2. For each comment, examine the relevant code
28
28
  3. If you agree: make the change in the file
29
29
  4. If you disagree: skip it (do NOT modify the file for that comment)
30
- 5. After processing all comments, output a JSON summary
30
+ 5. After making all changes, commit them with a descriptive message summarizing what was resolved (do NOT push)
31
+ 6. After committing, output a JSON summary
31
32
 
32
33
  **CRITICAL - Result Format**:
33
34
  After making all changes, you MUST output a JSON result. Use the exact comment_id from each comment (comment_1, comment_2, etc.):
@@ -8,13 +8,29 @@
8
8
  export declare function buildCredentialArgs(token: string): string[];
9
9
  /**
10
10
  * Get the workspace path for a PR resolve operation.
11
+ * Includes owner/repo to avoid collisions when resolving PRs from different repos.
11
12
  */
12
- export declare function getResolveWorkspacePath(prNumber: number): string;
13
+ export declare function getResolveWorkspacePath(owner: string, repo: string, prNumber: number): string;
14
+ export interface ForkFallbackInfo {
15
+ upstreamOwner: string;
16
+ upstreamRepo: string;
17
+ }
18
+ export interface PrepareWorkspaceOptions {
19
+ owner: string;
20
+ repo: string;
21
+ headRef: string;
22
+ prNumber: number;
23
+ token: string;
24
+ verbose?: boolean;
25
+ forkFallback?: ForkFallbackInfo;
26
+ }
13
27
  /**
14
28
  * Clone or reuse a repo for PR resolve.
29
+ * For fork PRs, clones from the fork repo. If the fork branch is unavailable,
30
+ * falls back to fetching the PR ref from the upstream repo.
15
31
  * Returns the workspace path.
16
32
  */
17
- export declare function prepareWorkspace(owner: string, repo: string, headRef: string, prNumber: number, token: string, verbose?: boolean): string;
33
+ export declare function prepareWorkspace(options: PrepareWorkspaceOptions): string;
18
34
  /**
19
35
  * Push changes from workspace back to remote.
20
36
  */