ai-sdlc 0.3.0 → 0.3.1-alpha.0-alpha.1

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 (84) hide show
  1. package/README.md +78 -6
  2. package/dist/agents/implementation.d.ts +5 -0
  3. package/dist/agents/implementation.d.ts.map +1 -1
  4. package/dist/agents/implementation.js +78 -9
  5. package/dist/agents/implementation.js.map +1 -1
  6. package/dist/agents/planning.d.ts.map +1 -1
  7. package/dist/agents/planning.js +10 -3
  8. package/dist/agents/planning.js.map +1 -1
  9. package/dist/agents/research.d.ts.map +1 -1
  10. package/dist/agents/research.js +14 -6
  11. package/dist/agents/research.js.map +1 -1
  12. package/dist/agents/review.d.ts +81 -0
  13. package/dist/agents/review.d.ts.map +1 -1
  14. package/dist/agents/review.js +405 -39
  15. package/dist/agents/review.js.map +1 -1
  16. package/dist/agents/single-task.d.ts +1 -1
  17. package/dist/agents/single-task.d.ts.map +1 -1
  18. package/dist/agents/single-task.js +1 -1
  19. package/dist/cli/batch-processor.d.ts +64 -0
  20. package/dist/cli/batch-processor.d.ts.map +1 -0
  21. package/dist/cli/batch-processor.js +85 -0
  22. package/dist/cli/batch-processor.js.map +1 -0
  23. package/dist/cli/batch-validator.d.ts +80 -0
  24. package/dist/cli/batch-validator.d.ts.map +1 -0
  25. package/dist/cli/batch-validator.js +121 -0
  26. package/dist/cli/batch-validator.js.map +1 -0
  27. package/dist/cli/commands.d.ts +7 -0
  28. package/dist/cli/commands.d.ts.map +1 -1
  29. package/dist/cli/commands.js +285 -1
  30. package/dist/cli/commands.js.map +1 -1
  31. package/dist/cli/dependency-resolver.d.ts +49 -0
  32. package/dist/cli/dependency-resolver.d.ts.map +1 -0
  33. package/dist/cli/dependency-resolver.js +133 -0
  34. package/dist/cli/dependency-resolver.js.map +1 -0
  35. package/dist/cli/epic-processor.d.ts +16 -0
  36. package/dist/cli/epic-processor.d.ts.map +1 -0
  37. package/dist/cli/epic-processor.js +489 -0
  38. package/dist/cli/epic-processor.js.map +1 -0
  39. package/dist/cli/formatting.d.ts +15 -0
  40. package/dist/cli/formatting.d.ts.map +1 -1
  41. package/dist/cli/formatting.js +19 -0
  42. package/dist/cli/formatting.js.map +1 -1
  43. package/dist/cli/progress-dashboard.d.ts +58 -0
  44. package/dist/cli/progress-dashboard.d.ts.map +1 -0
  45. package/dist/cli/progress-dashboard.js +216 -0
  46. package/dist/cli/progress-dashboard.js.map +1 -0
  47. package/dist/cli/table-renderer.d.ts.map +1 -1
  48. package/dist/cli/table-renderer.js +5 -1
  49. package/dist/cli/table-renderer.js.map +1 -1
  50. package/dist/core/agent-executor.d.ts +13 -0
  51. package/dist/core/agent-executor.d.ts.map +1 -0
  52. package/dist/core/agent-executor.js +153 -0
  53. package/dist/core/agent-executor.js.map +1 -0
  54. package/dist/core/config.d.ts +16 -1
  55. package/dist/core/config.d.ts.map +1 -1
  56. package/dist/core/config.js +113 -0
  57. package/dist/core/config.js.map +1 -1
  58. package/dist/core/git-utils.d.ts +19 -0
  59. package/dist/core/git-utils.d.ts.map +1 -1
  60. package/dist/core/git-utils.js +58 -0
  61. package/dist/core/git-utils.js.map +1 -1
  62. package/dist/core/kanban.d.ts +125 -1
  63. package/dist/core/kanban.d.ts.map +1 -1
  64. package/dist/core/kanban.js +371 -4
  65. package/dist/core/kanban.js.map +1 -1
  66. package/dist/core/orchestrator.d.ts +63 -0
  67. package/dist/core/orchestrator.d.ts.map +1 -0
  68. package/dist/core/orchestrator.js +320 -0
  69. package/dist/core/orchestrator.js.map +1 -0
  70. package/dist/core/story.d.ts +84 -0
  71. package/dist/core/story.d.ts.map +1 -1
  72. package/dist/core/story.js +159 -14
  73. package/dist/core/story.js.map +1 -1
  74. package/dist/core/worktree.d.ts +7 -0
  75. package/dist/core/worktree.d.ts.map +1 -1
  76. package/dist/core/worktree.js +44 -0
  77. package/dist/core/worktree.js.map +1 -1
  78. package/dist/index.js +53 -0
  79. package/dist/index.js.map +1 -1
  80. package/dist/types/index.d.ts +252 -0
  81. package/dist/types/index.d.ts.map +1 -1
  82. package/dist/types/index.js +23 -0
  83. package/dist/types/index.js.map +1 -1
  84. package/package.json +1 -1
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Group stories into parallel execution phases based on dependencies.
3
+ * Uses iterative topological sort to identify stories that can run concurrently.
4
+ *
5
+ * @param stories - Array of stories to group
6
+ * @param preSatisfiedDeps - Optional set of dependency IDs that are already satisfied (e.g., done stories)
7
+ * @returns Array of phases, where each phase contains stories that can run in parallel
8
+ * @throws Error if circular dependencies detected
9
+ *
10
+ * @example
11
+ * // Stories: A -> B, A -> C, B -> D, C -> D
12
+ * // Returns: [[A], [B, C], [D]]
13
+ */
14
+ export function groupStoriesByPhase(stories, preSatisfiedDeps) {
15
+ const phases = [];
16
+ const remaining = new Set(stories);
17
+ const completed = new Set(preSatisfiedDeps || []);
18
+ while (remaining.size > 0) {
19
+ // Find all stories whose dependencies are satisfied
20
+ const currentPhase = Array.from(remaining).filter(story => {
21
+ const deps = story.frontmatter.dependencies || [];
22
+ return deps.every(dep => completed.has(dep));
23
+ });
24
+ // If no stories can be processed, we have a circular dependency
25
+ if (currentPhase.length === 0) {
26
+ const cycle = detectCircularDependencies(Array.from(remaining));
27
+ throw new Error(`Circular dependency detected: ${cycle.join(' → ')}`);
28
+ }
29
+ phases.push(currentPhase);
30
+ // Mark current phase stories as completed
31
+ currentPhase.forEach(story => {
32
+ remaining.delete(story);
33
+ completed.add(story.frontmatter.id);
34
+ });
35
+ }
36
+ return phases;
37
+ }
38
+ /**
39
+ * Detect circular dependencies using depth-first search.
40
+ *
41
+ * @param stories - Array of stories to check
42
+ * @returns Array of story IDs forming the cycle, or empty array if no cycle
43
+ *
44
+ * @example
45
+ * // Stories: A -> B, B -> C, C -> A
46
+ * // Returns: ['S-001', 'S-002', 'S-003', 'S-001']
47
+ */
48
+ export function detectCircularDependencies(stories) {
49
+ const storyMap = new Map();
50
+ stories.forEach(story => storyMap.set(story.frontmatter.id, story));
51
+ const visited = new Set();
52
+ const recursionStack = new Set();
53
+ const path = [];
54
+ function dfs(storyId) {
55
+ if (recursionStack.has(storyId)) {
56
+ // Found a cycle - return the path from cycle start to current
57
+ const cycleStart = path.indexOf(storyId);
58
+ return [...path.slice(cycleStart), storyId];
59
+ }
60
+ if (visited.has(storyId)) {
61
+ return null; // Already processed this branch
62
+ }
63
+ visited.add(storyId);
64
+ recursionStack.add(storyId);
65
+ path.push(storyId);
66
+ const story = storyMap.get(storyId);
67
+ if (story) {
68
+ const deps = story.frontmatter.dependencies || [];
69
+ for (const dep of deps) {
70
+ const cycle = dfs(dep);
71
+ if (cycle) {
72
+ return cycle;
73
+ }
74
+ }
75
+ }
76
+ recursionStack.delete(storyId);
77
+ path.pop();
78
+ return null;
79
+ }
80
+ // Check all stories as starting points
81
+ for (const story of stories) {
82
+ const cycle = dfs(story.frontmatter.id);
83
+ if (cycle) {
84
+ return cycle;
85
+ }
86
+ }
87
+ return [];
88
+ }
89
+ /**
90
+ * Validate that all story dependencies are satisfied.
91
+ * Checks for missing dependencies and circular references.
92
+ *
93
+ * @param stories - Array of stories to validate
94
+ * @param preSatisfiedDeps - Optional set of dependency IDs that are already satisfied (e.g., done stories)
95
+ * @returns Validation result with errors if any
96
+ *
97
+ * @example
98
+ * const result = validateDependencies(stories);
99
+ * if (!result.valid) {
100
+ * console.error(result.errors);
101
+ * }
102
+ */
103
+ export function validateDependencies(stories, preSatisfiedDeps) {
104
+ const errors = [];
105
+ const storyIds = new Set(stories.map(s => s.frontmatter.id));
106
+ const satisfiedDeps = preSatisfiedDeps || new Set();
107
+ // Check for missing dependencies (excluding pre-satisfied ones like done stories)
108
+ for (const story of stories) {
109
+ const deps = story.frontmatter.dependencies || [];
110
+ for (const dep of deps) {
111
+ if (!storyIds.has(dep) && !satisfiedDeps.has(dep)) {
112
+ errors.push(`Story ${story.frontmatter.id} depends on ${dep}, but ${dep} is not in the epic`);
113
+ }
114
+ }
115
+ }
116
+ // Only check for circular dependencies if all dependencies exist
117
+ // (missing dependencies will cause groupStoriesByPhase to fail with misleading error)
118
+ if (errors.length === 0) {
119
+ try {
120
+ groupStoriesByPhase(stories, preSatisfiedDeps);
121
+ }
122
+ catch (error) {
123
+ if (error instanceof Error) {
124
+ errors.push(error.message);
125
+ }
126
+ }
127
+ }
128
+ return {
129
+ valid: errors.length === 0,
130
+ errors,
131
+ };
132
+ }
133
+ //# sourceMappingURL=dependency-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dependency-resolver.js","sourceRoot":"","sources":["../../src/cli/dependency-resolver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { Story, EpicProcessingOptions } from '../types/index.js';
2
+ /**
3
+ * Normalize epic ID by stripping 'epic-' prefix if present
4
+ * Both 'epic-foo' and 'foo' become 'foo'
5
+ */
6
+ export declare function normalizeEpicId(epicId: string): string;
7
+ /**
8
+ * Discover stories for an epic with normalized ID
9
+ * Filters out stories that are already done
10
+ */
11
+ export declare function discoverEpicStories(sdlcRoot: string, epicId: string): Story[];
12
+ /**
13
+ * Main epic processing function
14
+ */
15
+ export declare function processEpic(options: EpicProcessingOptions): Promise<number>;
16
+ //# sourceMappingURL=epic-processor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"epic-processor.d.ts","sourceRoot":"","sources":["../../src/cli/epic-processor.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,EAAE,qBAAqB,EAA6C,MAAM,mBAAmB,CAAC;AAoB5G;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,CAc7E;AAgXD;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CA0MjF"}
@@ -0,0 +1,489 @@
1
+ import path from 'path';
2
+ import { spawn } from 'child_process';
3
+ import { getSdlcRoot, loadConfig, validateWorktreeBasePath, DEFAULT_MERGE_CONFIG } from '../core/config.js';
4
+ import { findStoriesByEpic } from '../core/kanban.js';
5
+ import { GitWorktreeService } from '../core/worktree.js';
6
+ import { getThemedChalk } from '../core/theme.js';
7
+ import { groupStoriesByPhase, validateDependencies } from './dependency-resolver.js';
8
+ import { createDashboard, updateStoryStatus, markStorySkipped, markStoryFailed, advancePhase, startDashboardRenderer, } from './progress-dashboard.js';
9
+ import { StoryLogger } from '../core/story-logger.js';
10
+ import { parseStory, isPRMerged, markPRMerged } from '../core/story.js';
11
+ import { waitForChecks, mergePullRequest } from '../agents/review.js';
12
+ import fs from 'fs';
13
+ /**
14
+ * Normalize epic ID by stripping 'epic-' prefix if present
15
+ * Both 'epic-foo' and 'foo' become 'foo'
16
+ */
17
+ export function normalizeEpicId(epicId) {
18
+ return epicId.startsWith('epic-') ? epicId.slice(5) : epicId;
19
+ }
20
+ /**
21
+ * Discover stories for an epic with normalized ID
22
+ * Filters out stories that are already done
23
+ */
24
+ export function discoverEpicStories(sdlcRoot, epicId) {
25
+ const normalized = normalizeEpicId(epicId);
26
+ const stories = findStoriesByEpic(sdlcRoot, normalized);
27
+ // Filter out stories that are already done
28
+ const activeStories = stories.filter(story => story.frontmatter.status !== 'done');
29
+ // Sort by priority (ascending) then created date (ascending)
30
+ return activeStories.sort((a, b) => {
31
+ if (a.frontmatter.priority !== b.frontmatter.priority) {
32
+ return a.frontmatter.priority - b.frontmatter.priority;
33
+ }
34
+ return a.frontmatter.created.localeCompare(b.frontmatter.created);
35
+ });
36
+ }
37
+ /**
38
+ * Format execution plan for display
39
+ */
40
+ function formatExecutionPlan(epicId, phases) {
41
+ const lines = [];
42
+ const totalStories = phases.reduce((sum, phase) => sum + phase.length, 0);
43
+ lines.push(`\nFound ${totalStories} stories for epic: ${epicId}\n`);
44
+ phases.forEach((phase, index) => {
45
+ const phaseNum = index + 1;
46
+ const storyCount = phase.length;
47
+ const parallelNote = storyCount > 1 ? ', parallel' : '';
48
+ lines.push(`Phase ${phaseNum} (${storyCount} ${storyCount === 1 ? 'story' : 'stories'}${parallelNote}):`);
49
+ phase.forEach(story => {
50
+ const deps = story.frontmatter.dependencies || [];
51
+ const depsStr = deps.length > 0 ? ` (depends: ${deps.join(', ')})` : '';
52
+ lines.push(` • ${story.frontmatter.id}: ${story.frontmatter.title}${depsStr}`);
53
+ });
54
+ lines.push('');
55
+ });
56
+ // Estimate time (rough: 15-30 min per story, parallelized)
57
+ const estimatedMinutes = phases.length * 15; // Very rough estimate
58
+ lines.push(`Estimated time: ${estimatedMinutes}-${estimatedMinutes * 2} minutes`);
59
+ return lines.join('\n');
60
+ }
61
+ /**
62
+ * Merge a story's PR after successful completion
63
+ * Waits for CI checks to pass before merging
64
+ */
65
+ async function mergeStoryPR(storyId, worktreePath, config, logger) {
66
+ const mergeConfig = config.merge ?? DEFAULT_MERGE_CONFIG;
67
+ if (!mergeConfig.enabled) {
68
+ logger.log('DEBUG', 'PR merge is disabled in config');
69
+ return { success: true };
70
+ }
71
+ // Load the story from the worktree to get pr_url
72
+ const sdlcRoot = path.join(worktreePath, config.sdlcFolder);
73
+ const storyPath = path.join(sdlcRoot, 'stories', storyId, 'story.md');
74
+ if (!fs.existsSync(storyPath)) {
75
+ logger.log('WARN', `Story file not found in worktree: ${storyPath}`);
76
+ return { success: true }; // Not a failure - story might not have a PR
77
+ }
78
+ let story;
79
+ try {
80
+ story = parseStory(storyPath);
81
+ }
82
+ catch (error) {
83
+ logger.log('ERROR', `Failed to parse story: ${error}`);
84
+ return { success: false, error: `Failed to parse story: ${error}` };
85
+ }
86
+ // Check if story has a PR URL
87
+ const prUrl = story.frontmatter.pr_url;
88
+ if (!prUrl) {
89
+ logger.log('DEBUG', 'Story has no PR URL, skipping merge');
90
+ return { success: true };
91
+ }
92
+ // Check if already merged
93
+ if (isPRMerged(story)) {
94
+ logger.log('DEBUG', 'PR already merged');
95
+ return { success: true };
96
+ }
97
+ logger.log('INFO', `Waiting for CI checks on PR: ${prUrl}`);
98
+ // Wait for CI checks to pass
99
+ const checksResult = await waitForChecks(prUrl, worktreePath, {
100
+ timeout: mergeConfig.checksTimeout,
101
+ pollingInterval: mergeConfig.checksPollingInterval,
102
+ requireAllChecksPassing: mergeConfig.requireAllChecksPassing,
103
+ });
104
+ if (!checksResult.allPassed) {
105
+ if (checksResult.timedOut) {
106
+ logger.log('WARN', `CI checks timed out after ${mergeConfig.checksTimeout}ms`);
107
+ return { success: false, error: 'CI checks timed out - manual merge required' };
108
+ }
109
+ logger.log('ERROR', `CI checks failed: ${checksResult.error}`);
110
+ return { success: false, error: checksResult.error || 'CI checks failed' };
111
+ }
112
+ logger.log('INFO', 'CI checks passed, merging PR');
113
+ // Merge the PR
114
+ const mergeResult = await mergePullRequest(prUrl, worktreePath, {
115
+ strategy: mergeConfig.strategy,
116
+ deleteBranchAfterMerge: mergeConfig.deleteBranchAfterMerge,
117
+ });
118
+ if (!mergeResult.success) {
119
+ logger.log('ERROR', `PR merge failed: ${mergeResult.error}`);
120
+ return { success: false, error: mergeResult.error || 'PR merge failed' };
121
+ }
122
+ logger.log('INFO', `PR merged successfully${mergeResult.mergeSha ? ` (SHA: ${mergeResult.mergeSha})` : ''}`);
123
+ // Update story with merge metadata
124
+ try {
125
+ await markPRMerged(story, mergeResult.mergeSha);
126
+ logger.log('DEBUG', 'Updated story with merge metadata');
127
+ }
128
+ catch (error) {
129
+ // Log but don't fail - the merge succeeded
130
+ logger.log('WARN', `Failed to update story with merge metadata: ${error}`);
131
+ }
132
+ return { success: true };
133
+ }
134
+ /**
135
+ * Process a single story in an isolated worktree
136
+ */
137
+ async function processStoryInWorktree(story, dashboard, worktreeService, keepWorktrees, config) {
138
+ const storyId = story.frontmatter.id;
139
+ const slug = story.frontmatter.slug;
140
+ updateStoryStatus(dashboard, storyId, 'in-progress');
141
+ try {
142
+ // Create worktree
143
+ const worktreePath = worktreeService.create({
144
+ storyId,
145
+ slug,
146
+ });
147
+ // Verify worktree was created
148
+ if (!fs.existsSync(worktreePath)) {
149
+ const error = `Worktree creation failed: ${worktreePath} does not exist`;
150
+ markStoryFailed(dashboard, storyId, error);
151
+ return { success: false, error };
152
+ }
153
+ // Log to story-specific epic run log
154
+ const sdlcRoot = getSdlcRoot();
155
+ const logger = new StoryLogger(storyId, sdlcRoot);
156
+ logger.log('INFO', `Starting story execution in worktree: ${worktreePath}`);
157
+ // Spawn ai-sdlc run process in worktree
158
+ // Use the same invocation method as the current process (handles global install, npx, or dev mode)
159
+ // Use --no-worktree since we're already in an isolated worktree
160
+ const result = await new Promise((resolve) => {
161
+ const proc = spawn(process.execPath, [process.argv[1], 'run', '--story', storyId, '--auto', '--no-worktree'], {
162
+ cwd: worktreePath,
163
+ stdio: ['ignore', 'pipe', 'pipe'],
164
+ shell: false,
165
+ });
166
+ let stdout = '';
167
+ let stderr = '';
168
+ proc.stdout?.on('data', (data) => {
169
+ stdout += data.toString();
170
+ });
171
+ proc.stderr?.on('data', (data) => {
172
+ stderr += data.toString();
173
+ });
174
+ proc.on('close', (code) => {
175
+ // Log subprocess output for debugging
176
+ if (stdout.trim()) {
177
+ logger.log('DEBUG', `Subprocess stdout:\n${stdout}`);
178
+ }
179
+ if (stderr.trim()) {
180
+ logger.log('DEBUG', `Subprocess stderr:\n${stderr}`);
181
+ }
182
+ if (code === 0) {
183
+ logger.log('INFO', 'Story execution completed successfully');
184
+ resolve({ success: true });
185
+ }
186
+ else {
187
+ const error = `Process exited with code ${code}`;
188
+ logger.log('ERROR', `Story execution failed: ${error}\n${stderr}`);
189
+ resolve({ success: false, error });
190
+ }
191
+ });
192
+ proc.on('error', (err) => {
193
+ logger.log('ERROR', `Failed to spawn process: ${err.message}`);
194
+ resolve({ success: false, error: err.message });
195
+ });
196
+ });
197
+ // If execution succeeded, attempt PR merge (if enabled)
198
+ if (result.success && config.merge?.enabled) {
199
+ const mergeResult = await mergeStoryPR(storyId, worktreePath, config, logger);
200
+ if (!mergeResult.success) {
201
+ // Merge failure is a story failure
202
+ result.success = false;
203
+ result.error = mergeResult.error || 'PR merge failed';
204
+ logger.log('ERROR', `Story failed due to merge failure: ${result.error}`);
205
+ }
206
+ }
207
+ // Cleanup worktree if not keeping
208
+ if (!keepWorktrees) {
209
+ try {
210
+ // Use force cleanup after successful merge since branch may be deleted
211
+ const forceCleanup = result.success && config.merge?.enabled && config.merge?.deleteBranchAfterMerge;
212
+ worktreeService.remove(worktreePath, forceCleanup);
213
+ }
214
+ catch (cleanupError) {
215
+ // Log but don't fail the story
216
+ logger.log('WARN', `Failed to cleanup worktree: ${cleanupError}`);
217
+ }
218
+ }
219
+ if (result.success) {
220
+ updateStoryStatus(dashboard, storyId, 'completed');
221
+ }
222
+ else {
223
+ markStoryFailed(dashboard, storyId, result.error || 'Unknown error');
224
+ }
225
+ return result;
226
+ }
227
+ catch (error) {
228
+ const errorMsg = error instanceof Error ? error.message : String(error);
229
+ markStoryFailed(dashboard, storyId, errorMsg);
230
+ return { success: false, error: errorMsg };
231
+ }
232
+ }
233
+ /**
234
+ * Execute a single phase with concurrency limit
235
+ */
236
+ async function executePhase(phase, phaseNumber, maxConcurrent, dashboard, worktreeService, keepWorktrees, config) {
237
+ const result = {
238
+ phase: phaseNumber,
239
+ succeeded: [],
240
+ failed: [],
241
+ skipped: [],
242
+ };
243
+ const queue = [...phase];
244
+ const active = new Set();
245
+ while (queue.length > 0 || active.size > 0) {
246
+ // Fill up to maxConcurrent
247
+ while (active.size < maxConcurrent && queue.length > 0) {
248
+ const story = queue.shift();
249
+ const promise = processStoryInWorktree(story, dashboard, worktreeService, keepWorktrees, config)
250
+ .then(result => ({
251
+ storyId: story.frontmatter.id,
252
+ ...result,
253
+ }));
254
+ active.add(promise);
255
+ // Remove from active when done
256
+ promise.finally(() => active.delete(promise));
257
+ }
258
+ if (active.size > 0) {
259
+ // Wait for at least one to complete
260
+ const completed = await Promise.race(active);
261
+ if (completed.success) {
262
+ result.succeeded.push(completed.storyId);
263
+ }
264
+ else {
265
+ result.failed.push(completed.storyId);
266
+ }
267
+ }
268
+ }
269
+ return result;
270
+ }
271
+ /**
272
+ * Generate final epic summary
273
+ */
274
+ function generateEpicSummary(epicId, phases, phaseResults, failedStories, skippedStories, startTime) {
275
+ const totalStories = phases.reduce((sum, phase) => sum + phase.length, 0);
276
+ const completed = phaseResults.reduce((sum, r) => sum + r.succeeded.length, 0);
277
+ const failed = Array.from(failedStories.keys()).length;
278
+ const skipped = Array.from(skippedStories.keys()).length;
279
+ return {
280
+ epicId,
281
+ totalStories,
282
+ completed,
283
+ failed,
284
+ skipped,
285
+ duration: Date.now() - startTime,
286
+ failedStories: Array.from(failedStories.entries()).map(([storyId, error]) => ({
287
+ storyId,
288
+ error,
289
+ })),
290
+ skippedStories: Array.from(skippedStories.entries()).map(([storyId, reason]) => ({
291
+ storyId,
292
+ reason,
293
+ })),
294
+ };
295
+ }
296
+ /**
297
+ * Print epic summary to console
298
+ */
299
+ function printEpicSummary(summary, chalk) {
300
+ console.log('\n' + chalk.bold('═══ Epic Summary ═══'));
301
+ console.log(`\nEpic: ${chalk.bold(summary.epicId)}`);
302
+ console.log('');
303
+ if (summary.completed > 0) {
304
+ console.log(chalk.success(`✓ Completed: ${summary.completed} ${summary.completed === 1 ? 'story' : 'stories'}`));
305
+ }
306
+ if (summary.failed > 0) {
307
+ console.log(chalk.error(`✗ Failed: ${summary.failed} ${summary.failed === 1 ? 'story' : 'stories'}`));
308
+ summary.failedStories.forEach(({ storyId, error }) => {
309
+ console.log(chalk.error(` • ${storyId}: ${error}`));
310
+ });
311
+ }
312
+ if (summary.skipped > 0) {
313
+ console.log(chalk.warning(`⊘ Skipped: ${summary.skipped} ${summary.skipped === 1 ? 'story' : 'stories'} (dependencies failed)`));
314
+ summary.skippedStories.forEach(({ storyId, reason }) => {
315
+ console.log(chalk.dim(` • ${storyId}: ${reason}`));
316
+ });
317
+ }
318
+ const durationSeconds = Math.floor(summary.duration / 1000);
319
+ const minutes = Math.floor(durationSeconds / 60);
320
+ const seconds = durationSeconds % 60;
321
+ console.log('');
322
+ console.log(`Duration: ${minutes}m ${seconds}s`);
323
+ console.log('');
324
+ }
325
+ /**
326
+ * Main epic processing function
327
+ */
328
+ export async function processEpic(options) {
329
+ const config = loadConfig();
330
+ const c = getThemedChalk(config);
331
+ const sdlcRoot = getSdlcRoot();
332
+ const projectRoot = process.cwd();
333
+ // Validate worktrees enabled
334
+ if (!config.worktree?.enabled) {
335
+ console.log(c.error('Error: Epic processing requires worktrees to be enabled'));
336
+ console.log(c.dim('Set "worktree.enabled": true in .ai-sdlc.json'));
337
+ return 1;
338
+ }
339
+ // Get effective configuration
340
+ const maxConcurrent = options.maxConcurrent ?? config.epic?.maxConcurrent ?? 3;
341
+ const keepWorktrees = options.keepWorktrees ?? config.epic?.keepWorktrees ?? false;
342
+ const continueOnFailure = config.epic?.continueOnFailure ?? true;
343
+ // Apply CLI overrides for merge config
344
+ if (options.merge !== undefined) {
345
+ config.merge = config.merge ?? { ...DEFAULT_MERGE_CONFIG };
346
+ config.merge.enabled = options.merge;
347
+ }
348
+ if (options.mergeStrategy !== undefined) {
349
+ config.merge = config.merge ?? { ...DEFAULT_MERGE_CONFIG };
350
+ config.merge.strategy = options.mergeStrategy;
351
+ }
352
+ // Validate maxConcurrent
353
+ if (maxConcurrent < 1) {
354
+ console.log(c.error('Error: --max-concurrent must be >= 1'));
355
+ return 1;
356
+ }
357
+ // Discover stories (active only - done stories are filtered out)
358
+ console.log(c.info(`Discovering stories for epic: ${options.epicId}`));
359
+ const stories = discoverEpicStories(sdlcRoot, options.epicId);
360
+ // Also get done story IDs to treat as pre-satisfied dependencies
361
+ const normalized = normalizeEpicId(options.epicId);
362
+ const allEpicStories = findStoriesByEpic(sdlcRoot, normalized);
363
+ const doneStoryIds = new Set(allEpicStories
364
+ .filter(s => s.frontmatter.status === 'done')
365
+ .map(s => s.frontmatter.id));
366
+ if (stories.length === 0) {
367
+ if (doneStoryIds.size > 0) {
368
+ console.log(c.success(`All ${doneStoryIds.size} stories in epic are already done!`));
369
+ }
370
+ else {
371
+ console.log(c.warning(`No stories found for epic: ${options.epicId}`));
372
+ }
373
+ return 0; // Not an error
374
+ }
375
+ // Validate dependencies (done stories count as satisfied)
376
+ const validation = validateDependencies(stories, doneStoryIds);
377
+ if (!validation.valid) {
378
+ console.log(c.error('Error: Invalid dependencies detected:'));
379
+ validation.errors.forEach(error => console.log(c.error(` • ${error}`)));
380
+ return 1;
381
+ }
382
+ // Group into phases (done stories count as already completed)
383
+ const phases = groupStoriesByPhase(stories, doneStoryIds);
384
+ // Display execution plan
385
+ console.log(formatExecutionPlan(options.epicId, phases));
386
+ // Dry run - stop here
387
+ if (options.dryRun) {
388
+ console.log(c.info('\nDry run complete - no stories executed'));
389
+ return 0;
390
+ }
391
+ // Confirm execution (unless --force)
392
+ if (!options.force) {
393
+ console.log(c.warning('\nContinue? [Y/n] '));
394
+ // For now, assume yes in automated context
395
+ // TODO: Add actual prompt in interactive mode
396
+ }
397
+ // Initialize worktree service with resolved basePath
398
+ let resolvedBasePath;
399
+ try {
400
+ resolvedBasePath = validateWorktreeBasePath(config.worktree.basePath, projectRoot);
401
+ }
402
+ catch (error) {
403
+ console.log(c.error(`Configuration Error: ${error instanceof Error ? error.message : String(error)}`));
404
+ return 1;
405
+ }
406
+ const worktreeService = new GitWorktreeService(projectRoot, resolvedBasePath);
407
+ // Create dashboard
408
+ const dashboard = createDashboard(options.epicId, phases);
409
+ const stopRenderer = startDashboardRenderer(dashboard);
410
+ const startTime = Date.now();
411
+ const failedStories = new Map();
412
+ const skippedStories = new Map();
413
+ const phaseResults = [];
414
+ try {
415
+ // Execute phases sequentially
416
+ for (let i = 0; i < phases.length; i++) {
417
+ const phase = phases[i];
418
+ // Check if any stories in this phase should be skipped due to failed or unmerged dependencies
419
+ const phaseToExecute = phase.filter(story => {
420
+ const deps = story.frontmatter.dependencies || [];
421
+ // Check for failed dependencies
422
+ const hasFailedDep = deps.some(dep => failedStories.has(dep));
423
+ if (hasFailedDep) {
424
+ const failedDep = deps.find(dep => failedStories.has(dep));
425
+ const reason = `Dependency failed: ${failedDep}`;
426
+ markStorySkipped(dashboard, story.frontmatter.id, reason);
427
+ skippedStories.set(story.frontmatter.id, reason);
428
+ return false;
429
+ }
430
+ // If merge is enabled, check for unmerged dependencies
431
+ // A dependency is unmerged if it has a pr_url but pr_merged !== true
432
+ if (config.merge?.enabled) {
433
+ for (const depId of deps) {
434
+ // Skip done stories (already validated) and failed stories (handled above)
435
+ if (doneStoryIds.has(depId) || failedStories.has(depId))
436
+ continue;
437
+ // Check if this dependency story has been completed in this run
438
+ const depCompleted = phaseResults.some(pr => pr.succeeded.includes(depId));
439
+ if (!depCompleted)
440
+ continue;
441
+ // Try to load the dependency story to check merge status
442
+ try {
443
+ const depStoryPath = path.join(sdlcRoot, 'stories', depId, 'story.md');
444
+ if (fs.existsSync(depStoryPath)) {
445
+ const depStory = parseStory(depStoryPath);
446
+ if (depStory.frontmatter.pr_url && !isPRMerged(depStory)) {
447
+ const reason = `Waiting for dependency merge: ${depId}`;
448
+ markStorySkipped(dashboard, story.frontmatter.id, reason);
449
+ skippedStories.set(story.frontmatter.id, reason);
450
+ return false;
451
+ }
452
+ }
453
+ }
454
+ catch {
455
+ // If we can't read the story, skip this check
456
+ }
457
+ }
458
+ }
459
+ return true;
460
+ });
461
+ // Skip phase if all stories are blocked
462
+ if (phaseToExecute.length === 0) {
463
+ continue;
464
+ }
465
+ // Execute phase
466
+ const result = await executePhase(phaseToExecute, i + 1, maxConcurrent, dashboard, worktreeService, keepWorktrees, config);
467
+ phaseResults.push(result);
468
+ // Track failed stories
469
+ result.failed.forEach(storyId => {
470
+ failedStories.set(storyId, 'Execution failed');
471
+ });
472
+ // Stop on failure if not continuing
473
+ if (!continueOnFailure && result.failed.length > 0) {
474
+ console.log(c.error('\nStopping due to failure (continueOnFailure = false)'));
475
+ break;
476
+ }
477
+ advancePhase(dashboard);
478
+ }
479
+ }
480
+ finally {
481
+ stopRenderer();
482
+ }
483
+ // Generate and print summary
484
+ const summary = generateEpicSummary(options.epicId, phases, phaseResults, failedStories, skippedStories, startTime);
485
+ printEpicSummary(summary, c);
486
+ // Return exit code
487
+ return summary.failed > 0 ? 1 : 0;
488
+ }
489
+ //# sourceMappingURL=epic-processor.js.map