edsger 0.45.1 → 0.47.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 (140) hide show
  1. package/.claude/settings.local.json +3 -23
  2. package/dist/api/__tests__/app-store.test.d.ts +7 -0
  3. package/dist/api/__tests__/app-store.test.js +60 -0
  4. package/dist/api/__tests__/intelligence.test.d.ts +11 -0
  5. package/dist/api/__tests__/intelligence.test.js +315 -0
  6. package/dist/api/features/__tests__/feature-utils.test.d.ts +4 -0
  7. package/dist/api/features/__tests__/feature-utils.test.js +370 -0
  8. package/dist/api/features/__tests__/status-updater.test.d.ts +4 -0
  9. package/dist/api/features/__tests__/status-updater.test.js +88 -0
  10. package/dist/commands/build/__tests__/build.test.d.ts +5 -0
  11. package/dist/commands/build/__tests__/build.test.js +206 -0
  12. package/dist/commands/build/__tests__/detect-project.test.d.ts +6 -0
  13. package/dist/commands/build/__tests__/detect-project.test.js +160 -0
  14. package/dist/commands/build/__tests__/run-build.test.d.ts +6 -0
  15. package/dist/commands/build/__tests__/run-build.test.js +433 -0
  16. package/dist/commands/intelligence/__tests__/command.test.d.ts +4 -0
  17. package/dist/commands/intelligence/__tests__/command.test.js +48 -0
  18. package/dist/commands/run-sheet/index.js +6 -0
  19. package/dist/commands/workflow/core/__tests__/feature-filter.test.d.ts +5 -0
  20. package/dist/commands/workflow/core/__tests__/feature-filter.test.js +316 -0
  21. package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.d.ts +4 -0
  22. package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.js +397 -0
  23. package/dist/commands/workflow/core/__tests__/state-manager.test.d.ts +4 -0
  24. package/dist/commands/workflow/core/__tests__/state-manager.test.js +384 -0
  25. package/dist/commands/workflow/executors/phase-executor.js +3 -1
  26. package/dist/commands/workflow/phase-orchestrator.js +1 -2
  27. package/dist/config/__tests__/config.test.d.ts +4 -0
  28. package/dist/config/__tests__/config.test.js +286 -0
  29. package/dist/config/__tests__/feature-status.test.d.ts +4 -0
  30. package/dist/config/__tests__/feature-status.test.js +111 -0
  31. package/dist/errors/__tests__/index.test.d.ts +4 -0
  32. package/dist/errors/__tests__/index.test.js +349 -0
  33. package/dist/index.js +0 -0
  34. package/dist/phases/app-store-generation/__tests__/agent.test.d.ts +5 -0
  35. package/dist/phases/app-store-generation/__tests__/agent.test.js +142 -0
  36. package/dist/phases/app-store-generation/__tests__/context.test.d.ts +4 -0
  37. package/dist/phases/app-store-generation/__tests__/context.test.js +284 -0
  38. package/dist/phases/app-store-generation/__tests__/prompts.test.d.ts +4 -0
  39. package/dist/phases/app-store-generation/__tests__/prompts.test.js +122 -0
  40. package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.d.ts +5 -0
  41. package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.js +826 -0
  42. package/dist/phases/app-store-generation/index.js +1 -2
  43. package/dist/phases/branch-planning/index.js +1 -2
  44. package/dist/phases/bug-fixing/analyzer.js +1 -2
  45. package/dist/phases/code-implementation/index.js +1 -2
  46. package/dist/phases/code-refine/index.js +1 -2
  47. package/dist/phases/code-review/__tests__/diff-utils.test.d.ts +1 -0
  48. package/dist/phases/code-review/__tests__/diff-utils.test.js +101 -0
  49. package/dist/phases/code-review/index.js +1 -2
  50. package/dist/phases/code-testing/analyzer.js +1 -2
  51. package/dist/phases/feature-analysis/index.js +1 -2
  52. package/dist/phases/functional-testing/analyzer.js +1 -2
  53. package/dist/phases/growth-analysis/index.js +1 -2
  54. package/dist/phases/intelligence-analysis/__tests__/context.test.d.ts +4 -0
  55. package/dist/phases/intelligence-analysis/__tests__/context.test.js +192 -0
  56. package/dist/phases/intelligence-analysis/__tests__/matching.test.d.ts +13 -0
  57. package/dist/phases/intelligence-analysis/__tests__/matching.test.js +154 -0
  58. package/dist/phases/intelligence-analysis/__tests__/orchestration.test.d.ts +5 -0
  59. package/dist/phases/intelligence-analysis/__tests__/orchestration.test.js +378 -0
  60. package/dist/phases/intelligence-analysis/__tests__/prompts.test.d.ts +4 -0
  61. package/dist/phases/intelligence-analysis/__tests__/prompts.test.js +33 -0
  62. package/dist/phases/pr-execution/__tests__/file-assigner.test.d.ts +1 -0
  63. package/dist/phases/pr-execution/__tests__/file-assigner.test.js +303 -0
  64. package/dist/phases/pr-execution/index.js +1 -0
  65. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.d.ts +1 -0
  66. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +157 -0
  67. package/dist/phases/pr-resolve/__tests__/prompts.test.d.ts +1 -0
  68. package/dist/phases/pr-resolve/__tests__/prompts.test.js +116 -0
  69. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.d.ts +1 -0
  70. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.js +138 -0
  71. package/dist/phases/pr-resolve/__tests__/types.test.d.ts +1 -0
  72. package/dist/phases/pr-resolve/__tests__/types.test.js +43 -0
  73. package/dist/phases/pr-resolve/__tests__/workspace.test.d.ts +1 -0
  74. package/dist/phases/pr-resolve/__tests__/workspace.test.js +111 -0
  75. package/dist/phases/pr-review/__tests__/prompts.test.d.ts +1 -0
  76. package/dist/phases/pr-review/__tests__/prompts.test.js +49 -0
  77. package/dist/phases/pr-review/__tests__/review-comments.test.d.ts +1 -0
  78. package/dist/phases/pr-review/__tests__/review-comments.test.js +110 -0
  79. package/dist/phases/pr-shared/__tests__/agent-utils.test.d.ts +1 -0
  80. package/dist/phases/pr-shared/__tests__/agent-utils.test.js +91 -0
  81. package/dist/phases/pr-shared/__tests__/context.test.d.ts +1 -0
  82. package/dist/phases/pr-shared/__tests__/context.test.js +94 -0
  83. package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.d.ts +1 -0
  84. package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.js +331 -0
  85. package/dist/phases/pr-splitting/index.js +1 -2
  86. package/dist/phases/release-sync/github.d.ts +12 -0
  87. package/dist/phases/release-sync/github.js +39 -0
  88. package/dist/phases/release-sync/snapshot.js +0 -1
  89. package/dist/phases/run-sheet/index.d.ts +15 -0
  90. package/dist/phases/run-sheet/index.js +161 -29
  91. package/dist/phases/run-sheet/render.d.ts +23 -5
  92. package/dist/phases/run-sheet/render.js +195 -31
  93. package/dist/phases/smoke-test/__tests__/agent.test.d.ts +4 -0
  94. package/dist/phases/smoke-test/__tests__/agent.test.js +84 -0
  95. package/dist/phases/smoke-test/__tests__/github.test.d.ts +9 -0
  96. package/dist/phases/smoke-test/__tests__/github.test.js +120 -0
  97. package/dist/phases/smoke-test/__tests__/snapshot.test.d.ts +8 -0
  98. package/dist/phases/smoke-test/__tests__/snapshot.test.js +93 -0
  99. package/dist/phases/smoke-test/agent.js +2 -4
  100. package/dist/phases/smoke-test/github.d.ts +54 -0
  101. package/dist/phases/smoke-test/github.js +101 -0
  102. package/dist/phases/smoke-test/index.js +11 -6
  103. package/dist/phases/smoke-test/snapshot.d.ts +27 -0
  104. package/dist/phases/smoke-test/snapshot.js +157 -0
  105. package/dist/phases/technical-design/index.js +1 -2
  106. package/dist/phases/test-cases-analysis/index.js +1 -2
  107. package/dist/phases/user-stories-analysis/index.js +1 -2
  108. package/dist/services/coaching/__tests__/coaching-agent.test.d.ts +1 -0
  109. package/dist/services/coaching/__tests__/coaching-agent.test.js +74 -0
  110. package/dist/services/coaching/__tests__/coaching-loop.test.d.ts +1 -0
  111. package/dist/services/coaching/__tests__/coaching-loop.test.js +59 -0
  112. package/dist/services/coaching/__tests__/self-rating.test.d.ts +1 -0
  113. package/dist/services/coaching/__tests__/self-rating.test.js +188 -0
  114. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +4 -0
  115. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +133 -0
  116. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +4 -0
  117. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +336 -0
  118. package/dist/services/lifecycle-agent/index.d.ts +24 -0
  119. package/dist/services/lifecycle-agent/index.js +25 -0
  120. package/dist/services/lifecycle-agent/phase-criteria.d.ts +57 -0
  121. package/dist/services/lifecycle-agent/phase-criteria.js +335 -0
  122. package/dist/services/lifecycle-agent/transition-rules.d.ts +60 -0
  123. package/dist/services/lifecycle-agent/transition-rules.js +184 -0
  124. package/dist/services/lifecycle-agent/types.d.ts +190 -0
  125. package/dist/services/lifecycle-agent/types.js +12 -0
  126. package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.d.ts +1 -0
  127. package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.js +122 -0
  128. package/dist/services/phase-hooks/__tests__/hook-executor.test.d.ts +1 -0
  129. package/dist/services/phase-hooks/__tests__/hook-executor.test.js +321 -0
  130. package/dist/services/phase-hooks/__tests__/hook-runner.test.d.ts +1 -0
  131. package/dist/services/phase-hooks/__tests__/hook-runner.test.js +261 -0
  132. package/dist/services/phase-hooks/__tests__/plugin-loader.test.d.ts +1 -0
  133. package/dist/services/phase-hooks/__tests__/plugin-loader.test.js +158 -0
  134. package/dist/services/video/__tests__/video-pipeline.test.d.ts +6 -0
  135. package/dist/services/video/__tests__/video-pipeline.test.js +249 -0
  136. package/dist/workspace/__tests__/workspace-manager.test.d.ts +7 -0
  137. package/dist/workspace/__tests__/workspace-manager.test.js +52 -0
  138. package/dist/workspace/workspace-manager.js +17 -4
  139. package/package.json +1 -1
  140. package/.env.local +0 -12
