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
@@ -21,16 +21,20 @@ 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
+ * For fork PRs, clones from the fork repo. If the fork branch is unavailable,
32
+ * falls back to fetching the PR ref from the upstream repo.
30
33
  * Returns the workspace path.
31
34
  */
32
- export function prepareWorkspace(owner, repo, headRef, prNumber, token, verbose) {
33
- 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);
34
38
  const repoUrl = `https://github.com/${owner}/${repo}.git`;
35
39
  const gitCredentialArgs = buildCredentialArgs(token);
36
40
  if (existsSync(join(repoPath, '.git'))) {
@@ -66,16 +70,41 @@ export function prepareWorkspace(owner, repo, headRef, prNumber, token, verbose)
66
70
  }
67
71
  else {
68
72
  logInfo(`Cloning ${owner}/${repo} (branch: ${headRef})...`);
69
- execFileSync('git', [
70
- ...gitCredentialArgs,
71
- 'clone',
72
- '--branch',
73
- headRef,
74
- '--single-branch',
75
- repoUrl,
76
- repoPath,
77
- ], { stdio: 'pipe' });
78
- logSuccess(`Cloned to ${repoPath}`);
73
+ try {
74
+ execFileSync('git', [
75
+ ...gitCredentialArgs,
76
+ 'clone',
77
+ '--branch',
78
+ headRef,
79
+ '--single-branch',
80
+ repoUrl,
81
+ repoPath,
82
+ ], { stdio: 'pipe' });
83
+ logSuccess(`Cloned to ${repoPath}`);
84
+ }
85
+ catch (error) {
86
+ // If clone fails and this is a fork PR, fall back to upstream PR ref
87
+ if (forkFallback) {
88
+ logInfo(`Fork branch not available, falling back to PR ref from ${forkFallback.upstreamOwner}/${forkFallback.upstreamRepo}`);
89
+ const upstreamUrl = `https://github.com/${forkFallback.upstreamOwner}/${forkFallback.upstreamRepo}.git`;
90
+ execFileSync('git', [...gitCredentialArgs, 'clone', upstreamUrl, repoPath], { stdio: 'pipe' });
91
+ // Fetch the PR ref and check it out
92
+ execFileSync('git', [
93
+ ...gitCredentialArgs,
94
+ 'fetch',
95
+ 'origin',
96
+ `pull/${prNumber}/head:${headRef}`,
97
+ ], { cwd: repoPath, stdio: 'pipe' });
98
+ execSync(`git checkout ${headRef}`, {
99
+ cwd: repoPath,
100
+ stdio: 'pipe',
101
+ });
102
+ logSuccess(`Cloned via PR ref to ${repoPath}`);
103
+ }
104
+ else {
105
+ throw error;
106
+ }
107
+ }
79
108
  }
80
109
  // Configure git user for commits
