edsger 0.42.1 → 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 +29 -13
  32. package/dist/phases/pr-resolve/prompts.js +2 -1
  33. package/dist/phases/pr-resolve/workspace.d.ts +12 -2
  34. package/dist/phases/pr-resolve/workspace.js +6 -4
  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
@@ -52,7 +52,14 @@ export declare function connectWebDeploy(configId: string, verbose?: boolean): P
52
52
  connected: boolean;
53
53
  } | null>;
54
54
  export declare function listVercelProjects(configId: string, verbose?: boolean): Promise<VercelProject[]>;
55
- export declare function createVercelProject(configId: string, name: string, repo: string, framework?: string, rootDirectory?: string, verbose?: boolean): Promise<VercelProject | null>;
55
+ export declare function createVercelProject(options: {
56
+ configId: string;
57
+ name: string;
58
+ repo: string;
59
+ framework?: string;
60
+ rootDirectory?: string;
61
+ verbose?: boolean;
62
+ }): Promise<VercelProject | null>;
56
63
  export declare function linkVercelProject(configId: string, project: VercelProject, verbose?: boolean): Promise<WebDeployConfig | null>;
57
64
  export declare function listDeployments(configId: string, limit?: number, verbose?: boolean): Promise<VercelDeployment[]>;
58
65
  export declare function listEnvVars(configId: string, verbose?: boolean): Promise<VercelEnvVar[]>;
@@ -93,7 +93,8 @@ export async function listVercelProjects(configId, verbose) {
93
93
  return [];
94
94
  }
95
95
  }