@@ -0,0 +1,331 @@
1
+ import assert from 'node:assert';
2
+ import { describe, it } from 'node:test';
3
+ import { autoFixPROrdering, getTransitiveDependencies, parseImportSpecifiers, resolveImportToChangedFile, } from '../import-dep-validator.js';
4
+ // ============================================================
5
+ // parseImportSpecifiers
6
+ // ============================================================
7
+ void describe('parseImportSpecifiers', () => {
8
+ void it('parses named imports', () => {
9
+ const result = parseImportSpecifiers(`import { foo } from './utils'`);
10
+ assert.deepStrictEqual(result, ['./utils']);
11
+ });
12
+ void it('parses default imports', () => {
13
+ const result = parseImportSpecifiers(`import foo from './utils'`);
14
+ assert.deepStrictEqual(result, ['./utils']);
15
+ });
16
+ void it('parses namespace imports', () => {
17
+ const result = parseImportSpecifiers(`import * as foo from './utils'`);
18
+ assert.deepStrictEqual(result, ['./utils']);
19
+ });
20
+ void it('parses side-effect imports', () => {
21
+ const result = parseImportSpecifiers(`import './polyfill'`);
22
+ assert.deepStrictEqual(result, ['./polyfill']);
23
+ });
24
+ void it('parses type-only imports', () => {
25
+ const result = parseImportSpecifiers(`import type { Foo } from './types'`);
26
+ assert.deepStrictEqual(result, ['./types']);
27
+ });
28
+ void it('parses re-exports', () => {
29
+ const result = parseImportSpecifiers(`export { foo } from './utils'`);
30
+ assert.deepStrictEqual(result, ['./utils']);
31
+ });
32
+ void it('parses star re-exports', () => {
33
+ const result = parseImportSpecifiers(`export * from './utils'`);
34
+ assert.deepStrictEqual(result, ['./utils']);
35
+ });
36
+ void it('parses dynamic imports', () => {
37
+ const result = parseImportSpecifiers(`const m = import('./lazy')`);
38
+ assert.deepStrictEqual(result, ['./lazy']);
39
+ });
40
+ void it('parses multiple imports', () => {
41
+ const source = `
42
+ import { foo } from './utils'
43
+ import Bar from '../components/Bar'
44
+ import type { Baz } from './types'
45
+ `;
46
+ const result = parseImportSpecifiers(source);
47
+ assert.deepStrictEqual(result, ['./utils', '../components/Bar', './types']);
48
+ });
49
+ void it('filters out non-relative imports', () => {
50
+ const source = `
51
+ import { X } from 'lodash'
52
+ import React from 'react'
53
+ import { Y } from './local'
54
+ import { Z } from '@scope/package'
55
+ `;
56
+ const result = parseImportSpecifiers(source);
57
+ assert.deepStrictEqual(result, ['./local']);
58
+ });
59
+ void it('parses multiline imports', () => {
60
+ const source = `import {
61
+ foo,
62
+ bar,
63
+ baz
64
+ } from './utils'`;
65
+ const result = parseImportSpecifiers(source);
66
+ assert.deepStrictEqual(result, ['./utils']);
67
+ });
68
+ void it('deduplicates specifiers', () => {
69
+ const source = `
70
+ import { foo } from './utils'
71
+ import { bar } from './utils'
72
+ `;
73
+ const result = parseImportSpecifiers(source);
74
+ assert.deepStrictEqual(result, ['./utils']);
75
+ });
76
+ void it('handles .js extension in specifier', () => {
77
+ const result = parseImportSpecifiers(`import { X } from './utils.js'`);
78
+ assert.deepStrictEqual(result, ['./utils.js']);
79
+ });
80
+ void it('returns empty for empty source', () => {
81
+ assert.deepStrictEqual(parseImportSpecifiers(''), []);
82
+ });
83
+ void it('returns empty for non-relative only', () => {
84
+ const source = `import express from 'express'`;
85
+ assert.deepStrictEqual(parseImportSpecifiers(source), []);
86
+ });
87
+ void it('parses export default from', () => {
88
+ const result = parseImportSpecifiers(`export { default as X } from './module'`);
89
+ assert.deepStrictEqual(result, ['./module']);
90
+ });
91
+ });
92
+ // ============================================================
93
+ // resolveImportToChangedFile
94
+ // ============================================================
95
+ void describe('resolveImportToChangedFile', () => {
96
+ void it('resolves exact match with extension', () => {
97
+ const changed = new Set(['src/utils.ts']);
98
+ const result = resolveImportToChangedFile('./utils.ts', 'src/app.ts', changed);
99
+ assert.strictEqual(result, 'src/utils.ts');
100
+ });
101
+ void it('infers .ts extension', () => {
102
+ const changed = new Set(['src/utils.ts']);
103
+ const result = resolveImportToChangedFile('./utils', 'src/app.ts', changed);
104
+ assert.strictEqual(result, 'src/utils.ts');
105
+ });
106
+ void it('infers .tsx extension', () => {
107
+ const changed = new Set(['src/Component.tsx']);
108
+ const result = resolveImportToChangedFile('./Component', 'src/app.ts', changed);
109
+ assert.strictEqual(result, 'src/Component.tsx');
110
+ });
111
+ void it('resolves index file', () => {
112
+ const changed = new Set(['src/types/index.ts']);
113
+ const result = resolveImportToChangedFile('./types', 'src/app.ts', changed);
114
+ assert.strictEqual(result, 'src/types/index.ts');
115
+ });
116
+ void it('resolves .js to .ts (ESM convention)', () => {
117
+ const changed = new Set(['src/utils.ts']);
118
+ const result = resolveImportToChangedFile('./utils.js', 'src/app.ts', changed);
119
+ assert.strictEqual(result, 'src/utils.ts');
120
+ });
121
+ void it('resolves parent directory traversal', () => {
122
+ const changed = new Set(['src/shared/helpers.ts']);
123
+ const result = resolveImportToChangedFile('../shared/helpers', 'src/components/Button.ts', changed);
124
+ assert.strictEqual(result, 'src/shared/helpers.ts');
125
+ });
126
+ void it('resolves double parent traversal', () => {
127
+ const changed = new Set(['src/utils.ts']);
128
+ const result = resolveImportToChangedFile('../../utils', 'src/features/explore/App.ts', changed);
129
+ assert.strictEqual(result, 'src/utils.ts');
130
+ });
131
+ void it('returns null for non-changed file', () => {
132
+ const changed = new Set(['src/other.ts']);
133
+ const result = resolveImportToChangedFile('./utils', 'src/app.ts', changed);
134
+ assert.strictEqual(result, null);
135
+ });
136
+ void it('returns null for external package import path', () => {
137
+ // This shouldn't happen since parseImportSpecifiers filters these,
138
+ // but test defensive behavior
139
+ const changed = new Set(['src/utils.ts']);
140
+ const result = resolveImportToChangedFile('./nonexistent', 'src/app.ts', changed);
141
+ assert.strictEqual(result, null);
142
+ });
143
+ void it('prefers .ts over .tsx when both exist', () => {
144
+ const changed = new Set(['src/utils.ts', 'src/utils.tsx']);
145
+ const result = resolveImportToChangedFile('./utils', 'src/app.ts', changed);
146
+ assert.strictEqual(result, 'src/utils.ts');
147
+ });
148
+ });
149
+ // ============================================================
150
+ // getTransitiveDependencies
151
+ // ============================================================
152
+ void describe('getTransitiveDependencies', () => {
153
+ void it('returns direct dependency', () => {
154
+ const graph = new Map([
155
+ ['A', new Set(['B'])],
156
+ ['B', new Set()],
157
+ ]);
158
+ const result = getTransitiveDependencies('A', graph);
159
+ assert.deepStrictEqual(result, new Set(['B']));
160
+ });
161
+ void it('returns transitive dependencies', () => {
162
+ const graph = new Map([
163
+ ['A', new Set(['B'])],
164
+ ['B', new Set(['C'])],
165
+ ['C', new Set()],
166
+ ]);
167
+ const result = getTransitiveDependencies('A', graph);
168
+ assert.deepStrictEqual(result, new Set(['B', 'C']));
169
+ });
170
+ void it('handles diamond dependency', () => {
171
+ const graph = new Map([
172
+ ['A', new Set(['B', 'C'])],
173
+ ['B', new Set(['D'])],
174
+ ['C', new Set(['D'])],
175
+ ['D', new Set()],
176
+ ]);
177
+ const result = getTransitiveDependencies('A', graph);
178
+ assert.deepStrictEqual(result, new Set(['B', 'C', 'D']));
179
+ });
180
+ void it('handles cycles without infinite loop', () => {
181
+ const graph = new Map([
182
+ ['A', new Set(['B'])],
183
+ ['B', new Set(['A'])],
184
+ ]);
185
+ const result = getTransitiveDependencies('A', graph);
186
+ assert.deepStrictEqual(result, new Set(['B', 'A']));
187
+ });
188
+ void it('returns empty set for no dependencies', () => {
189
+ const graph = new Map([['A', new Set()]]);
190
+ const result = getTransitiveDependencies('A', graph);
191
+ assert.deepStrictEqual(result, new Set());
192
+ });
193
+ void it('returns empty set for unknown file', () => {
194
+ const graph = new Map();
195
+ const result = getTransitiveDependencies('unknown', graph);
196
+ assert.deepStrictEqual(result, new Set());
197
+ });
198
+ });
199
+ // ============================================================
200
+ // autoFixPROrdering
201
+ // ============================================================
202
+ function makePR(sequence, name, files, dependsOn) {
203
+ return {
204
+ sequence,
205
+ name,
206
+ description: name,
207
+ branch_name: `pr/feat/${sequence}-${name.toLowerCase().replace(/\s/g, '-')}`,
208
+ depends_on_branch_name: dependsOn ?? null,
209
+ files: files.map((f) => ({ path: f, change_type: 'modified' })),
210
+ };
211
+ }
212
+ void describe('autoFixPROrdering', () => {
213
+ void it('returns unchanged when no violations', () => {
214
+ const prs = [
215
+ makePR(1, 'Foundation', ['src/utils.ts']),
216
+ makePR(2, 'Components', ['src/app.ts'], 'pr/feat/1-foundation'),
217
+ ];
218
+ // app.ts depends on utils.ts, and utils.ts is in PR 1 (earlier) — OK
219
+ const graph = new Map([
220
+ ['src/app.ts', new Set(['src/utils.ts'])],
221
+ ['src/utils.ts', new Set()],
222
+ ]);
223
+ const result = autoFixPROrdering(prs, graph);
224
+ assert.strictEqual(result.movedFiles.length, 0);
225
+ assert.strictEqual(result.pullRequests[0].files?.length, 1);
226
+ assert.strictEqual(result.pullRequests[1].files?.length, 1);
227
+ });
228
+ void it('moves dependency from later PR to earlier PR', () => {
229
+ const prs = [
230
+ makePR(1, 'Components', ['src/app.ts']),
231
+ makePR(2, 'Utils', ['src/utils.ts'], 'pr/feat/1-components'),
232
+ ];
233
+ // app.ts (PR 1) depends on utils.ts (PR 2) — violation!
234
+ const graph = new Map([
235
+ ['src/app.ts', new Set(['src/utils.ts'])],
236
+ ['src/utils.ts', new Set()],
237
+ ]);
238
+ const result = autoFixPROrdering(prs, graph);
239
+ assert.strictEqual(result.movedFiles.length, 1);
240
+ assert.strictEqual(result.movedFiles[0].file, 'src/utils.ts');
241
+ // utils.ts should now be in PR 1
242
+ const pr1Files = result.pullRequests[0].files?.map((f) => f.path) ?? [];
243
+ assert.ok(pr1Files.includes('src/utils.ts'));
244
+ assert.ok(pr1Files.includes('src/app.ts'));
245
+ // PR 2 should be empty
246
+ assert.strictEqual(result.pullRequests[1].files?.length, 0);
247
+ });
248
+ void it('handles transitive dependency moves', () => {
249
+ const prs = [
250
+ makePR(1, 'App', ['src/app.ts']),
251
+ makePR(2, 'Service', ['src/service.ts'], 'pr/feat/1-app'),
252
+ makePR(3, 'Utils', ['src/utils.ts'], 'pr/feat/2-service'),
253
+ ];
254
+ // app.ts → service.ts → utils.ts (chain across 3 PRs)
255
+ const graph = new Map([
256
+ ['src/app.ts', new Set(['src/service.ts'])],
257
+ ['src/service.ts', new Set(['src/utils.ts'])],
258
+ ['src/utils.ts', new Set()],
259
+ ]);
260
+ const result = autoFixPROrdering(prs, graph);
261
+ // Both service.ts and utils.ts should move to PR 1
262
+ const pr1Files = result.pullRequests[0].files?.map((f) => f.path) ?? [];
263
+ assert.ok(pr1Files.includes('src/app.ts'));
264
+ assert.ok(pr1Files.includes('src/service.ts'));
265
+ assert.ok(pr1Files.includes('src/utils.ts'));
266
+ });
267
+ void it('moves dep to earliest PR that needs it', () => {
268
+ const prs = [
269
+ makePR(1, 'PR1', ['src/a.ts']),
270
+ makePR(2, 'PR2', ['src/b.ts'], 'pr/feat/1-pr1'),
271
+ makePR(3, 'PR3', ['src/utils.ts'], 'pr/feat/2-pr2'),
272
+ ];
273
+ // Both a.ts (PR 1) and b.ts (PR 2) import utils.ts (PR 3)
274
+ const graph = new Map([
275
+ ['src/a.ts', new Set(['src/utils.ts'])],
276
+ ['src/b.ts', new Set(['src/utils.ts'])],
277
+ ['src/utils.ts', new Set()],
278
+ ]);
279
+ const result = autoFixPROrdering(prs, graph);
280
+ // utils.ts should move to PR 1 (earliest needer)
281
+ const pr1Files = result.pullRequests[0].files?.map((f) => f.path) ?? [];
282
+ assert.ok(pr1Files.includes('src/utils.ts'));
283
+ });
284
+ void it('handles no dependencies at all', () => {
285
+ const prs = [
286
+ makePR(1, 'PR1', ['src/a.ts']),
287
+ makePR(2, 'PR2', ['src/b.ts'], 'pr/feat/1-pr1'),
288
+ ];
289
+ const graph = new Map([
290
+ ['src/a.ts', new Set()],
291
+ ['src/b.ts', new Set()],
292
+ ]);
293
+ const result = autoFixPROrdering(prs, graph);
294
+ assert.strictEqual(result.movedFiles.length, 0);
295
+ });
296
+ void it('handles single PR with all files', () => {
297
+ const prs = [makePR(1, 'Everything', ['src/a.ts', 'src/b.ts', 'src/c.ts'])];
298
+ const graph = new Map([
299
+ ['src/a.ts', new Set(['src/b.ts'])],
300
+ ['src/b.ts', new Set(['src/c.ts'])],
301
+ ['src/c.ts', new Set()],
302
+ ]);
303
+ const result = autoFixPROrdering(prs, graph);
304
+ assert.strictEqual(result.movedFiles.length, 0);
305
+ });
306
+ void it('does not mutate original PR files arrays', () => {
307
+ const originalFiles = [{ path: 'src/app.ts', change_type: 'modified' }];
308
+ const prs = [
309
+ {
310
+ sequence: 1,
311
+ name: 'PR1',
312
+ description: 'PR1',
313
+ files: originalFiles,
314
+ },
315
+ {
316
+ sequence: 2,
317
+ name: 'PR2',
318
+ description: 'PR2',
319
+ files: [{ path: 'src/utils.ts', change_type: 'modified' }],
320
+ },
321
+ ];
322
+ const graph = new Map([
323
+ ['src/app.ts', new Set(['src/utils.ts'])],
324
+ ['src/utils.ts', new Set()],
325
+ ]);
326
+ autoFixPROrdering(prs, graph);
327
+ // Original files array should not be modified
328
+ assert.strictEqual(originalFiles.length, 1);
329
+ assert.strictEqual(originalFiles[0].path, 'src/app.ts');
330
+ });
331
+ });
@@ -27,9 +27,8 @@ 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
31
30
  // eslint-disable-next-line complexity