81
110
  try {
@@ -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}`);
@@ -24,7 +24,9 @@ async function* prompt(analysisPrompt) {
24
24
  setTimeout(res, 10000);
25
25
  });
26
26
  }
27
- export const generateTechnicalDesign = async (options, config, checklistContext) => {
27
+ export const generateTechnicalDesign = async (options, config, checklistContext
28
+ // eslint-disable-next-line complexity
29
+ ) => {
28
30
  const { featureId, verbose } = options;
29
31
  if (verbose) {
30
32
  logInfo(`Starting technical design generation for feature ID: ${featureId}`);
@@ -6,7 +6,9 @@ import { executeTestCasesAnalysisQuery, parseAnalysisResult } from './agent.js';
6
6
  import { prepareTestCasesAnalysisContext } from './context.js';
7
7
  import { buildTestCasesAnalysisResult, deleteSpecificTestCases, deleteTestCaseArtifacts, getAllDraftTestCaseIds, resetReadyTestCasesToDraft, saveTestCasesAsDraft, updateTestCasesToReady, } from './outcome.js';
8
8
  import { createTestCasesAnalysisSystemPrompt } from './prompts.js';
9
- export const analyseTestCases = async (options, config, checklistContext) => {
9
+ export const analyseTestCases = async (options, config, checklistContext
10
+ // eslint-disable-next-line complexity
11
+ ) => {
10
12
  const { featureId, verbose } = options;
11
13
  if (verbose) {
12
14
  logInfo(`Starting test cases analysis for feature ID: ${featureId}`);
@@ -6,7 +6,9 @@ import { executeUserStoriesAnalysisQuery, parseAnalysisResult, } from './agent.j
6
6
  import { prepareUserStoriesAnalysisContext } from './context.js';
7
7
  import { buildUserStoriesAnalysisResult, deleteSpecificUserStories, deleteUserStoryArtifacts, getAllDraftUserStoryIds, resetReadyUserStoriesToDraft, saveUserStoriesAsDraft, updateUserStoriesToReady, } from './outcome.js';
8
8
  import { createUserStoriesAnalysisSystemPrompt } from './prompts.js';
9
- export const analyseUserStories = async (options, config, checklistContext) => {
9
+ export const analyseUserStories = async (options, config, checklistContext
10
+ // eslint-disable-next-line complexity
11
+ ) => {
10
12
  const { featureId, verbose } = options;
11
13
  if (verbose) {
12
14
  logInfo(`Starting user stories analysis for feature ID: ${featureId}`);
@@ -163,6 +163,7 @@ void describe('executeHook', () => {
163
163
  ) {
164
164
  // Return a function that returns an async iterable
165
165
  return () => ({
166
+ // eslint-disable-next-line @typescript-eslint/require-await
166
167
  async *[Symbol.asyncIterator]() {
167
168
  for (const msg of messages) {
168
169
  yield msg;
@@ -172,7 +173,7 @@ void describe('executeHook', () => {
172
173
  }
173
174
  function makeDeps(overrides = {}) {
174
175
  return {
175
- loadSkillFile: async () => defaultSkillFile,
176
+ loadSkillFile: () => Promise.resolve(defaultSkillFile),
176
177
  queryFn: mockQuery([
177
178
  {
178
179
  type: 'result',
@@ -184,7 +185,7 @@ void describe('executeHook', () => {
184
185
  };
185
186
  }
186
187
  void it('returns skipped when skill file not found', async () => {
187
- const deps = makeDeps({ loadSkillFile: async () => null });
188
+ const deps = makeDeps({ loadSkillFile: () => Promise.resolve(null) });
188
189
  const result = await executeHook(makeBinding(), makeContext(), false, deps);
189
190
  assert.strictEqual(result.status, 'skipped');
190
191
  assert.ok(result.message.includes('not found'));
@@ -266,7 +267,7 @@ void describe('executeHook', () => {
266
267
  void it('passes correct model and maxTurns from frontmatter', async () => {
267
268
  let capturedOptions = {};
268
269
  const deps = makeDeps({
269
- loadSkillFile: async () => ({
270
+ loadSkillFile: () => Promise.resolve({
270
271
  frontmatter: { model: 'haiku', maxTurns: 5 },
271
272
  body: 'Test prompt',
272
273
  }),
@@ -274,6 +275,7 @@ void describe('executeHook', () => {
274
275
  queryFn: ((opts) => {
275
276
  capturedOptions = opts.options;
276
277
  return {
278
+ // eslint-disable-next-line @typescript-eslint/require-await
277
279
  async *[Symbol.asyncIterator]() {
278
280
  yield {
279
281
  type: 'result',
@@ -292,7 +294,7 @@ void describe('executeHook', () => {
292
294
  void it('uses DEFAULT_MODEL when frontmatter has no model', async () => {
293
295
  let capturedOptions = {};
294
296
  const deps = makeDeps({
295
- loadSkillFile: async () => ({
297
+ loadSkillFile: () => Promise.resolve({
296
298
  frontmatter: {},
297
299
  body: 'Test prompt',
298
300
  }),
@@ -300,6 +302,7 @@ void describe('executeHook', () => {
300
302
  queryFn: ((opts) => {
301
303
  capturedOptions = opts.options;
302
304
  return {
305
+ // eslint-disable-next-line @typescript-eslint/require-await
303
306
  async *[Symbol.asyncIterator]() {
304
307
  yield {
305
308
  type: 'result',
@@ -37,10 +37,10 @@ function makeContext(overrides = {}) {
37
37
  }
38
38
  function makeDeps(overrides = {}) {
39
39
  return {
40
- executeHook: async (binding) => makeResult(binding),
40
+ executeHook: (binding) => Promise.resolve(makeResult(binding)),
41
41
  getCachedBindings: () => null,
42
42
  getBindingsForPhase: () => [],
43
- logHookEvent: async () => { },
43
+ logHookEvent: () => Promise.resolve(),
44
44
  ...overrides,
45
45
  };
46
46
  }
@@ -86,9 +86,9 @@ void describe('runHooksForPhase', () => {
86
86
  const deps = makeDeps({
87
87
  getCachedBindings: () => cached,
88
88
  getBindingsForPhase: () => [binding],
89
- executeHook: async (b) => {
89
+ executeHook: (b) => {
90
90
  executeCalls.push(b);
91
- return makeResult(b);
91
+ return Promise.resolve(makeResult(b));
92
92
  },
93
93
  });
94
94
  const result = await runHooksForPhase(makeContext(), deps);
@@ -109,9 +109,9 @@ void describe('runHooksForPhase', () => {
109
109
  const deps = makeDeps({
110
110
  getCachedBindings: () => cached,
111
111
  getBindingsForPhase: () => [b1, b2],
112
- executeHook: async (b) => {
112
+ executeHook: (b) => {
113
113
  order.push(b.id);
114
- return makeResult(b);
114
+ return Promise.resolve(makeResult(b));
115
115
  },
116
116
  });
117
117
  await runHooksForPhase(makeContext(), deps);
@@ -133,15 +133,15 @@ void describe('runHooksForPhase', () => {
133
133
  const deps = makeDeps({
134
134
  getCachedBindings: () => cached,
135
135
  getBindingsForPhase: () => [b1, b2],
136
- executeHook: async (b) => {
136
+ executeHook: (b) => {
137
137
  executedIds.push(b.id);
138
138
  if (b.id === 'blocker') {
139
- return makeResult(b, {
139
+ return Promise.resolve(makeResult(b, {
140
140
  status: 'error',
141
141
  message: 'Validation failed',
142
- });
142
+ }));
143
143
  }
144
- return makeResult(b);
144
+ return Promise.resolve(makeResult(b));
145
145
  },
146
146
  });
147
147
  const result = await runHooksForPhase(makeContext(), deps);
@@ -165,14 +165,14 @@ void describe('runHooksForPhase', () => {
165
165
  const deps = makeDeps({
166
166
  getCachedBindings: () => cached,
167
167
  getBindingsForPhase: () => [b1, b2],
168
- executeHook: async (b) => {
168
+ executeHook: (b) => {
169
169
  if (b.id === 'warner') {
170
- return makeResult(b, {
170
+ return Promise.resolve(makeResult(b, {
171
171
  status: 'error',
172
172
  message: 'Non-critical issue',
173
- });
173
+ }));
174
174
  }
175
- return makeResult(b);
175
+ return Promise.resolve(makeResult(b));
176
176
  },
177
177
  });
178
178
  const result = await runHooksForPhase(makeContext(), deps);
@@ -192,11 +192,11 @@ void describe('runHooksForPhase', () => {
192
192
  const deps = makeDeps({
193
193
  getCachedBindings: () => cached,
194
194
  getBindingsForPhase: () => [b1, b2],
195
- executeHook: async (b) => {
195
+ executeHook: (b) => {
196
196
  if (b.id === 'skipper') {
197
- return makeResult(b, { status: 'error', message: 'Ignored' });
197
+ return Promise.resolve(makeResult(b, { status: 'error', message: 'Ignored' }));
198
198
  }
199
- return makeResult(b);
199
+ return Promise.resolve(makeResult(b));
200
200
  },
201
201
  });
202
202
  const result = await runHooksForPhase(makeContext(), deps);
@@ -214,8 +214,9 @@ void describe('runHooksForPhase', () => {
214
214
  const deps = makeDeps({
215
215
  getCachedBindings: () => cached,
216
216
  getBindingsForPhase: () => [binding],
217
- logHookEvent: async ({ result: r }) => {
217
+ logHookEvent: ({ result: r }) => {
218
218
  logCalls.push(r.hookId);
219
+ return Promise.resolve();
219
220
  },
220
221
  });
221
222
  await runHooksForPhase(makeContext(), deps);
@@ -231,8 +232,8 @@ void describe('runHooksForPhase', () => {
231
232
  const deps = makeDeps({
232
233
  getCachedBindings: () => cached,
233
234
  getBindingsForPhase: () => [binding],
234
- logHookEvent: async () => {
235
- throw new Error('Logging failed');
235
+ logHookEvent: () => {
236
+ return Promise.reject(new Error('Logging failed'));
236
237
  },
237
238
  });
238
239
  const result = await runHooksForPhase(makeContext(), deps);
@@ -250,7 +251,7 @@ void describe('runHooksForPhase', () => {
250
251
  const deps = makeDeps({
251
252
  getCachedBindings: () => cached,
252
253
  getBindingsForPhase: () => [binding],
253
- executeHook: async (b) => makeResult(b, { status: 'skipped', message: 'Not found' }),
254
+ executeHook: (b) => Promise.resolve(makeResult(b, { status: 'skipped', message: 'Not found' })),
254
255
  });
255
256
  const result = await runHooksForPhase(makeContext(), deps);
256
257
  // 'skipped' is not an error, so on_failure policy should not apply
@@ -94,6 +94,7 @@ export async function executeHook(binding, context, verbose, deps = defaultDeps)
94
94
  if (message.type === 'assistant') {
95
95
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
96
  for (const item of (message.message?.content ?? [])) {
97
+ // eslint-disable-next-line max-depth
97
98
  if (item.type === 'text') {
98
99
  resultText += item.text;
99
100
  }