96
- export async function createVercelProject(configId, name, repo, framework, rootDirectory, verbose) {
96
+ export async function createVercelProject(options) {
97
+ const { configId, name, repo, framework, rootDirectory, verbose } = options;
97
98
  if (verbose) {
98
99
  logInfo(`Creating Vercel project: ${name} → ${repo}`);
99
100
  }
@@ -36,7 +36,9 @@ const logAndMarkPhaseCompleted = async (result, verbose) => {
36
36
  * Orchestrate phase execution based on execution mode
37
37
  * Routes to appropriate phase sequence based on mode (only_*, from_*, full_pipeline)
38
38
  */
39
- export const runPipelineByMode = async (options, config, executionMode) => {
39
+ export const runPipelineByMode = async (options, config, executionMode
40
+ // eslint-disable-next-line complexity
41
+ ) => {
40
42
  const { featureId, verbose } = options;
41
43
  if (verbose) {
42
44
  logInfo(`🚀 Starting pipeline with mode: ${executionMode} for feature: ${featureId}`);
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  /**
2
3
  * Unit tests for screenshot composer pure functions.
3
4
  * Non-exported functions are re-implemented inline for testing (project pattern).
@@ -13,7 +13,9 @@ function truncateKeywords(keywords) {
13
13
  }
14
14
  return keywords.slice(0, 100).replace(/,[^,]*$/, '');
15
15
  }
16
- export const generateAppStoreAssets = async (options, config) => {
16
+ export const generateAppStoreAssets = async (options, config
17
+ // eslint-disable-next-line complexity
18
+ ) => {
17
19
  const { productId, targetStore, screenshotsOnly, listingsOnly, verbose } = options;
18
20
  if (verbose) {
19
21
  logInfo(`Starting app store generation for product: ${productId}`);
@@ -386,7 +386,10 @@ function escapeHtml(str) {
386
386
  */
387
387
  export async function generateStoreScreenshots(specs, options) {
388
388
  await ensurePlaywright(isPlaywrightAvailable);
389
- const chromium = (await loadChromium());
389
+ const chromium = await loadChromium();
390
+ if (!chromium) {
391
+ throw new Error('Failed to load chromium');
392
+ }
390
393
  const { productId, targetStore, locale: _locale, verbose } = options;
391
394
  const outputDir = join(tmpdir(), 'edsger-app-store-assets', productId);
392
395
  await mkdir(outputDir, { recursive: true });
@@ -403,13 +406,16 @@ export async function generateStoreScreenshots(specs, options) {
403
406
  logInfo(`Generating screenshot ${specIdx + 1}/${specs.length}: "${spec.headline}"`);
404
407
  // Step 1: Render the AI HTML template at a base viewport to get raw app mock
405
408
  const deviceFrame = mapDeviceFrame(spec.device_frame);
406
- const isPhone = deviceFrame === 'iphone';
407
- const isTablet = deviceFrame === 'ipad';
408
- const baseViewport = isPhone
409
- ? { width: 390, height: 844 }
410
- : isTablet
411
- ? { width: 1024, height: 1366 }
412
- : { width: 1280, height: 720 };
409
+ let baseViewport;
410
+ if (deviceFrame === 'iphone') {
411
+ baseViewport = { width: 390, height: 844 };
412
+ }
413
+ else if (deviceFrame === 'ipad') {
414
+ baseViewport = { width: 1024, height: 1366 };
415
+ }
416
+ else {
417
+ baseViewport = { width: 1280, height: 720 };
418
+ }
413
419
  let rawScreenshotBuffer;
414
420
  try {
415
421
  const rawContext = await browser.newContext({
@@ -435,8 +441,26 @@ export async function generateStoreScreenshots(specs, options) {
435
441
  // Use tight canvas dimensions so the device fills most of the frame image.
436
442
  // This prevents excess padding that would make the device appear small in composition.
437
443
  const rawDataUrl = `data:image/png;base64,${rawScreenshotBuffer.toString('base64')}`;
438
- const frameWidth = deviceFrame === 'iphone' ? 500 : deviceFrame === 'ipad' ? 900 : 1600;
439
- const frameHeight = deviceFrame === 'iphone' ? 1050 : deviceFrame === 'ipad' ? 1200 : 1000;
444
+ let frameWidth;
445
+ if (deviceFrame === 'iphone') {
446
+ frameWidth = 500;
447
+ }
448
+ else if (deviceFrame === 'ipad') {
449
+ frameWidth = 900;
450
+ }
451
+ else {
452
+ frameWidth = 1600;
453
+ }
454
+ let frameHeight;
455
+ if (deviceFrame === 'iphone') {
456
+ frameHeight = 1050;
457
+ }
458
+ else if (deviceFrame === 'ipad') {
459
+ frameHeight = 1200;
460
+ }
461
+ else {
462
+ frameHeight = 1000;
463
+ }
440
464
  const frameHtml = generateDeviceFrameHtml(rawDataUrl, deviceFrame, {
441
465
  background: 'transparent',
442
466
  shadow: true,
@@ -116,7 +116,9 @@ async function persistBranches(sortedBranches, featureId, verbose) {
116
116
  }
117
117
  }
118
118
  }
119
- export const planFeatureBranches = async (options, config) => {
119
+ export const planFeatureBranches = async (options, config
120
+ // eslint-disable-next-line complexity
121
+ ) => {
120
122
  const { featureId, verbose, replaceExisting } = options;
121
123
  if (verbose) {
122
124
  logInfo(`Starting branch planning for feature ID: ${featureId}`);
@@ -32,7 +32,9 @@ async function* prompt(bugFixPrompt) {
32
32
  setTimeout(res, 10000);
33
33
  });
34
34
  }
35
- export const fixTestFailures = async (options, config) => {
35
+ export const fixTestFailures = async (options, config
36
+ // eslint-disable-next-line complexity
37
+ ) => {
36
38
  const { featureId, testErrors, attemptNumber = 1, verbose } = options;
37
39
  if (verbose) {
38
40
  logInfo(`Starting bug fixing for feature ID: ${featureId} (Attempt ${attemptNumber})`);
@@ -170,7 +170,9 @@ message, lastAssistantResponse, featureId, verbose
170
170
  }
171
171
  return null;
172
172
  }
173
- export const implementFeatureCode = async (options, config, checklistContext) => {
173
+ export const implementFeatureCode = async (options, config, checklistContext
174
+ // eslint-disable-next-line complexity
175
+ ) => {
174
176
  const { featureId, verbose, baseBranch = 'main' } = options;
175
177
  if (verbose) {
176
178
  logInfo(`Starting code implementation for feature ID: ${featureId}`);
@@ -41,7 +41,9 @@ export const MAX_REFINE_ITERATIONS = 10;
41
41
  * Similar to technical-design, this includes an iterative improvement cycle:
42
42
  * refine → verification → improve → re-refine (if needed)
43
43
  */
44
- export const refineCodeFromPRFeedback = async (options, config, checklistContext) => {
44
+ export const refineCodeFromPRFeedback = async (options, config, checklistContext
45
+ // eslint-disable-next-line complexity
46
+ ) => {
45
47
  const { featureId, githubToken, verbose } = options;
46
48
  if (verbose) {
47
49
  logInfo(`Starting code refine for feature ID: ${featureId}`);
@@ -1,8 +1,8 @@
1
1
  import assert from 'node:assert';
2
2
  import { describe, it } from 'node:test';
3
3
  import { buildLineToPositionMap, findClosestPosition } from '../diff-utils.js';
4
- describe('buildLineToPositionMap', () => {
5
- it('maps simple additions correctly', () => {
4
+ void describe('buildLineToPositionMap', () => {
5
+ void it('maps simple additions correctly', () => {
6
6
  const patch = `@@ -1,3 +1,4 @@
7
7
  line 1
8
8
  +new line
@@ -18,7 +18,7 @@ describe('buildLineToPositionMap', () => {
18
18
  // line 3 is at position 4 (new file line 4)
19
19
  assert.strictEqual(map.get(4), 4);
20
20
  });
21
- it('handles deletions - deleted lines have no new file line number', () => {
21
+ void it('handles deletions - deleted lines have no new file line number', () => {
22
22
  const patch = `@@ -1,3 +1,2 @@
23
23
  line 1
24
24
  -deleted line
@@ -29,7 +29,7 @@ describe('buildLineToPositionMap', () => {
29
29
  // line 3 in old file becomes line 2 in new file, position 3
30
30
  assert.strictEqual(map.get(2), 3);
31
31
  });
32
- it('handles multiple hunks', () => {
32
+ void it('handles multiple hunks', () => {
33
33
  const patch = `@@ -1,2 +1,2 @@
34
34
  line 1
35
35
  +added
@@ -44,13 +44,13 @@ describe('buildLineToPositionMap', () => {
44
44
  assert.strictEqual(map.get(10), 3);
45
45
  assert.strictEqual(map.get(11), 4);
46
46
  });
47
- it('returns empty map for patch with only hunk header', () => {
47
+ void it('returns empty map for patch with only hunk header', () => {
48
48
  const patch = '@@ -0,0 +1 @@';
49
49
  const map = buildLineToPositionMap(patch);
50
50
  // Hunk header only, no content lines
51
51
  assert.strictEqual(map.size, 0);
52
52
  });
53
- it('handles hunk starting at line other than 1', () => {
53
+ void it('handles hunk starting at line other than 1', () => {
54
54
  const patch = `@@ -50,3 +50,3 @@
55
55
  context
56
56
  -old
@@ -62,8 +62,8 @@ describe('buildLineToPositionMap', () => {
62
62
  assert.strictEqual(map.get(51), 3);
63
63
  });
64
64
  });
65
- describe('findClosestPosition', () => {
66
- it('returns exact match when available', () => {
65
+ void describe('findClosestPosition', () => {
66
+ void it('returns exact match when available', () => {
67
67
  const map = new Map([
68
68
  [10, 5],
69
69
  [11, 6],
@@ -72,7 +72,7 @@ describe('findClosestPosition', () => {
72
72
  const result = findClosestPosition(11, map);
73
73
  assert.deepStrictEqual(result, { position: 6, actualLine: 11 });
74
74
  });
75
- it('returns nearby line below first', () => {
75
+ void it('returns nearby line below first', () => {
76
76
  const map = new Map([
77
77
  [10, 5],
78
78
  [15, 8],
@@ -84,7 +84,7 @@ describe('findClosestPosition', () => {
84
84
  const result = findClosestPosition(12, map);
85
85
  assert.deepStrictEqual(result, { position: 5, actualLine: 10 });
86
86
  });
87
- it('returns null when no line within range', () => {
87
+ void it('returns null when no line within range', () => {
88
88
  const map = new Map([
89
89
  [1, 1],
90
90
  [100, 50],
@@ -93,7 +93,7 @@ describe('findClosestPosition', () => {
93
93
  const result = findClosestPosition(50, map);
94
94
  assert.strictEqual(result, null);
95
95
  });
96
- it('returns null for empty map', () => {
96
+ void it('returns null for empty map', () => {
97
97
  const map = new Map();
98
98
  const result = findClosestPosition(5, map);
99
99
  assert.strictEqual(result, null);
@@ -67,7 +67,9 @@ baseBranchInfo, baseBranchForRebase, originalBaseBranchForRebase, verbose) {
67
67
  /**
68
68
  * Main code review function
69
69
  */
70
- export const reviewPullRequest = async (options, config, checklistContext) => {
70
+ export const reviewPullRequest = async (options, config, checklistContext
71
+ // eslint-disable-next-line complexity
72
+ ) => {
71
73
  const { featureId, githubToken, verbose } = options;
72
74
  if (verbose) {
73
75
  logInfo(`Starting code review for feature ID: ${featureId}`);
@@ -31,7 +31,9 @@ async function* prompt(testingPrompt) {
31
31
  setTimeout(res, 10000);
32
32
  });
33
33
  }
34
- export const writeCodeTests = async (options, config) => {
34
+ export const writeCodeTests = async (options, config
35
+ // eslint-disable-next-line complexity
36
+ ) => {
35
37
  const { featureId, verbose } = options;
36
38
  if (verbose) {
37
39
  logInfo(`Starting code testing phase for feature ID: ${featureId}`);
@@ -6,7 +6,9 @@ import { executeAnalysisQuery, parseAnalysisResult } from './agent.js';
6
6
  import { prepareAnalysisContext } from './context.js';
7
7
  import { buildAnalysisResult, deleteArtifacts, deleteSpecificArtifacts, getAllDraftArtifactIds, resetReadyArtifactsToDraft, saveAnalysisArtifactsAsDraft, updateArtifactsToReady, } from './outcome.js';
8
8
  import { createFeatureAnalysisSystemPrompt } from './prompts.js';
9
- export const analyseFeature = async (options, config, checklistContext) => {
9
+ export const analyseFeature = 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 feature analysis for feature ID: ${featureId}`);
@@ -326,7 +326,9 @@ async function saveTestResults(featureId, testStatus, structuredTestResult, last
326
326
  }
327
327
  return testReportResult;
328
328
  }
329
- export const runFunctionalTesting = async (options, config, checklistContext) => {
329
+ export const runFunctionalTesting = async (options, config, checklistContext
330
+ // eslint-disable-next-line complexity
331
+ ) => {
330
332
  const { featureId, verbose } = options;
331
333
  if (verbose) {
332
334
  logInfo(`Starting functional testing for feature ID: ${featureId}`);
@@ -49,7 +49,9 @@ contentSuggestions) {
49
49
  }
50
50
  return plans;
51
51
  }
52
- export const analyseGrowth = async (options, config) => {
52
+ export const analyseGrowth = async (options, config
53
+ // eslint-disable-next-line complexity
54
+ ) => {
53
55
  const { productId, verbose, guidance, analysisId } = options;
54
56
  if (verbose) {
55
57
  logInfo(`Starting growth analysis for product ID: ${productId}`);
@@ -110,26 +110,26 @@ function makeDeps(aiResult, contextOverride) {
110
110
  saveSnapCalls,
111
111
  saveRptCalls,
112
112
  updateRptCalls,
113
- prepareContext: async () => ({
113
+ prepareContext: () => Promise.resolve({
114
114
  context: contextOverride || makeContext(),
115
115
  analysisPrompt: 'fake prompt',
116
116
  }),
117
- executeQuery: async () => aiResult,
118
- batchCreate: async (_productId, competitors) => {
117
+ executeQuery: () => Promise.resolve(aiResult),
118
+ batchCreate: (_productId, competitors) => {
119
119
  batchCreateCalls.push([_productId, competitors]);
120
- return competitors.map((c) => makeCompetitor(c.name, `discovered-${batchCreateCounter++}`, 'suggested'));
120
+ return Promise.resolve(competitors.map((c) => makeCompetitor(c.name, `discovered-${batchCreateCounter++}`, 'suggested')));
121
121
  },
122
- saveSnap: async (snap) => {
122
+ saveSnap: (snap) => {
123
123
  saveSnapCalls.push([snap]);
124
- return makeSnapshot(snap.competitor_id, `snap-${saveSnapCalls.length}`);
124
+ return Promise.resolve(makeSnapshot(snap.competitor_id, `snap-${saveSnapCalls.length}`));
125
125
  },
126
- saveRpt: async (report) => {
126
+ saveRpt: (report) => {
127
127
  saveRptCalls.push([report]);
128
- return makeReport(`rpt-${saveRptCalls.length}`);
128
+ return Promise.resolve(makeReport(`rpt-${saveRptCalls.length}`));
129
129
  },
130
- updateRpt: async (_id, updates) => {
130
+ updateRpt: (_id, updates) => {
131
131
  updateRptCalls.push([_id, updates]);
132
- return makeReport(_id);
132
+ return Promise.resolve(makeReport(_id));
133
133
  },
134
134
  };
135
135
  }
@@ -368,8 +368,8 @@ void describe('analyseIntelligence — guidance', () => {
368
368
  void describe('analyseIntelligence — exception in deps', () => {
369
369
  void it('should return error when context fetch throws', async () => {
370
370
  const deps = makeDeps(null);
371
- deps.prepareContext = async () => {
372
- throw new Error('Network error');
371
+ deps.prepareContext = () => {
372
+ return Promise.reject(new Error('Network error'));
373
373
  };
374
374
  const result = await analyseIntelligence({ productId: 'prod-001' }, fakeConfig, deps);
375
375
  assert.strictEqual(result.status, 'error');
@@ -261,12 +261,14 @@ function parseIntelligenceResult(responseText) {
261
261
  error: 'JSON parsing failed: could not extract structured data from response',
262
262
  };
263
263
  }
264
+ // eslint-disable-next-line @typescript-eslint/require-await
264
265
  async function* prompt(analysisPrompt) {
265
266
  yield {
266
267
  type: 'user',
267
268
  message: { role: 'user', content: analysisPrompt },
268
269
  };
269
270
  }
271
+ // eslint-disable-next-line complexity
270
272
  export async function executeIntelligenceQuery(currentPrompt, systemPrompt, _config, verbose, cwd) {
271
273
  let lastAssistantResponse = '';
272
274
  let structuredResult = null;
@@ -74,6 +74,7 @@ const defaultDeps = {
74
74
  * 2. AI agent researches competitors and market via web search
75
75
  * 3. Save discovered competitors, snapshots, and report
76
76
  */
77
+ // eslint-disable-next-line complexity
77
78
  export async function analyseIntelligence(options, config, deps = defaultDeps) {
78
79
  const { productId, reportType = 'competitive', verbose, guidance, reportId, } = options;
79
80
  try {
@@ -29,9 +29,19 @@ export const createIntelligencePromptWithContext = (productId, contextInfo, repo
29
29
  **Competitor Discovery**: While analyzing confirmed competitors, also look for new entrants or products that should be tracked. Include any new finds in the "discovered_competitors" array.`;
30
30
  }
31
31
  else {
32
+ let focusLabel;
33
+ if (reportType === 'research') {
34
+ focusLabel = 'papers and reports';
35
+ }
36
+ else if (reportType === 'market') {
37
+ focusLabel = 'market signals and trends';
38
+ }
39
+ else {
40
+ focusLabel = 'product-market fit metrics';
41
+ }
32
42
  discoveryInstructions = `
33
43
 
34
- **Note**: This is a ${reportType} analysis — focus on ${reportType === 'research' ? 'papers and reports' : reportType === 'market' ? 'market signals and trends' : 'product-market fit metrics'}. Do NOT discover new competitors. Set "discovered_competitors" to an empty array.`;
44
+ **Note**: This is a ${reportType} analysis — focus on ${focusLabel}. Do NOT discover new competitors. Set "discovered_competitors" to an empty array.`;
35
45
  }
36
46
  const researchInstructions = reportType === 'research'
37
47
  ? `
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  /**
2
3
  * Output format contracts for each phase.
3
4
  * These define the JSON structure that LLM responses must follow.
@@ -20,8 +20,8 @@ function makePR(overrides) {
20
20
  ...overrides,
21
21
  };
22
22
  }
23
- function cf(path, change_type = 'modified') {
24
- return { path, change_type };
23
+ function cf(path, changeType = 'modified') {
24
+ return { path, change_type: changeType };
25
25
  }
26
26
  // ============================================================
27
27
  // findUnassignedFiles
@@ -133,8 +133,9 @@ void describe('applyAssignments', () => {
133
133
  ];
134
134
  const unassigned = [cf('b.ts', 'added')];
135
135
  const updates = [];
136
- const mockUpdater = async (prId, u) => {
136
+ const mockUpdater = (prId, u) => {
137
137
  updates.push({ prId, files: u.files });
138
+ return Promise.resolve();
138
139
  };
139
140
  const count = await applyAssignments([{ file: 'b.ts', pr_id: 'pr-1' }], unassigned, prs, mockUpdater);
140
141
  assert.strictEqual(count, 1);
@@ -155,8 +156,9 @@ void describe('applyAssignments', () => {
155
156
  ];
156
157
  const unassigned = [cf('a.ts')];
157
158
  const updates = [];
158
- const mockUpdater = async (_prId, u) => {
159
+ const mockUpdater = (_prId, u) => {
159
160
  updates.push({ files: u.files });
161
+ return Promise.resolve();
160
162
  };
161
163
  await applyAssignments([{ file: 'a.ts', pr_id: 'pr-1' }], unassigned, prs, mockUpdater);
162
164
  // File list should not have duplicates
@@ -168,8 +170,9 @@ void describe('applyAssignments', () => {
168
170
  const prs = [makePR({ id: 'pr-1', sequence: 1, files: [] })];
169
171
  const unassigned = [cf('x.ts', 'added')];
170
172
  let called = false;
171
- const mockUpdater = async () => {
173
+ const mockUpdater = () => {
172
174
  called = true;
175
+ return Promise.resolve();
173
176
  };
174
177
  const count = await applyAssignments([{ file: 'x.ts', pr_id: 'unknown-id' }], unassigned, prs, mockUpdater);
175
178
  assert.strictEqual(count, 0);
@@ -179,8 +182,9 @@ void describe('applyAssignments', () => {
179
182
  const prs = [makePR({ id: 'pr-1', sequence: 1, files: [] })];
180
183
  const unassigned = [cf('new.ts', 'added')];
181
184
  const updates = [];
182
- const mockUpdater = async (_prId, u) => {
185
+ const mockUpdater = (_prId, u) => {
183
186
  updates.push({ files: u.files });
187
+ return Promise.resolve();
184
188
  };
185
189
  await applyAssignments([{ file: 'new.ts', pr_id: 'pr-1' }], unassigned, prs, mockUpdater);
186
190
  assert.strictEqual(updates[0].files[0].change_type, 'added');
@@ -195,8 +199,9 @@ void describe('applyAssignments', () => {
195
199
  cf('b.ts', 'modified'),
196
200
  ];
197
201
  const updates = [];
198
- const mockUpdater = async (prId) => {
202
+ const mockUpdater = (prId) => {
199
203
  updates.push({ prId });
204
+ return Promise.resolve();
200
205
  };
201
206
  const count = await applyAssignments([
202
207
  { file: 'a.ts', pr_id: 'pr-1' },
@@ -208,8 +213,8 @@ void describe('applyAssignments', () => {
208
213
  void it('handles updater failure gracefully', async () => {
209
214
  const prs = [makePR({ id: 'pr-1', sequence: 1, files: [] })];
210
215
  const unassigned = [cf('a.ts')];
211
- const mockUpdater = async () => {
212
- throw new Error('DB error');
216
+ const mockUpdater = () => {
217
+ return Promise.reject(new Error('DB error'));
213
218
  };
214
219
  const count = await applyAssignments([{ file: 'a.ts', pr_id: 'pr-1' }], unassigned, prs, mockUpdater);
215
220
  assert.strictEqual(count, 0);
@@ -235,8 +240,9 @@ void describe('removeDeletedFilesFromPRs', () => {
235
240
  cf('gone.ts', 'deleted'),
236
241
  ];
237
242
  const updates = [];
238
- const mockUpdater = async (prId, u) => {
243
+ const mockUpdater = (prId, u) => {
239
244
  updates.push({ prId, files: u.files });
245
+ return Promise.resolve();
240
246
  };
241
247
  const count = await removeDeletedFilesFromPRs(changedFiles, prs, mockUpdater);
242
248
  assert.strictEqual(count, 1);
@@ -253,8 +259,9 @@ void describe('removeDeletedFilesFromPRs', () => {
253
259
  }),
254
260
  ];
255
261
  let called = false;
256
- const mockUpdater = async () => {
262
+ const mockUpdater = () => {
257
263
  called = true;
264
+ return Promise.resolve();
258
265
  };
259
266
  const count = await removeDeletedFilesFromPRs([cf('a.ts')], prs, mockUpdater);
260
267
  assert.strictEqual(count, 0);
@@ -274,8 +281,9 @@ void describe('removeDeletedFilesFromPRs', () => {
274
281
  }),
275
282
  ];
276
283
  const updates = [];
277
- const mockUpdater = async (prId) => {
284
+ const mockUpdater = (prId) => {
278
285
  updates.push(prId);
286
+ return Promise.resolve();
279
287
  };
280
288
  await removeDeletedFilesFromPRs([cf('b.ts', 'deleted')], prs, mockUpdater);
281
289
  // Only pr-2 should be updated
@@ -284,8 +292,9 @@ void describe('removeDeletedFilesFromPRs', () => {
284
292
  void it('handles PRs with null files', async () => {
285
293
  const prs = [makePR({ id: 'pr-1', sequence: 1, files: null })];
286
294
  let called = false;
287
- const mockUpdater = async () => {
295
+ const mockUpdater = () => {
288
296
  called = true;
297
+ return Promise.resolve();
289
298
  };
290
299
  const count = await removeDeletedFilesFromPRs([cf('a.ts', 'deleted')], prs, mockUpdater);
291
300
  assert.strictEqual(count, 0);
@@ -193,7 +193,9 @@ export async function fetchPRExecutionContext(featureId, verbose) {
193
193
  // If the branch was rebased, lastSyncedCommit is no longer in the history.
194
194
  // Treat this as a full re-execution so downstream logic uses the correct
195
195
  // prompt and skips incremental file-assignment against an invalid base.
196
- if (isIncrementalSync && !isAncestor(lastSyncedCommit, devRef)) {
196
+ if (isIncrementalSync &&
197
+ lastSyncedCommit &&
198
+ !isAncestor(lastSyncedCommit, devRef)) {
197
199
  if (verbose) {
198
200
  logInfo(`⚠️ Last synced commit ${lastSyncedCommit} is no longer in branch history (likely rebased). Treating as full re-execution.`);
199
201
  }
@@ -201,7 +203,7 @@ export async function fetchPRExecutionContext(featureId, verbose) {
201
203
  isIncrementalSync = false;
202
204
  }
203
205
  // Get diff info: for incremental, diff from last sync; for first run, diff from main
204
- const diffBase = isIncrementalSync ? lastSyncedCommit : 'main';
206
+ const diffBase = isIncrementalSync && lastSyncedCommit ? lastSyncedCommit : 'main';
205
207
  const diffStat = getDiffStat(diffBase, devRef);
206
208
  const changedFiles = getChangedFiles(diffBase, devRef);
207
209
  if (verbose) {
@@ -97,6 +97,7 @@ async function callLLMForAssignment(prompt, verbose) {
97
97
  })) {
98
98
  if (message.type === 'assistant' && message.message?.content) {
99
99
  for (const item of message.message.content) {
100
+ // eslint-disable-next-line max-depth
100
101
  if (item.type === 'text') {
101
102
  responseText += `${item.text}\n`;
102
103
  }