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.
Files changed (115) hide show
  1. package/.claude/settings.local.json +3 -23
  2. package/dist/commands/pr-resolve/index.d.ts +1 -0
  3. package/dist/commands/pr-resolve/index.js +1 -0
  4. package/dist/index.js +1 -0
  5. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.d.ts +1 -0
  6. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +157 -0
  7. package/dist/phases/pr-resolve/__tests__/types.test.d.ts +1 -0
  8. package/dist/phases/pr-resolve/__tests__/types.test.js +43 -0
  9. package/dist/phases/pr-resolve/checklist-learner.d.ts +28 -0
  10. package/dist/phases/pr-resolve/checklist-learner.js +128 -0
  11. package/dist/phases/pr-resolve/index.d.ts +4 -0
  12. package/dist/phases/pr-resolve/index.js +23 -5
  13. package/dist/phases/pr-resolve/types.d.ts +18 -0
  14. package/dist/phases/pr-resolve/types.js +14 -0
  15. package/dist/phases/pr-splitting/context.js +20 -15
  16. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +4 -0
  17. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +133 -0
  18. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +4 -0
  19. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +336 -0
  20. package/dist/services/lifecycle-agent/index.d.ts +24 -0
  21. package/dist/services/lifecycle-agent/index.js +25 -0
  22. package/dist/services/lifecycle-agent/phase-criteria.d.ts +57 -0
  23. package/dist/services/lifecycle-agent/phase-criteria.js +335 -0
  24. package/dist/services/lifecycle-agent/transition-rules.d.ts +60 -0
  25. package/dist/services/lifecycle-agent/transition-rules.js +184 -0
  26. package/dist/services/lifecycle-agent/types.d.ts +190 -0
  27. package/dist/services/lifecycle-agent/types.js +12 -0
  28. package/package.json +1 -1
  29. package/.env.local +0 -12
  30. package/dist/api/features/__tests__/regression-prevention.test.d.ts +0 -5
  31. package/dist/api/features/__tests__/regression-prevention.test.js +0 -338
  32. package/dist/api/features/__tests__/status-updater.integration.test.d.ts +0 -5
  33. package/dist/api/features/__tests__/status-updater.integration.test.js +0 -497
  34. package/dist/commands/workflow/pipeline-runner.d.ts +0 -17
  35. package/dist/commands/workflow/pipeline-runner.js +0 -393
  36. package/dist/commands/workflow/runner.d.ts +0 -26
  37. package/dist/commands/workflow/runner.js +0 -119
  38. package/dist/commands/workflow/workflow-runner.d.ts +0 -26
  39. package/dist/commands/workflow/workflow-runner.js +0 -119
  40. package/dist/phases/code-implementation/analyzer-helpers.d.ts +0 -28
  41. package/dist/phases/code-implementation/analyzer-helpers.js +0 -177
  42. package/dist/phases/code-implementation/analyzer.d.ts +0 -32
  43. package/dist/phases/code-implementation/analyzer.js +0 -629
  44. package/dist/phases/code-implementation/context-fetcher.d.ts +0 -17
  45. package/dist/phases/code-implementation/context-fetcher.js +0 -86
  46. package/dist/phases/code-implementation/mcp-server.d.ts +0 -1
  47. package/dist/phases/code-implementation/mcp-server.js +0 -93
  48. package/dist/phases/code-implementation/prompts-improvement.d.ts +0 -5
  49. package/dist/phases/code-implementation/prompts-improvement.js +0 -108
  50. package/dist/phases/code-implementation-verification/verifier.d.ts +0 -31
  51. package/dist/phases/code-implementation-verification/verifier.js +0 -196
  52. package/dist/phases/code-refine/analyzer.d.ts +0 -41
  53. package/dist/phases/code-refine/analyzer.js +0 -561
  54. package/dist/phases/code-refine/context-fetcher.d.ts +0 -94
  55. package/dist/phases/code-refine/context-fetcher.js +0 -423
  56. package/dist/phases/code-refine-verification/analysis/llm-analyzer.d.ts +0 -22
  57. package/dist/phases/code-refine-verification/analysis/llm-analyzer.js +0 -134
  58. package/dist/phases/code-refine-verification/verifier.d.ts +0 -47
  59. package/dist/phases/code-refine-verification/verifier.js +0 -597
  60. package/dist/phases/code-review/analyzer.d.ts +0 -29
  61. package/dist/phases/code-review/analyzer.js +0 -363
  62. package/dist/phases/code-review/context-fetcher.d.ts +0 -92
  63. package/dist/phases/code-review/context-fetcher.js +0 -296
  64. package/dist/phases/feature-analysis/analyzer-helpers.d.ts +0 -10
  65. package/dist/phases/feature-analysis/analyzer-helpers.js +0 -47
  66. package/dist/phases/feature-analysis/analyzer.d.ts +0 -11
  67. package/dist/phases/feature-analysis/analyzer.js +0 -208
  68. package/dist/phases/feature-analysis/context-fetcher.d.ts +0 -26
  69. package/dist/phases/feature-analysis/context-fetcher.js +0 -134
  70. package/dist/phases/feature-analysis/http-fallback.d.ts +0 -20
  71. package/dist/phases/feature-analysis/http-fallback.js +0 -95
  72. package/dist/phases/feature-analysis/mcp-server.d.ts +0 -1
  73. package/dist/phases/feature-analysis/mcp-server.js +0 -144
  74. package/dist/phases/feature-analysis/prompts-improvement.d.ts +0 -8
  75. package/dist/phases/feature-analysis/prompts-improvement.js +0 -109
  76. package/dist/phases/feature-analysis-verification/verifier.d.ts +0 -37
  77. package/dist/phases/feature-analysis-verification/verifier.js +0 -147
  78. package/dist/phases/technical-design/analyzer-helpers.d.ts +0 -25
  79. package/dist/phases/technical-design/analyzer-helpers.js +0 -39
  80. package/dist/phases/technical-design/analyzer.d.ts +0 -21
  81. package/dist/phases/technical-design/analyzer.js +0 -461
  82. package/dist/phases/technical-design/context-fetcher.d.ts +0 -12
  83. package/dist/phases/technical-design/context-fetcher.js +0 -39
  84. package/dist/phases/technical-design/http-fallback.d.ts +0 -17
  85. package/dist/phases/technical-design/http-fallback.js +0 -151
  86. package/dist/phases/technical-design/mcp-server.d.ts +0 -1
  87. package/dist/phases/technical-design/mcp-server.js +0 -157
  88. package/dist/phases/technical-design/prompts-improvement.d.ts +0 -5
  89. package/dist/phases/technical-design/prompts-improvement.js +0 -93
  90. package/dist/phases/technical-design-verification/verifier.d.ts +0 -53
  91. package/dist/phases/technical-design-verification/verifier.js +0 -170
  92. package/dist/services/feature-branches.d.ts +0 -77
  93. package/dist/services/feature-branches.js +0 -205
  94. package/dist/workflow-runner/config/phase-configs.d.ts +0 -5
  95. package/dist/workflow-runner/config/phase-configs.js +0 -120
  96. package/dist/workflow-runner/core/feature-filter.d.ts +0 -16
  97. package/dist/workflow-runner/core/feature-filter.js +0 -46
  98. package/dist/workflow-runner/core/index.d.ts +0 -8
  99. package/dist/workflow-runner/core/index.js +0 -12
  100. package/dist/workflow-runner/core/pipeline-evaluator.d.ts +0 -24
  101. package/dist/workflow-runner/core/pipeline-evaluator.js +0 -32
  102. package/dist/workflow-runner/core/state-manager.d.ts +0 -24
  103. package/dist/workflow-runner/core/state-manager.js +0 -42
  104. package/dist/workflow-runner/core/workflow-logger.d.ts +0 -20
  105. package/dist/workflow-runner/core/workflow-logger.js +0 -65
  106. package/dist/workflow-runner/executors/phase-executor.d.ts +0 -8
  107. package/dist/workflow-runner/executors/phase-executor.js +0 -248
  108. package/dist/workflow-runner/feature-workflow-runner.d.ts +0 -26
  109. package/dist/workflow-runner/feature-workflow-runner.js +0 -119
  110. package/dist/workflow-runner/index.d.ts +0 -2
  111. package/dist/workflow-runner/index.js +0 -2
  112. package/dist/workflow-runner/pipeline-runner.d.ts +0 -17
  113. package/dist/workflow-runner/pipeline-runner.js +0 -393
  114. package/dist/workflow-runner/workflow-processor.d.ts +0 -54
  115. package/dist/workflow-runner/workflow-processor.js +0 -170
@@ -1,28 +1,8 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Read(//Users/steven/development/edsger/**)",
5
- "Bash(npm run build)",
6
- "Bash(node:*)",
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
  }
@@ -6,5 +6,6 @@ export interface PRResolveCliOptions {
6
6
  prUrl: string;
7
7
  prId?: string;
8
8
  verbose?: boolean;
9
+ learn?: boolean;
9
10
  }
10
11
  export declare function runPRResolve(productId: string, options: PRResolveCliOptions): Promise<void>;
@@ -26,6 +26,7 @@ export async function runPRResolve(productId, options) {
26
26
  repo: githubConfig.repo,
27
27
  prId,
28
28
  verbose,
29
+ learn: options.learn,
29
30
  });
30
31
  if (result.status === 'success') {
31
32
  logSuccess(`PR resolve completed: ${result.message}`);
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,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
- resolveResult = tryExtractResult(responseText, 'resolve_result');
88
- if (!resolveResult) {
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
- resolveResult = tryExtractResult(lastAssistantResponse, 'resolve_result');
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.comments;
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
  }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Unit tests for phase quality criteria definitions
3
+ */
4
+ export {};