edsger 0.42.1 → 0.44.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 (98) hide show
  1. package/.claude/settings.local.json +23 -3
  2. package/.env.local +12 -0
  3. package/dist/api/release-test-cases.d.ts +7 -0
  4. package/dist/api/release-test-cases.js +21 -0
  5. package/dist/api/releases.d.ts +41 -0
  6. package/dist/api/releases.js +31 -0
  7. package/dist/api/web-deploy.d.ts +8 -1
  8. package/dist/api/web-deploy.js +2 -1
  9. package/dist/commands/release-sync/index.d.ts +5 -0
  10. package/dist/commands/release-sync/index.js +38 -0
  11. package/dist/commands/smoke-test/index.d.ts +5 -0
  12. package/dist/commands/smoke-test/index.js +40 -0
  13. package/dist/commands/workflow/phase-orchestrator.js +3 -1
  14. package/dist/index.js +40 -0
  15. package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.js +1 -0
  16. package/dist/phases/app-store-generation/index.js +3 -1
  17. package/dist/phases/app-store-generation/screenshot-composer.js +34 -10
  18. package/dist/phases/branch-planning/index.js +3 -1
  19. package/dist/phases/bug-fixing/analyzer.js +3 -1
  20. package/dist/phases/code-implementation/index.js +3 -1
  21. package/dist/phases/code-refine/index.js +3 -1
  22. package/dist/phases/code-review/__tests__/diff-utils.test.js +11 -11
  23. package/dist/phases/code-review/index.js +3 -1
  24. package/dist/phases/code-testing/analyzer.js +3 -1
  25. package/dist/phases/feature-analysis/index.js +3 -1
  26. package/dist/phases/functional-testing/analyzer.js +3 -1
  27. package/dist/phases/growth-analysis/index.js +3 -1
  28. package/dist/phases/intelligence-analysis/__tests__/orchestration.test.js +12 -12
  29. package/dist/phases/intelligence-analysis/agent.js +2 -0
  30. package/dist/phases/intelligence-analysis/index.js +1 -0
  31. package/dist/phases/intelligence-analysis/prompts.js +11 -1
  32. package/dist/phases/output-contracts.js +1 -0
  33. package/dist/phases/pr-execution/__tests__/file-assigner.test.js +22 -13
  34. package/dist/phases/pr-execution/context.js +4 -2
  35. package/dist/phases/pr-execution/file-assigner.js +1 -0
  36. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +11 -11
  37. package/dist/phases/pr-resolve/__tests__/prompts.test.js +12 -12
  38. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.js +6 -6
  39. package/dist/phases/pr-resolve/__tests__/types.test.js +11 -11
  40. package/dist/phases/pr-resolve/__tests__/workspace.test.js +13 -13
  41. package/dist/phases/pr-resolve/checklist-learner.js +34 -9
  42. package/dist/phases/pr-resolve/index.js +29 -13
  43. package/dist/phases/pr-resolve/prompts.js +2 -1
  44. package/dist/phases/pr-resolve/workspace.d.ts +12 -2
  45. package/dist/phases/pr-resolve/workspace.js +6 -4
  46. package/dist/phases/pr-review/__tests__/prompts.test.js +9 -9
  47. package/dist/phases/pr-review/__tests__/review-comments.test.js +6 -6
  48. package/dist/phases/pr-review/index.js +1 -0
  49. package/dist/phases/pr-shared/__tests__/agent-utils.test.js +17 -17
  50. package/dist/phases/pr-shared/__tests__/context.test.js +12 -12
  51. package/dist/phases/pr-splitting/import-dep-validator.js +14 -6
  52. package/dist/phases/pr-splitting/index.js +3 -1
  53. package/dist/phases/release-sync/__tests__/github.test.d.ts +9 -0
  54. package/dist/phases/release-sync/__tests__/github.test.js +123 -0
  55. package/dist/phases/release-sync/__tests__/snapshot.test.d.ts +8 -0
  56. package/dist/phases/release-sync/__tests__/snapshot.test.js +93 -0
  57. package/dist/phases/release-sync/github.d.ts +54 -0
  58. package/dist/phases/release-sync/github.js +101 -0
  59. package/dist/phases/release-sync/index.d.ts +24 -0
  60. package/dist/phases/release-sync/index.js +147 -0
  61. package/dist/phases/release-sync/snapshot.d.ts +27 -0
  62. package/dist/phases/release-sync/snapshot.js +159 -0
  63. package/dist/phases/smoke-test/__tests__/agent.test.d.ts +4 -0
  64. package/dist/phases/smoke-test/__tests__/agent.test.js +85 -0
  65. package/dist/phases/smoke-test/agent.d.ts +12 -0
  66. package/dist/phases/smoke-test/agent.js +94 -0
  67. package/dist/phases/smoke-test/index.d.ts +22 -0
  68. package/dist/phases/smoke-test/index.js +233 -0
  69. package/dist/phases/smoke-test/prompts.d.ts +15 -0
  70. package/dist/phases/smoke-test/prompts.js +35 -0
  71. package/dist/phases/technical-design/index.js +3 -1
  72. package/dist/phases/test-cases-analysis/index.js +3 -1
  73. package/dist/phases/user-stories-analysis/index.js +3 -1
  74. package/dist/services/phase-hooks/__tests__/hook-executor.test.js +7 -4
  75. package/dist/services/phase-hooks/__tests__/hook-runner.test.js +22 -21
  76. package/dist/services/phase-hooks/hook-executor.js +1 -0
  77. package/dist/services/phase-hooks/plugin-loader.js +3 -0
  78. package/dist/services/video/screenshot-generator.js +8 -2
  79. package/dist/skills/phase/smoke-test/SKILL.md +80 -0
  80. package/dist/utils/json-extract.d.ts +6 -0
  81. package/dist/utils/json-extract.js +44 -0
  82. package/dist/workspace/__tests__/workspace-manager.test.d.ts +7 -0
  83. package/dist/workspace/__tests__/workspace-manager.test.js +52 -0
  84. package/dist/workspace/workspace-manager.d.ts +31 -0
  85. package/dist/workspace/workspace-manager.js +96 -10
  86. package/package.json +1 -1
  87. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +0 -4
  88. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +0 -133
  89. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +0 -4
  90. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +0 -336
  91. package/dist/services/lifecycle-agent/index.d.ts +0 -24
  92. package/dist/services/lifecycle-agent/index.js +0 -25
  93. package/dist/services/lifecycle-agent/phase-criteria.d.ts +0 -57
  94. package/dist/services/lifecycle-agent/phase-criteria.js +0 -335
  95. package/dist/services/lifecycle-agent/transition-rules.d.ts +0 -60
  96. package/dist/services/lifecycle-agent/transition-rules.js +0 -184
  97. package/dist/services/lifecycle-agent/types.d.ts +0 -190
  98. package/dist/services/lifecycle-agent/types.js +0 -12
