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.
- package/README.md +78 -6
- package/dist/agents/implementation.d.ts +5 -0
- package/dist/agents/implementation.d.ts.map +1 -1
- package/dist/agents/implementation.js +78 -9
- package/dist/agents/implementation.js.map +1 -1
- package/dist/agents/planning.d.ts.map +1 -1
- package/dist/agents/planning.js +10 -3
- package/dist/agents/planning.js.map +1 -1
- package/dist/agents/research.d.ts.map +1 -1
- package/dist/agents/research.js +14 -6
- package/dist/agents/research.js.map +1 -1
- package/dist/agents/review.d.ts +81 -0
- package/dist/agents/review.d.ts.map +1 -1
- package/dist/agents/review.js +405 -39
- package/dist/agents/review.js.map +1 -1
- package/dist/agents/single-task.d.ts +1 -1
- package/dist/agents/single-task.d.ts.map +1 -1
- package/dist/agents/single-task.js +1 -1
- package/dist/cli/batch-processor.d.ts +64 -0
- package/dist/cli/batch-processor.d.ts.map +1 -0
- package/dist/cli/batch-processor.js +85 -0
- package/dist/cli/batch-processor.js.map +1 -0
- package/dist/cli/batch-validator.d.ts +80 -0
- package/dist/cli/batch-validator.d.ts.map +1 -0
- package/dist/cli/batch-validator.js +121 -0
- package/dist/cli/batch-validator.js.map +1 -0
- package/dist/cli/commands.d.ts +7 -0
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +285 -1
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/dependency-resolver.d.ts +49 -0
- package/dist/cli/dependency-resolver.d.ts.map +1 -0
- package/dist/cli/dependency-resolver.js +133 -0
- package/dist/cli/dependency-resolver.js.map +1 -0
- package/dist/cli/epic-processor.d.ts +16 -0
- package/dist/cli/epic-processor.d.ts.map +1 -0
- package/dist/cli/epic-processor.js +489 -0
- package/dist/cli/epic-processor.js.map +1 -0
- package/dist/cli/formatting.d.ts +15 -0
- package/dist/cli/formatting.d.ts.map +1 -1
- package/dist/cli/formatting.js +19 -0
- package/dist/cli/formatting.js.map +1 -1
- package/dist/cli/progress-dashboard.d.ts +58 -0
- package/dist/cli/progress-dashboard.d.ts.map +1 -0
- package/dist/cli/progress-dashboard.js +216 -0
- package/dist/cli/progress-dashboard.js.map +1 -0
- package/dist/cli/table-renderer.d.ts.map +1 -1
- package/dist/cli/table-renderer.js +5 -1
- package/dist/cli/table-renderer.js.map +1 -1
- package/dist/core/agent-executor.d.ts +13 -0
- package/dist/core/agent-executor.d.ts.map +1 -0
- package/dist/core/agent-executor.js +153 -0
- package/dist/core/agent-executor.js.map +1 -0
- package/dist/core/config.d.ts +16 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +113 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/git-utils.d.ts +19 -0
- package/dist/core/git-utils.d.ts.map +1 -1
- package/dist/core/git-utils.js +58 -0
- package/dist/core/git-utils.js.map +1 -1
- package/dist/core/kanban.d.ts +125 -1
- package/dist/core/kanban.d.ts.map +1 -1
- package/dist/core/kanban.js +371 -4
- package/dist/core/kanban.js.map +1 -1
- package/dist/core/orchestrator.d.ts +63 -0
- package/dist/core/orchestrator.d.ts.map +1 -0
- package/dist/core/orchestrator.js +320 -0
- package/dist/core/orchestrator.js.map +1 -0
- package/dist/core/story.d.ts +84 -0
- package/dist/core/story.d.ts.map +1 -1
- package/dist/core/story.js +159 -14
- package/dist/core/story.js.map +1 -1
- package/dist/core/worktree.d.ts +7 -0
- package/dist/core/worktree.d.ts.map +1 -1
- package/dist/core/worktree.js +44 -0
- package/dist/core/worktree.js.map +1 -1
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +252 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +23 -0
- package/dist/types/index.js.map +1 -1
- 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":"AAUA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAgB,EAAE,gBAA8B;IAClF,MAAM,MAAM,GAAc,EAAE,CAAC;IAC7B,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IACnC,MAAM,SAAS,GAAG,IAAI,GAAG,CAAS,gBAAgB,IAAI,EAAE,CAAC,CAAC;IAE1D,OAAO,SAAS,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;QAC1B,oDAAoD;QACpD,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;YACxD,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,CAAC,YAAY,IAAI,EAAE,CAAC;YAClD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/C,CAAC,CAAC,CAAC;QAEH,gEAAgE;QAChE,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,0BAA0B,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;YAChE,MAAM,IAAI,KAAK,CAAC,iCAAiC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACxE,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAE1B,0CAA0C;QAC1C,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;YAC3B,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACxB,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,0BAA0B,CAAC,OAAgB;IACzD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAiB,CAAC;IAC1C,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC;IAEpE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAC;IACzC,MAAM,IAAI,GAAa,EAAE,CAAC;IAE1B,SAAS,GAAG,CAAC,OAAe;QAC1B,IAAI,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAChC,8DAA8D;YAC9D,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACzC,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC;QAC9C,CAAC;QAED,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC,CAAC,gCAAgC;QAC/C,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACrB,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC5B,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAEnB,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACpC,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,CAAC,YAAY,IAAI,EAAE,CAAC;YAClD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;gBACvB,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;gBACvB,IAAI,KAAK,EAAE,CAAC;oBACV,OAAO,KAAK,CAAC;gBACf,CAAC;YACH,CAAC;QACH,CAAC;QAED,cAAc,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC/B,IAAI,CAAC,GAAG,EAAE,CAAC;QAEX,OAAO,IAAI,CAAC;IACd,CAAC;IAED,uCAAuC;IACvC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACxC,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAgB,EAAE,gBAA8B;IACnF,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC;IAC7D,MAAM,aAAa,GAAG,gBAAgB,IAAI,IAAI,GAAG,EAAU,CAAC;IAE5D,kFAAkF;IAClF,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,CAAC,YAAY,IAAI,EAAE,CAAC;QAClD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBAClD,MAAM,CAAC,IAAI,CACT,SAAS,KAAK,CAAC,WAAW,CAAC,EAAE,eAAe,GAAG,SAAS,GAAG,qBAAqB,CACjF,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,iEAAiE;IACjE,sFAAsF;IACtF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,IAAI,CAAC;YACH,mBAAmB,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC;QACjD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;gBAC3B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC;QAC1B,MAAM;KACP,CAAC;AACJ,CAAC"}
|
|
@@ -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
|