32
- ) => {
31
+ export const splitFeatureIntoPRs = async (options, config) => {
33
32
  const { featureId, verbose, replaceExisting } = options;
34
33
  if (verbose) {
35
34
  logInfo(`Starting PR splitting for feature ID: ${featureId}`);
@@ -23,6 +23,7 @@ export interface CompareFile {
23
23
  }
24
24
  export interface CompareCommit {
25
25
  sha: string;
26
+ html_url?: string;
26
27
  commit: {
27
28
  message: string;
28
29
  author?: {
@@ -45,6 +46,17 @@ export declare function getDefaultBranchHead(owner: string, repo: string, token:
45
46
  sha: string;
46
47
  }>;
47
48
  export declare function fetchCompare(owner: string, repo: string, base: string, head: string, token: string): Promise<CompareResponse>;
49
+ /**
50
+ * Paginated commit list for base...head. Unlike `fetchCompare` (single
51
+ * page, capped at ~250 by GitHub), this loops pages until exhausted or
52
+ * the caller-specified cap is hit. We don't need file patches here —
53
+ * run-sheet just wants the commit list — so we ignore `data.files`.
54
+ */
55
+ export declare function fetchAllCompareCommits(owner: string, repo: string, base: string, head: string, token: string, maxPages?: number): Promise<{
56
+ commits: CompareCommit[];
57
+ truncated: boolean;
58
+ totalCommits: number;
59
+ }>;
48
60
  export declare function buildDiffDigest(compare: CompareResponse): string;
49
61
  export declare function summariseStats(compare: CompareResponse): {
50
62
  files_changed: number;
@@ -50,6 +50,7 @@ export async function fetchCompare(owner, repo, base, head, token) {
50
50
  })),
51
51
  commits: (data.commits ?? []).map((c) => ({
52
52
  sha: c.sha,
53
+ html_url: c.html_url ?? undefined,
53
54
  commit: {
54
55
  message: c.commit?.message ?? '',
55
56
  author: c.commit?.author
@@ -59,6 +60,44 @@ export async function fetchCompare(owner, repo, base, head, token) {
59
60
  })),
60
61
  };
61
62
  }
63
+ /**
64
+ * Paginated commit list for base...head. Unlike `fetchCompare` (single
65
+ * page, capped at ~250 by GitHub), this loops pages until exhausted or
66
+ * the caller-specified cap is hit. We don't need file patches here —
67
+ * run-sheet just wants the commit list — so we ignore `data.files`.
68
+ */
69
+ export async function fetchAllCompareCommits(owner, repo, base, head, token, maxPages = 8) {
70
+ const gh = octokit(token);
71
+ const perPage = 100;
72
+ const out = [];
73
+ let totalCommits = 0;
74
+ for (let page = 1; page <= maxPages; page++) {
75
+ const { data } = await gh.repos.compareCommits({
76
+ owner,
77
+ repo,
78
+ base,
79
+ head,
80
+ per_page: perPage,
81
+ page,
82
+ });
83
+ totalCommits = data.total_commits ?? totalCommits;
84
+ const batch = (data.commits ?? []).map((c) => ({
85
+ sha: c.sha,
86
+ html_url: c.html_url ?? undefined,
87
+ commit: {
88
+ message: c.commit?.message ?? '',
89
+ author: c.commit?.author
90
+ ? { name: c.commit.author.name ?? undefined }
91
+ : null,
92
+ },
93
+ }));
94
+ out.push(...batch);
95
+ if (batch.length < perPage) {
96
+ return { commits: out, truncated: false, totalCommits };
97
+ }
98
+ }
99
+ return { commits: out, truncated: out.length < totalCommits, totalCommits };
100
+ }
62
101
  export function buildDiffDigest(compare) {
63
102
  const lines = [];
64
103
  lines.push(`Total commits: ${compare.total_commits}`);
@@ -96,7 +96,6 @@ function userMessage(content) {
96
96
  async function* makePrompt(text) {
97
97
  yield userMessage(text);
98
98
  }
99
- // eslint-disable-next-line complexity -- agent loop with message-type handling
100
99
  export async function detectSnapshotVersion(options) {
101
100
  const { cwd, latestReleaseTag, verbose } = options;
102
101
  if (verbose) {
@@ -23,7 +23,9 @@ export interface RunSheetResult {
23
23
  runSheetId?: string;
24
24
  missingPlaceholders?: string[];
25
25
  cloneError?: string | null;
26
+ commitsError?: string | null;
26
27
  commitsTruncated?: boolean;
28
+ isDraft?: boolean;
27
29
  }
28
30
  /**
29
31
  * Atomic-create a `.lock` file alongside the workspace dir. Returns
@@ -36,4 +38,17 @@ export interface RunSheetResult {
36
38
  export declare function tryAcquireFileLock(lockPath: string): boolean;
37
39
  /** Exported for unit tests; not part of the public CLI surface. */
38
40
  export declare function releaseFileLock(lockPath: string): void;
41
+ /**
42
+ * Delete `run-sheet-*` workspaces that are both:
43
+ * - not currently locked (lock file missing or stale), AND
44
+ * - older than WORKSPACE_MAX_AGE_MS by mtime.
45
+ *
46
+ * Best-effort: any error on a single entry is logged and skipped so a
47
+ * corrupt dir doesn't block the current generation.
48
+ *
49
+ * Exported for unit tests.
50
+ */
51
+ export declare function pruneStaleRunSheetWorkspaces(workspaceRoot: string, now?: number, maxAgeMs?: number): {
52
+ removed: string[];
53
+ };
39
54
  export declare function runRunSheet(options: RunSheetOptions): Promise<RunSheetResult>;