@@ -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}`);
@@ -69,9 +70,17 @@ export async function resolveStandalonePR(options) {
69
70
  // For fork PRs, pass fallback info so prepareWorkspace can fetch the PR ref
70
71
  // from upstream if the fork branch is unavailable.
71
72
  // For deleted forks, clone upstream directly and always use PR ref fallback.
72
- const repoPath = prepareWorkspace(cloneOwner, cloneRepo, headRef, prInfo.prNumber, githubToken, verbose, isFork
73
- ? { upstreamOwner: owner, upstreamRepo: repo }
74
- : undefined);
73
+ const repoPath = prepareWorkspace({
74
+ owner: cloneOwner,
75
+ repo: cloneRepo,
76
+ headRef,
77
+ prNumber: prInfo.prNumber,
78
+ token: githubToken,
79
+ verbose,
80
+ forkFallback: isFork
81
+ ? { upstreamOwner: owner, upstreamRepo: repo }
82
+ : undefined,
83
+ });
75
84
  try {
76
85
  // Run Claude Agent SDK to evaluate and fix comments
77
86
  const systemPrompt = createResolveSystemPrompt();
@@ -104,6 +113,7 @@ export async function resolveStandalonePR(options) {
104
113
  logInfo('Agent completed, parsing results...');
105
114
  const responseText = message.result || lastAssistantResponse;
106
115
  const parsed = tryExtractResult(responseText, 'resolve_result');
116
+ // eslint-disable-next-line max-depth
107
117
  if (isResolveResult(parsed)) {
108
118
  resolveResult = parsed;
109
119
  }
@@ -114,17 +124,19 @@ export async function resolveStandalonePR(options) {
114
124
  else {
115
125
  logError(`Agent incomplete: ${message.subtype}`);
116
126
  // Try to salvage partial results from last response
127
+ // eslint-disable-next-line max-depth
117
128
  if (lastAssistantResponse) {
118
129
  const salvaged = tryExtractResult(lastAssistantResponse, 'resolve_result');
130
+ // eslint-disable-next-line max-depth
119
131
  if (isResolveResult(salvaged)) {
120
132
  resolveResult = salvaged;
121
133
  }
122
134
  }
123
135
  }
124
136
  }
125
- // Commit and push any changes the agent made
137
+ // Fallback: commit any leftover uncommitted changes the agent didn't commit itself
126
138
  if (hasUncommittedChanges(repoPath)) {
127
- logInfo('Committing changes...');
139
+ logInfo('Committing remaining uncommitted changes...');
128
140
  execSync('git add -A', { cwd: repoPath, stdio: 'pipe' });
129
141
  execSync('git commit -m "Resolve PR review comments\n\nAutomated resolution by Edsger AI"', { cwd: repoPath, stdio: 'pipe' });
130
142
  }
@@ -142,13 +154,16 @@ export async function resolveStandalonePR(options) {
142
154
  for (const comment of comments) {
143
155
  // Map comment_id back to real GraphQL thread ID
144
156
  const threadId = commentIdToThreadId.get(comment.comment_id);
157
+ // eslint-disable-next-line max-depth
145
158
  if (!threadId) {
146
159
  logError(`Unknown comment_id "${comment.comment_id}", skipping reply`);
147
160
  threadsErrored++;
148
161
  continue;
149
162
  }
163
+ // eslint-disable-next-line max-depth
150
164
  try {
151
165
  const replied = await replyToReviewThread(octokit, threadId, comment.reply, verbose);
166
+ // eslint-disable-next-line max-depth
152
167
  if (replied && comment.action === 'changed') {
153
168
  // Resolve the thread since the change was made
154
169
  await resolveReviewThread(octokit, threadId, verbose);
@@ -178,6 +193,7 @@ export async function resolveStandalonePR(options) {
178
193
  ? 'Changes were made to address review feedback. Please re-review.'
179
194
  : 'Reviewed this comment. No changes were made at this time.';
180
195
  const replied = await replyToReviewThread(octokit, thread.id, genericReply, verbose);
196
+ // eslint-disable-next-line max-depth
181
197
  if (replied) {
182
198
  threadsSkipped++;
183
199
  }
@@ -212,6 +228,14 @@ export async function resolveStandalonePR(options) {
212
228
  // Non-critical
213
229
  }
214
230
  }
231
+ // Clean up workspace on success
232
+ try {
233
+ rmSync(repoPath, { recursive: true, force: true });
234
+ logInfo(`Cleaned up workspace: ${repoPath}`);
235
+ }
236
+ catch {
237
+ // ignore cleanup errors
238
+ }
215
239
  return {
216
240
  status: 'success',
217
241
  message: `Resolved ${threadsAddressed} threads, skipped ${threadsSkipped}, ${threadsErrored} errors`,
@@ -228,14 +252,6 @@ export async function resolveStandalonePR(options) {
228
252
  logInfo(`Workspace preserved for inspection: ${repoPath}`);
229
253
  throw innerError;
230
254
  }
231
- // Only clean up on success
232
- try {
233
- rmSync(repoPath, { recursive: true, force: true });
234
- logInfo(`Cleaned up workspace: ${repoPath}`);
235
- }
236
- catch {
237
- // ignore cleanup errors
238
- }
239
255
  }
240
256
  catch (error) {
241
257
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -27,7 +27,8 @@ export function createResolveSystemPrompt() {
27
27
  2. For each comment, examine the relevant code
28
28
  3. If you agree: make the change in the file
29
29
  4. If you disagree: skip it (do NOT modify the file for that comment)
30
- 5. After 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,19 +8,29 @@
8
8
  export declare function buildCredentialArgs(token: string): string[];
9
9
  /**
10
10
  * Get the workspace path for a PR resolve operation.
11
+ * Includes owner/repo to avoid collisions when resolving PRs from different repos.
11
12
  */
12
- export declare function getResolveWorkspacePath(prNumber: number): string;
13
+ export declare function getResolveWorkspacePath(owner: string, repo: string, prNumber: number): string;
13
14
  export interface ForkFallbackInfo {
14
15
  upstreamOwner: string;
15
16
  upstreamRepo: string;
16
17
  }
18
+ export interface PrepareWorkspaceOptions {
19
+ owner: string;
20
+ repo: string;
21
+ headRef: string;
22
+ prNumber: number;
23
+ token: string;
24
+ verbose?: boolean;
25
+ forkFallback?: ForkFallbackInfo;
26
+ }
17
27
  /**
18
28
  * Clone or reuse a repo for PR resolve.
19
29
  * For fork PRs, clones from the fork repo. If the fork branch is unavailable,
20
30
  * falls back to fetching the PR ref from the upstream repo.
21
31
  * Returns the workspace path.
22
32
  */
23
- export declare function prepareWorkspace(owner: string, repo: string, headRef: string, prNumber: number, token: string, verbose?: boolean, forkFallback?: ForkFallbackInfo): string;
33
+ export declare function prepareWorkspace(options: PrepareWorkspaceOptions): string;
24
34
  /**
25
35
  * Push changes from workspace back to remote.
26
36
  */
@@ -21,9 +21,10 @@ export function buildCredentialArgs(token) {
21
21
  }
22
22
  /**
23
23
  * Get the workspace path for a PR resolve operation.
24
+ * Includes owner/repo to avoid collisions when resolving PRs from different repos.
24
25
  */
25
- export function getResolveWorkspacePath(prNumber) {
26
- return join(homedir(), 'edsger', `pr-resolve-${prNumber}`);
26
+ export function getResolveWorkspacePath(owner, repo, prNumber) {
27
+ return join(homedir(), 'edsger', `pr-resolve-${owner}-${repo}-${prNumber}`);
27
28
  }
28
29
  /**
29
30
  * Clone or reuse a repo for PR resolve.
@@ -31,8 +32,9 @@ export function getResolveWorkspacePath(prNumber) {
31
32
  * falls back to fetching the PR ref from the upstream repo.
32
33
  * Returns the workspace path.
33
34
  */
34
- export function prepareWorkspace(owner, repo, headRef, prNumber, token, verbose, forkFallback) {
35
- const repoPath = getResolveWorkspacePath(prNumber);
35
+ export function prepareWorkspace(options) {
36
+ const { owner, repo, headRef, prNumber, token, verbose, forkFallback } = options;
37
+ const repoPath = getResolveWorkspacePath(owner, repo, prNumber);
36
38
  const repoUrl = `https://github.com/${owner}/${repo}.git`;
37
39
  const gitCredentialArgs = buildCredentialArgs(token);
38
40
  if (existsSync(join(repoPath, '.git'))) {
@@ -1,46 +1,46 @@
1
1
  import assert from 'node:assert';
2
2
  import { describe, it } from 'node:test';
3
3
  import { createStandaloneReviewSystemPrompt, createStandaloneReviewUserPrompt, } from '../prompts.js';
4
- describe('createStandaloneReviewSystemPrompt', () => {
5
- it('includes review focus areas', () => {
4
+ void describe('createStandaloneReviewSystemPrompt', () => {
5
+ void it('includes review focus areas', () => {
6
6
  const prompt = createStandaloneReviewSystemPrompt();
7
7
  assert.ok(prompt.includes('Code Quality'));
8
8
  assert.ok(prompt.includes('Security'));
9
9
  assert.ok(prompt.includes('Performance'));
10
10
  });
11
- it('specifies JSON result format', () => {
11
+ void it('specifies JSON result format', () => {
12
12
  const prompt = createStandaloneReviewSystemPrompt();
13
13
  assert.ok(prompt.includes('review_result'));
14
14
  assert.ok(prompt.includes('"file"'));
15
15
  assert.ok(prompt.includes('"line"'));
16
16
  assert.ok(prompt.includes('"comment"'));
17
17
  });
18
- it('includes assessment options', () => {
18
+ void it('includes assessment options', () => {
19
19
  const prompt = createStandaloneReviewSystemPrompt();
20
20
  assert.ok(prompt.includes('APPROVE'));
21
21
  assert.ok(prompt.includes('REQUEST_CHANGES'));
22
22
  assert.ok(prompt.includes('COMMENT'));
23
23
  });
24
- it('does not include feature/checklist instructions', () => {
24
+ void it('does not include feature/checklist instructions', () => {
25
25
  const prompt = createStandaloneReviewSystemPrompt();
26
26
  assert.ok(!prompt.includes('checklist'));
27
27
  assert.ok(!prompt.includes('feature_id'));
28
28
  assert.ok(!prompt.includes('user stories'));
29
29
  });
30
30
  });
31
- describe('createStandaloneReviewUserPrompt', () => {
32
- it('includes context info in the prompt', () => {
31
+ void describe('createStandaloneReviewUserPrompt', () => {
32
+ void it('includes context info in the prompt', () => {
33
33
  const contextInfo = '# Pull Request\n**Title**: Fix auth bug\n';
34
34
  const prompt = createStandaloneReviewUserPrompt(contextInfo);
35
35
  assert.ok(prompt.includes('Fix auth bug'));
36
36
  });
37
- it('includes review instructions', () => {
37
+ void it('includes review instructions', () => {
38
38
  const prompt = createStandaloneReviewUserPrompt('some context');
39
39
  assert.ok(prompt.includes('Analyze Each File'));
40
40
  assert.ok(prompt.includes('Identify Issues'));
41
41
  assert.ok(prompt.includes('Actionable Feedback'));
42
42
  });
43
- it('mentions severity categories', () => {
43
+ void it('mentions severity categories', () => {
44
44
  const prompt = createStandaloneReviewUserPrompt('context');
45
45
  assert.ok(prompt.includes('Critical'));
46
46
  assert.ok(prompt.includes('Major'));
@@ -36,7 +36,7 @@ function mapCommentsToReviewPayload(agentComments, files) {
36
36
  }
37
37
  return result;
38
38
  }
39
- describe('review comment mapping (integration)', () => {
39
+ void describe('review comment mapping (integration)', () => {
40
40
  const samplePatch = `@@ -1,5 +1,7 @@
41
41
  import { useState } from 'react'
42
42
 
@@ -47,7 +47,7 @@ describe('review comment mapping (integration)', () => {
47
47
  + const [count, setCount] = useState<number>(0)
48
48
  return <div>{count}</div>`;
49
49
  const files = [{ filename: 'src/App.tsx', patch: samplePatch }];
50
- it('maps exact line comments to correct diff positions', () => {
50
+ void it('maps exact line comments to correct diff positions', () => {
51
51
  const comments = [
52
52
  {
53
53
  file: 'src/App.tsx',
@@ -63,7 +63,7 @@ describe('review comment mapping (integration)', () => {
63
63
  // No "Note" prefix since line was exact
64
64
  assert.ok(!result[0].body.includes('**Note**'));
65
65
  });
66
- it('adjusts line numbers when exact line not in diff', () => {
66
+ void it('adjusts line numbers when exact line not in diff', () => {
67
67
  // Line 2 is blank (in diff) but line 100 is way outside
68
68
  const comments = [{ file: 'src/App.tsx', line: 8, comment: 'Fix this' }];
69
69
  const result = mapCommentsToReviewPayload(comments, files);
@@ -73,7 +73,7 @@ describe('review comment mapping (integration)', () => {
73
73
  }
74
74
  // If no match within range, it's filtered out - also valid
75
75
  });
76
- it('filters out comments for files not in diff', () => {
76
+ void it('filters out comments for files not in diff', () => {
77
77
  const comments = [
78
78
  {
79
79
  file: 'src/other.ts',
@@ -84,7 +84,7 @@ describe('review comment mapping (integration)', () => {
84
84
  const result = mapCommentsToReviewPayload(comments, files);
85
85
  assert.strictEqual(result.length, 0);
86
86
  });
87
- it('handles multiple comments on same file', () => {
87
+ void it('handles multiple comments on same file', () => {
88
88
  const comments = [
89
89
  { file: 'src/App.tsx', line: 3, comment: 'Comment A' },
90
90
  { file: 'src/App.tsx', line: 6, comment: 'Comment B' },
@@ -94,7 +94,7 @@ describe('review comment mapping (integration)', () => {
94
94
  assert.ok(result[0].body.includes('Comment A'));
95
95
  assert.ok(result[1].body.includes('Comment B'));
96
96
  });
97
- it('handles files with no patch (binary files)', () => {
97
+ void it('handles files with no patch (binary files)', () => {
98
98
  const binaryFiles = [
99
99
  { filename: 'image.png' }, // no patch
100
100
  { filename: 'src/App.tsx', patch: samplePatch },
@@ -14,6 +14,7 @@ import { createStandaloneReviewSystemPrompt, createStandaloneReviewUserPrompt, }
14
14
  /**
15
15
  * Review a standalone PR and post comments to GitHub.
16
16
  */
17
+ // eslint-disable-next-line complexity
17
18
  export async function reviewStandalonePR(options) {
18
19
  const { pullRequestUrl, githubToken, verbose, prId } = options;
19
20
  logInfo(`Starting standalone PR review: ${pullRequestUrl}`);
@@ -1,8 +1,8 @@
1
1
  import assert from 'node:assert';
2
2
  import { describe, it } from 'node:test';
3
3
  import { extractTextFromContent, tryExtractResult, tryParseJsonFromResponse, userMessage, } from '../agent-utils.js';
4
- describe('userMessage', () => {
5
- it('creates a user message object', () => {
4
+ void describe('userMessage', () => {
5
+ void it('creates a user message object', () => {
6
6
  const msg = userMessage('hello');
7
7
  assert.deepStrictEqual(msg, {
8
8
  type: 'user',
@@ -10,8 +10,8 @@ describe('userMessage', () => {
10
10
  });
11
11
  });
12
12
  });
13
- describe('extractTextFromContent', () => {
14
- it('extracts text items from content array', () => {
13
+ void describe('extractTextFromContent', () => {
14
+ void it('extracts text items from content array', () => {
15
15
  const content = [
16
16
  { type: 'text', text: 'hello' },
17
17
  { type: 'tool_use', id: '123', name: 'read', input: {} },
@@ -20,35 +20,35 @@ describe('extractTextFromContent', () => {
20
20
  const result = extractTextFromContent(content);
21
21
  assert.strictEqual(result, 'hello\nworld\n');
22
22
  });
23
- it('returns empty string for no text items', () => {
23
+ void it('returns empty string for no text items', () => {
24
24
  const content = [{ type: 'tool_use', id: '123', name: 'read', input: {} }];
25
25
  const result = extractTextFromContent(content);
26
26
  assert.strictEqual(result, '');
27
27
  });
28
- it('returns empty string for empty array', () => {
28
+ void it('returns empty string for empty array', () => {
29
29
  assert.strictEqual(extractTextFromContent([]), '');
30
30
  });
31
31
  });
32
- describe('tryParseJsonFromResponse', () => {
33
- it('parses JSON from code block', () => {
32
+ void describe('tryParseJsonFromResponse', () => {
33
+ void it('parses JSON from code block', () => {
34
34
  const text = 'Here is the result:\n```json\n{"key": "value"}\n```\nDone.';
35
35
  const result = tryParseJsonFromResponse(text);
36
36
  assert.deepStrictEqual(result, { key: 'value' });
37
37
  });
38
- it('parses raw JSON', () => {
38
+ void it('parses raw JSON', () => {
39
39
  const text = '{"key": "value"}';
40
40
  const result = tryParseJsonFromResponse(text);
41
41
  assert.deepStrictEqual(result, { key: 'value' });
42
42
  });
43
- it('returns null for invalid JSON', () => {
43
+ void it('returns null for invalid JSON', () => {
44
44
  const result = tryParseJsonFromResponse('not json at all');
45
45
  assert.strictEqual(result, null);
46
46
  });
47
- it('returns null for empty string', () => {
47
+ void it('returns null for empty string', () => {
48
48
  const result = tryParseJsonFromResponse('');
49
49
  assert.strictEqual(result, null);
50
50
  });
51
- it('parses JSON block with surrounding text', () => {
51
+ void it('parses JSON block with surrounding text', () => {
52
52
  const text = `I analyzed the code and here are my findings:
53
53
 
54
54
  \`\`\`json
@@ -66,22 +66,22 @@ That's my review.`;
66
66
  assert.ok(result.review_result);
67
67
  });
68
68
  });
69
- describe('tryExtractResult', () => {
70
- it('extracts keyed result from JSON code block', () => {
69
+ void describe('tryExtractResult', () => {
70
+ void it('extracts keyed result from JSON code block', () => {
71
71
  const text = '```json\n{"review_result": {"summary": "good"}}\n```';
72
72
  const result = tryExtractResult(text, 'review_result');
73
73
  assert.deepStrictEqual(result, { summary: 'good' });
74
74
  });
75
- it('returns whole object if key not found but JSON valid', () => {
75
+ void it('returns whole object if key not found but JSON valid', () => {
76
76
  const text = '{"summary": "good", "comments": []}';
77
77
  const result = tryExtractResult(text, 'review_result');
78
78
  assert.deepStrictEqual(result, { summary: 'good', comments: [] });
79
79
  });
80
- it('returns null for unparseable text', () => {
80
+ void it('returns null for unparseable text', () => {
81
81
  const result = tryExtractResult('no json here', 'review_result');
82
82
  assert.strictEqual(result, null);
83
83
  });
84
- it('extracts resolve_result key', () => {
84
+ void it('extracts resolve_result key', () => {
85
85
  const text = '```json\n{"resolve_result": {"comments": [{"comment_id": "comment_1", "action": "changed", "reply": "fixed"}]}}\n```';
86
86
  const result = tryExtractResult(text, 'resolve_result');
87
87
  assert.ok(result);
@@ -1,8 +1,8 @@
1
1
  import assert from 'node:assert';
2
2
  import { describe, it } from 'node:test';
3
3
  import { formatStandalonePRContextForPrompt, parsePullRequestUrl, } from '../context.js';
4
- describe('parsePullRequestUrl (re-exported)', () => {
5
- it('parses a standard GitHub PR URL', () => {
4
+ void describe('parsePullRequestUrl (re-exported)', () => {
5
+ void it('parses a standard GitHub PR URL', () => {
6
6
  const result = parsePullRequestUrl('https://github.com/owner/repo/pull/123');
7
7
  assert.deepStrictEqual(result, {
8
8
  owner: 'owner',
@@ -10,7 +10,7 @@ describe('parsePullRequestUrl (re-exported)', () => {
10
10
  prNumber: 123,
11
11
  });
12
12
  });
13
- it('parses URL with trailing path segments', () => {
13
+ void it('parses URL with trailing path segments', () => {
14
14
  const result = parsePullRequestUrl('https://github.com/my-org/my-repo/pull/456/files');
15
15
  assert.deepStrictEqual(result, {
16
16
  owner: 'my-org',
@@ -18,14 +18,14 @@ describe('parsePullRequestUrl (re-exported)', () => {
18
18
  prNumber: 456,
19
19
  });
20
20
  });
21
- it('returns null for non-PR URL', () => {
21
+ void it('returns null for non-PR URL', () => {
22
22
  assert.strictEqual(parsePullRequestUrl('https://github.com/owner/repo/issues/1'), null);
23
23
  });
24
- it('returns null for non-GitHub URL', () => {
24
+ void it('returns null for non-GitHub URL', () => {
25
25
  assert.strictEqual(parsePullRequestUrl('https://example.com/pull/1'), null);
26
26
  });
27
27
  });
28
- describe('formatStandalonePRContextForPrompt', () => {
28
+ void describe('formatStandalonePRContextForPrompt', () => {
29
29
  const context = {
30
30
  pullRequestUrl: 'https://github.com/owner/repo/pull/42',
31
31
  pullRequestNumber: 42,
@@ -61,33 +61,33 @@ describe('formatStandalonePRContextForPrompt', () => {
61
61
  },
62
62
  ],
63
63
  };
64
- it('includes PR URL and number', () => {
64
+ void it('includes PR URL and number', () => {
65
65
  const output = formatStandalonePRContextForPrompt(context);
66
66
  assert.ok(output.includes('#42'));
67
67
  assert.ok(output.includes('github.com/owner/repo/pull/42'));
68
68
  });
69
- it('includes PR title and author', () => {
69
+ void it('includes PR title and author', () => {
70
70
  const output = formatStandalonePRContextForPrompt(context);
71
71
  assert.ok(output.includes('Fix bug in auth'));
72
72
  assert.ok(output.includes('@testuser'));
73
73
  });
74
- it('includes branch info', () => {
74
+ void it('includes branch info', () => {
75
75
  const output = formatStandalonePRContextForPrompt(context);
76
76
  assert.ok(output.includes('fix/auth'));
77
77
  assert.ok(output.includes('main'));
78
78
  });
79
- it('includes file diff', () => {
79
+ void it('includes file diff', () => {
80
80
  const output = formatStandalonePRContextForPrompt(context);
81
81
  assert.ok(output.includes('src/auth.ts'));
82
82
  assert.ok(output.includes('+added'));
83
83
  assert.ok(output.includes('+5 -2'));
84
84
  });
85
- it('includes commit info', () => {
85
+ void it('includes commit info', () => {
86
86
  const output = formatStandalonePRContextForPrompt(context);
87
87
  assert.ok(output.includes('abc1234'));
88
88
  assert.ok(output.includes('fix: auth bug'));
89
89
  });
90
- it('includes PR body/description', () => {
90
+ void it('includes PR body/description', () => {
91
91
  const output = formatStandalonePRContextForPrompt(context);
92
92
  assert.ok(output.includes('This fixes the login issue'));
93
93
  });
@@ -155,6 +155,9 @@ export function getTransitiveDependencies(file, graph) {
155
155
  const stack = [file];
156
156
  while (stack.length > 0) {
157
157
  const current = stack.pop();
158
+ if (current === undefined) {
159
+ continue;
160
+ }
158
161
  const deps = graph.get(current);
159
162
  if (!deps) {
160
163
  continue;
@@ -169,11 +172,8 @@ export function getTransitiveDependencies(file, graph) {
169
172
  return result;
170
173
  }
171
174
  const MAX_FIX_ITERATIONS = 100;
172
- /**
173
- * Move a dependency file from a later PR to an earlier PR that needs it.
174
- * Returns true if a file was moved.
175
- */
176
- function moveDepToEarlierPR(dep, prIdx, sorted, fileToPRIndex, movedFiles, reason) {
175
+ function moveDepToEarlierPR(options) {
176
+ const { dep, prIdx, sorted, fileToPRIndex, movedFiles, reason } = options;
177
177
  const depPRIdx = fileToPRIndex.get(dep);
178
178
  if (depPRIdx === undefined || depPRIdx <= prIdx) {
179
179
  return false;
@@ -229,7 +229,15 @@ export function autoFixPROrdering(pullRequests, dependencyGraph) {
229
229
  for (const file of sorted[prIdx].files ?? []) {
230
230
  const transitiveDeps = getTransitiveDependencies(file.path, dependencyGraph);
231
231
  for (const dep of transitiveDeps) {
232
- const moved = moveDepToEarlierPR(dep, prIdx, sorted, fileToPRIndex, movedFiles, `imported by ${file.path}`);
232
+ const moved = moveDepToEarlierPR({
233
+ dep,
234
+ prIdx,
235
+ sorted,
236
+ fileToPRIndex,
237
+ movedFiles,
238
+ reason: `imported by ${file.path}`,
239
+ });
240
+ // eslint-disable-next-line max-depth
233
241
  if (moved) {
234
242
  changed = true;
235
243
  }
@@ -27,7 +27,9 @@ async function* prompt(analysisPrompt) {
27
27
  * then uses AI to produce a PR split plan saved to the database.
28
28
  * Human review is expected before running the pr-execution phase.
29
29
  */
30
- export const splitFeatureIntoPRs = async (options, config) => {
30
+ export const splitFeatureIntoPRs = async (options, config
31
+ // eslint-disable-next-line complexity
32
+ ) => {
31
33
  const { featureId, verbose, replaceExisting } = options;
32
34
  if (verbose) {
33
35
  logInfo(`Starting PR splitting for feature ID: ${featureId}`);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Unit tests for smoke-test github helpers.
3
+ *
4
+ * Covers the pure functions that shape GitHub compare data into a
5
+ * prompt-ready digest. Network-facing functions (fetchLatestTwoReleases,
6
+ * fetchCompare) are exercised only indirectly — their output shape is
7
+ * fed through buildDiffDigest / summariseStats here.
8
+ */
9
+ export {};