@hyperdrive.bot/bmad-workflow 1.0.2
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/LICENSE +21 -0
- package/README.md +1017 -0
- package/bin/dev +5 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/config/show.d.ts +34 -0
- package/dist/commands/config/show.js +108 -0
- package/dist/commands/config/validate.d.ts +29 -0
- package/dist/commands/config/validate.js +131 -0
- package/dist/commands/decompose.d.ts +79 -0
- package/dist/commands/decompose.js +327 -0
- package/dist/commands/demo.d.ts +18 -0
- package/dist/commands/demo.js +107 -0
- package/dist/commands/epics/create.d.ts +123 -0
- package/dist/commands/epics/create.js +459 -0
- package/dist/commands/epics/list.d.ts +120 -0
- package/dist/commands/epics/list.js +280 -0
- package/dist/commands/hello/index.d.ts +12 -0
- package/dist/commands/hello/index.js +34 -0
- package/dist/commands/hello/world.d.ts +8 -0
- package/dist/commands/hello/world.js +24 -0
- package/dist/commands/prd/fix.d.ts +39 -0
- package/dist/commands/prd/fix.js +140 -0
- package/dist/commands/prd/validate.d.ts +112 -0
- package/dist/commands/prd/validate.js +302 -0
- package/dist/commands/stories/create.d.ts +95 -0
- package/dist/commands/stories/create.js +431 -0
- package/dist/commands/stories/develop.d.ts +91 -0
- package/dist/commands/stories/develop.js +460 -0
- package/dist/commands/stories/list.d.ts +84 -0
- package/dist/commands/stories/list.js +291 -0
- package/dist/commands/stories/move.d.ts +66 -0
- package/dist/commands/stories/move.js +273 -0
- package/dist/commands/stories/qa.d.ts +99 -0
- package/dist/commands/stories/qa.js +530 -0
- package/dist/commands/workflow.d.ts +97 -0
- package/dist/commands/workflow.js +390 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models/agent-options.d.ts +50 -0
- package/dist/models/agent-options.js +1 -0
- package/dist/models/agent-result.d.ts +29 -0
- package/dist/models/agent-result.js +1 -0
- package/dist/models/index.d.ts +10 -0
- package/dist/models/index.js +10 -0
- package/dist/models/phase-result.d.ts +65 -0
- package/dist/models/phase-result.js +7 -0
- package/dist/models/provider.d.ts +28 -0
- package/dist/models/provider.js +18 -0
- package/dist/models/story.d.ts +154 -0
- package/dist/models/story.js +18 -0
- package/dist/models/workflow-config.d.ts +148 -0
- package/dist/models/workflow-config.js +1 -0
- package/dist/models/workflow-result.d.ts +164 -0
- package/dist/models/workflow-result.js +7 -0
- package/dist/services/agents/agent-runner-factory.d.ts +31 -0
- package/dist/services/agents/agent-runner-factory.js +44 -0
- package/dist/services/agents/agent-runner.d.ts +46 -0
- package/dist/services/agents/agent-runner.js +29 -0
- package/dist/services/agents/claude-agent-runner.d.ts +81 -0
- package/dist/services/agents/claude-agent-runner.js +332 -0
- package/dist/services/agents/gemini-agent-runner.d.ts +82 -0
- package/dist/services/agents/gemini-agent-runner.js +350 -0
- package/dist/services/agents/index.d.ts +7 -0
- package/dist/services/agents/index.js +7 -0
- package/dist/services/file-system/file-manager.d.ts +110 -0
- package/dist/services/file-system/file-manager.js +223 -0
- package/dist/services/file-system/glob-matcher.d.ts +75 -0
- package/dist/services/file-system/glob-matcher.js +126 -0
- package/dist/services/file-system/path-resolver.d.ts +183 -0
- package/dist/services/file-system/path-resolver.js +400 -0
- package/dist/services/logging/workflow-logger.d.ts +232 -0
- package/dist/services/logging/workflow-logger.js +552 -0
- package/dist/services/orchestration/batch-processor.d.ts +113 -0
- package/dist/services/orchestration/batch-processor.js +187 -0
- package/dist/services/orchestration/dependency-graph-executor.d.ts +60 -0
- package/dist/services/orchestration/dependency-graph-executor.js +447 -0
- package/dist/services/orchestration/index.d.ts +10 -0
- package/dist/services/orchestration/index.js +8 -0
- package/dist/services/orchestration/input-detector.d.ts +125 -0
- package/dist/services/orchestration/input-detector.js +381 -0
- package/dist/services/orchestration/story-queue.d.ts +94 -0
- package/dist/services/orchestration/story-queue.js +170 -0
- package/dist/services/orchestration/story-type-detector.d.ts +80 -0
- package/dist/services/orchestration/story-type-detector.js +258 -0
- package/dist/services/orchestration/task-decomposition-service.d.ts +67 -0
- package/dist/services/orchestration/task-decomposition-service.js +607 -0
- package/dist/services/orchestration/workflow-orchestrator.d.ts +659 -0
- package/dist/services/orchestration/workflow-orchestrator.js +2201 -0
- package/dist/services/parsers/epic-parser.d.ts +117 -0
- package/dist/services/parsers/epic-parser.js +264 -0
- package/dist/services/parsers/prd-fixer.d.ts +86 -0
- package/dist/services/parsers/prd-fixer.js +194 -0
- package/dist/services/parsers/prd-parser.d.ts +123 -0
- package/dist/services/parsers/prd-parser.js +286 -0
- package/dist/services/parsers/standalone-story-parser.d.ts +114 -0
- package/dist/services/parsers/standalone-story-parser.js +255 -0
- package/dist/services/parsers/story-parser-factory.d.ts +81 -0
- package/dist/services/parsers/story-parser-factory.js +108 -0
- package/dist/services/parsers/story-parser.d.ts +122 -0
- package/dist/services/parsers/story-parser.js +262 -0
- package/dist/services/scaffolding/decompose-session-scaffolder.d.ts +74 -0
- package/dist/services/scaffolding/decompose-session-scaffolder.js +315 -0
- package/dist/services/scaffolding/file-scaffolder.d.ts +94 -0
- package/dist/services/scaffolding/file-scaffolder.js +314 -0
- package/dist/services/validation/config-validator.d.ts +88 -0
- package/dist/services/validation/config-validator.js +167 -0
- package/dist/types/task-graph.d.ts +142 -0
- package/dist/types/task-graph.js +5 -0
- package/dist/utils/colors.d.ts +49 -0
- package/dist/utils/colors.js +50 -0
- package/dist/utils/error-formatter.d.ts +64 -0
- package/dist/utils/error-formatter.js +279 -0
- package/dist/utils/errors.d.ts +170 -0
- package/dist/utils/errors.js +233 -0
- package/dist/utils/formatters.d.ts +84 -0
- package/dist/utils/formatters.js +162 -0
- package/dist/utils/logger.d.ts +63 -0
- package/dist/utils/logger.js +78 -0
- package/dist/utils/progress.d.ts +104 -0
- package/dist/utils/progress.js +161 -0
- package/dist/utils/retry.d.ts +114 -0
- package/dist/utils/retry.js +160 -0
- package/dist/utils/shared-flags.d.ts +28 -0
- package/dist/utils/shared-flags.js +43 -0
- package/package.json +119 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Epics Create Command
|
|
3
|
+
*
|
|
4
|
+
* Creates epic files from a PRD document using Claude AI agents.
|
|
5
|
+
* Supports batch processing with progress indicators and idempotent behavior.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```bash
|
|
9
|
+
* bmad-workflow epics create docs/prd.md
|
|
10
|
+
* bmad-workflow epics create docs/prd.md --start 2 --count 3
|
|
11
|
+
* bmad-workflow epics create docs/prd.md --reference docs/architecture.md
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { createAgentRunner } from '../../services/agents/agent-runner-factory.js';
|
|
17
|
+
import { FileManager } from '../../services/file-system/file-manager.js';
|
|
18
|
+
import { PathResolver } from '../../services/file-system/path-resolver.js';
|
|
19
|
+
import { PrdParser } from '../../services/parsers/prd-parser.js';
|
|
20
|
+
import { FileScaffolder } from '../../services/scaffolding/file-scaffolder.js';
|
|
21
|
+
import * as colors from '../../utils/colors.js';
|
|
22
|
+
import { ValidationError } from '../../utils/errors.js';
|
|
23
|
+
import { createLogger, generateCorrelationId } from '../../utils/logger.js';
|
|
24
|
+
import { createSpinner } from '../../utils/progress.js';
|
|
25
|
+
import { agentFlags } from '../../utils/shared-flags.js';
|
|
26
|
+
/**
|
|
27
|
+
* Epics Create Command
|
|
28
|
+
*
|
|
29
|
+
* Creates epic markdown files from a PRD document by:
|
|
30
|
+
* 1. Parsing epics from PRD
|
|
31
|
+
* 2. Filtering based on flags (start, count)
|
|
32
|
+
* 3. Skipping existing epic files (idempotent)
|
|
33
|
+
* 4. Creating placeholder files
|
|
34
|
+
* 5. Running Claude AI agents to populate content
|
|
35
|
+
* 6. Displaying progress and summary
|
|
36
|
+
*/
|
|
37
|
+
export default class EpicsCreate extends Command {
|
|
38
|
+
static args = {
|
|
39
|
+
'prd-path': Args.string({
|
|
40
|
+
description: 'Path to PRD markdown file',
|
|
41
|
+
required: true,
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
static description = 'Create epic files from PRD document';
|
|
45
|
+
static examples = [
|
|
46
|
+
'<%= config.bin %> <%= command.id %> docs/prd.md',
|
|
47
|
+
'<%= config.bin %> <%= command.id %> docs/prd.md --start 2 --count 3',
|
|
48
|
+
'<%= config.bin %> <%= command.id %> docs/prd.md --reference docs/architecture.md --interval 5',
|
|
49
|
+
'<%= config.bin %> <%= command.id %> docs/prd.md --prefix "PROJECT-" --start 1',
|
|
50
|
+
'<%= config.bin %> <%= command.id %> docs/prd.md --agent architect --task create-doc',
|
|
51
|
+
];
|
|
52
|
+
static flags = {
|
|
53
|
+
...agentFlags,
|
|
54
|
+
count: Flags.integer({
|
|
55
|
+
char: 'c',
|
|
56
|
+
description: 'Number of epics to create (default: all remaining)',
|
|
57
|
+
min: 1,
|
|
58
|
+
}),
|
|
59
|
+
interval: Flags.integer({
|
|
60
|
+
char: 'i',
|
|
61
|
+
default: 0,
|
|
62
|
+
description: 'Seconds to wait between epic creations',
|
|
63
|
+
min: 0,
|
|
64
|
+
}),
|
|
65
|
+
prefix: Flags.string({
|
|
66
|
+
char: 'p',
|
|
67
|
+
description: 'Filename prefix for epic files (e.g., "PROJECT-")',
|
|
68
|
+
}),
|
|
69
|
+
reference: Flags.string({
|
|
70
|
+
char: 'r',
|
|
71
|
+
description: 'Reference file paths for AI agent context (repeatable)',
|
|
72
|
+
multiple: true,
|
|
73
|
+
}),
|
|
74
|
+
start: Flags.integer({
|
|
75
|
+
char: 's',
|
|
76
|
+
default: 1,
|
|
77
|
+
description: 'Start from epic number N',
|
|
78
|
+
min: 1,
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* AI agent runner service (Claude or Gemini)
|
|
83
|
+
*/
|
|
84
|
+
agentRunner;
|
|
85
|
+
/**
|
|
86
|
+
* File manager service
|
|
87
|
+
*/
|
|
88
|
+
fileManager;
|
|
89
|
+
/**
|
|
90
|
+
* File scaffolder service
|
|
91
|
+
*/
|
|
92
|
+
fileScaffolder;
|
|
93
|
+
/**
|
|
94
|
+
* Logger instance
|
|
95
|
+
*/
|
|
96
|
+
logger;
|
|
97
|
+
/**
|
|
98
|
+
* Path resolver service
|
|
99
|
+
*/
|
|
100
|
+
pathResolver;
|
|
101
|
+
/**
|
|
102
|
+
* PRD parser service
|
|
103
|
+
*/
|
|
104
|
+
prdParser;
|
|
105
|
+
/**
|
|
106
|
+
* Execute the command
|
|
107
|
+
*/
|
|
108
|
+
async run() {
|
|
109
|
+
const { args, flags } = await this.parse(EpicsCreate);
|
|
110
|
+
// Initialize services with selected provider
|
|
111
|
+
const provider = (flags.provider || 'claude');
|
|
112
|
+
this.initializeServices(provider);
|
|
113
|
+
const correlationId = generateCorrelationId();
|
|
114
|
+
this.logger.info({
|
|
115
|
+
command: 'epics:create',
|
|
116
|
+
correlationId,
|
|
117
|
+
flags,
|
|
118
|
+
prdPath: args['prd-path'],
|
|
119
|
+
}, 'Executing epics create command');
|
|
120
|
+
try {
|
|
121
|
+
// Validate PRD path
|
|
122
|
+
await this.validatePrdPath(args['prd-path']);
|
|
123
|
+
// Parse PRD and extract epics
|
|
124
|
+
const allEpics = await this.parsePrd(args['prd-path']);
|
|
125
|
+
// Filter epics based on start/count flags
|
|
126
|
+
const filteredEpics = this.filterEpics(allEpics, flags.start, flags.count);
|
|
127
|
+
// Get epic directory
|
|
128
|
+
const epicDir = this.pathResolver.getEpicDir();
|
|
129
|
+
// Check for existing epics (idempotent behavior)
|
|
130
|
+
const { skipped, toCreate } = await this.checkExistingEpics(filteredEpics, epicDir, flags.prefix);
|
|
131
|
+
// Display initial status
|
|
132
|
+
this.displayInitialStatus(allEpics.length, filteredEpics.length, toCreate.length, skipped.length);
|
|
133
|
+
// Create epics
|
|
134
|
+
const results = await this.createEpics({
|
|
135
|
+
agent: flags.agent,
|
|
136
|
+
epicDir,
|
|
137
|
+
epics: toCreate,
|
|
138
|
+
interval: flags.interval,
|
|
139
|
+
prdPath: args['prd-path'],
|
|
140
|
+
prefix: flags.prefix || '',
|
|
141
|
+
references: flags.reference,
|
|
142
|
+
task: flags.task,
|
|
143
|
+
});
|
|
144
|
+
// Display summary
|
|
145
|
+
this.displaySummary({
|
|
146
|
+
created: results.filter((r) => r.success).length,
|
|
147
|
+
failed: results.filter((r) => !r.success).length,
|
|
148
|
+
results,
|
|
149
|
+
skipped: skipped.length,
|
|
150
|
+
total: allEpics.length,
|
|
151
|
+
});
|
|
152
|
+
this.logger.info({ correlationId }, 'Epics create command completed successfully');
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
this.logger.error({ correlationId, error }, 'Epics create command failed');
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Build Claude CLI prompt for epic population
|
|
161
|
+
*/
|
|
162
|
+
buildClaudePrompt(options) {
|
|
163
|
+
const { agent = 'sm', epic, epicPath, prdPath, references, task = 'draft' } = options;
|
|
164
|
+
const agentFile = agent;
|
|
165
|
+
const taskCommand = task;
|
|
166
|
+
let prompt = `@.bmad-core/agents/${agentFile}.md\n\n`;
|
|
167
|
+
prompt += `Create epic '${epic.number}: ${epic.title}' for PRD '${prdPath}'.\n\n`;
|
|
168
|
+
prompt += `Target file: @${epicPath}\n\n`;
|
|
169
|
+
prompt += 'References:\n';
|
|
170
|
+
prompt += `@${prdPath}\n`;
|
|
171
|
+
if (references && references.length > 0) {
|
|
172
|
+
for (const ref of references) {
|
|
173
|
+
prompt += `@${path.resolve(ref)}\n`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
prompt += '\nIMPORTANT: The target file has been pre-scaffolded with structure and metadata.\n';
|
|
177
|
+
prompt += '- DO NOT modify the Epic Header section (Epic ID, Status: Draft, Created date are already set)\n';
|
|
178
|
+
prompt += '- DO NOT change the document structure or section headers\n';
|
|
179
|
+
prompt += '- ONLY populate the empty content sections marked with [AI Agent will populate]\n';
|
|
180
|
+
prompt += '- Follow the template structure at @.bmad-core/templates/epic-tmpl.yaml for content guidance\n\n';
|
|
181
|
+
prompt += `Execute the *${taskCommand} command to populate the epic document.\n`;
|
|
182
|
+
prompt += 'Update the file at the target path with the epic content in the empty sections.\n';
|
|
183
|
+
return prompt;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Check for existing epic files to support idempotent behavior
|
|
187
|
+
*/
|
|
188
|
+
async checkExistingEpics(epics, epicDir, prefix) {
|
|
189
|
+
const toCreate = [];
|
|
190
|
+
const skipped = [];
|
|
191
|
+
/* eslint-disable no-await-in-loop */
|
|
192
|
+
for (const epic of epics) {
|
|
193
|
+
const fileName = this.generateEpicFileName(epic, prefix || '');
|
|
194
|
+
const filePath = path.join(epicDir, fileName);
|
|
195
|
+
const exists = await this.fileManager.fileExists(filePath);
|
|
196
|
+
if (exists) {
|
|
197
|
+
this.logger.info({ epicNumber: epic.number, filePath }, 'Epic file already exists, skipping');
|
|
198
|
+
skipped.push(epic);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
toCreate.push(epic);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/* eslint-enable no-await-in-loop */
|
|
205
|
+
return { skipped, toCreate };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Create epic files using Claude AI agents
|
|
209
|
+
*/
|
|
210
|
+
async createEpics(options) {
|
|
211
|
+
const { agent, epicDir, epics, interval, prdPath, prefix, references, task } = options;
|
|
212
|
+
const results = [];
|
|
213
|
+
/* eslint-disable no-await-in-loop */
|
|
214
|
+
for (let i = 0; i < epics.length; i++) {
|
|
215
|
+
const epic = epics[i];
|
|
216
|
+
const result = await this.createSingleEpic({
|
|
217
|
+
agent,
|
|
218
|
+
epic,
|
|
219
|
+
epicDir,
|
|
220
|
+
prdPath,
|
|
221
|
+
prefix,
|
|
222
|
+
references,
|
|
223
|
+
task,
|
|
224
|
+
});
|
|
225
|
+
results.push(result);
|
|
226
|
+
// Wait interval if not last epic
|
|
227
|
+
if (i < epics.length - 1 && interval > 0) {
|
|
228
|
+
await this.waitInterval(interval);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/* eslint-enable no-await-in-loop */
|
|
232
|
+
return results;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Create a single epic file
|
|
236
|
+
*/
|
|
237
|
+
async createSingleEpic(options) {
|
|
238
|
+
const { agent, epic, epicDir, prdPath, prefix, references, task } = options;
|
|
239
|
+
const fileName = this.generateEpicFileName(epic, prefix);
|
|
240
|
+
const filePath = path.join(epicDir, fileName);
|
|
241
|
+
const spinner = createSpinner(`Creating Epic ${epic.number}: ${epic.title}...`);
|
|
242
|
+
spinner.start();
|
|
243
|
+
const startTime = Date.now();
|
|
244
|
+
try {
|
|
245
|
+
// Step 1: Create scaffolded file with structured sections and populated metadata
|
|
246
|
+
const scaffoldedContent = this.generateScaffoldedContent(epic, prefix);
|
|
247
|
+
await this.fileManager.writeFile(filePath, scaffoldedContent);
|
|
248
|
+
this.logger.info({
|
|
249
|
+
epicNumber: epic.number,
|
|
250
|
+
filePath,
|
|
251
|
+
}, 'Epic scaffolded file created');
|
|
252
|
+
// Step 2: Build Claude prompt to populate the scaffolded file
|
|
253
|
+
const absolutePrdPath = path.resolve(prdPath);
|
|
254
|
+
const absoluteEpicPath = path.resolve(filePath);
|
|
255
|
+
const prompt = this.buildClaudePrompt({
|
|
256
|
+
agent,
|
|
257
|
+
epic,
|
|
258
|
+
epicPath: absoluteEpicPath,
|
|
259
|
+
prdPath: absolutePrdPath,
|
|
260
|
+
references: references || [],
|
|
261
|
+
task,
|
|
262
|
+
});
|
|
263
|
+
// Step 3: Run Claude AI agent to populate epic content sections
|
|
264
|
+
spinner.text = `Populating epic ${epic.number} with AI agent...`;
|
|
265
|
+
const result = await this.agentRunner.runAgent(prompt, {
|
|
266
|
+
agentType: 'sm',
|
|
267
|
+
references,
|
|
268
|
+
timeout: 1_800_000, // 30 minutes
|
|
269
|
+
});
|
|
270
|
+
if (!result.success) {
|
|
271
|
+
throw new Error(result.errors || 'Claude agent failed to populate epic');
|
|
272
|
+
}
|
|
273
|
+
// Step 4: Verify file was updated by Claude
|
|
274
|
+
const updatedContent = await this.fileManager.readFile(filePath);
|
|
275
|
+
if (updatedContent === scaffoldedContent) {
|
|
276
|
+
throw new Error('Claude did not update the epic file');
|
|
277
|
+
}
|
|
278
|
+
const duration = Date.now() - startTime;
|
|
279
|
+
const durationSec = (duration / 1000).toFixed(1);
|
|
280
|
+
spinner.succeed(colors.success(`Epic ${epic.number} created successfully (${durationSec}s)`));
|
|
281
|
+
this.logger.info({
|
|
282
|
+
duration,
|
|
283
|
+
epicNumber: epic.number,
|
|
284
|
+
filePath,
|
|
285
|
+
}, 'Epic created successfully');
|
|
286
|
+
return {
|
|
287
|
+
duration,
|
|
288
|
+
epic,
|
|
289
|
+
filePath,
|
|
290
|
+
success: true,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
const duration = Date.now() - startTime;
|
|
295
|
+
const err = error;
|
|
296
|
+
spinner.fail(colors.error(`Epic ${epic.number} creation failed - ${err.message}`));
|
|
297
|
+
this.logger.error({
|
|
298
|
+
duration,
|
|
299
|
+
epicNumber: epic.number,
|
|
300
|
+
error: err,
|
|
301
|
+
filePath,
|
|
302
|
+
}, 'Epic creation failed');
|
|
303
|
+
return {
|
|
304
|
+
duration,
|
|
305
|
+
epic,
|
|
306
|
+
error: err.message,
|
|
307
|
+
filePath,
|
|
308
|
+
success: false,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Display initial status before creating epics
|
|
314
|
+
*/
|
|
315
|
+
displayInitialStatus(totalEpics, filteredCount, toCreateCount, skippedCount) {
|
|
316
|
+
const boxTop = '┌─────────────────────────────────────────┐';
|
|
317
|
+
const boxDivider = '├─────────────────────────────────────────┤';
|
|
318
|
+
const boxBottom = '└─────────────────────────────────────────┘';
|
|
319
|
+
this.log('');
|
|
320
|
+
this.log(boxTop);
|
|
321
|
+
this.log('│ Epic Creation Status │');
|
|
322
|
+
this.log(boxDivider);
|
|
323
|
+
this.log(`│ Total epics in PRD: ${colors.bold(totalEpics.toString().padEnd(17))}│`);
|
|
324
|
+
this.log(`│ Epics in range: ${colors.bold(filteredCount.toString().padEnd(17))}│`);
|
|
325
|
+
this.log(`│ ${colors.success('To create:')} ${toCreateCount.toString().padEnd(17)}│`);
|
|
326
|
+
this.log(`│ ${colors.dim('Already exist:')} ${skippedCount.toString().padEnd(17)}│`);
|
|
327
|
+
this.log(boxBottom);
|
|
328
|
+
this.log('');
|
|
329
|
+
if (toCreateCount === 0) {
|
|
330
|
+
this.log(colors.info('All epics already exist. Nothing to create.'));
|
|
331
|
+
this.log('');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Display summary report after creating epics
|
|
336
|
+
*/
|
|
337
|
+
displaySummary(summary) {
|
|
338
|
+
const boxTop = '┌─────────────────────────────────────────┐';
|
|
339
|
+
const boxDivider = '├─────────────────────────────────────────┤';
|
|
340
|
+
const boxBottom = '└─────────────────────────────────────────┘';
|
|
341
|
+
this.log('');
|
|
342
|
+
this.log(boxTop);
|
|
343
|
+
this.log('│ Epic Creation Summary │');
|
|
344
|
+
this.log(boxDivider);
|
|
345
|
+
this.log(`│ Total epics: ${colors.bold(summary.total.toString().padEnd(17))}│`);
|
|
346
|
+
this.log(`│ ${colors.success('Created:')} ${summary.created.toString().padEnd(17)}│`);
|
|
347
|
+
this.log(`│ ${colors.dim('Skipped:')} ${summary.skipped.toString().padEnd(17)}│`);
|
|
348
|
+
this.log(`│ ${colors.error('Failed:')} ${summary.failed.toString().padEnd(17)}│`);
|
|
349
|
+
this.log(boxBottom);
|
|
350
|
+
// Display failed epics if any
|
|
351
|
+
const failedResults = summary.results.filter((r) => !r.success);
|
|
352
|
+
if (failedResults.length > 0) {
|
|
353
|
+
this.log('');
|
|
354
|
+
this.log(colors.bold('Failed Epics:'));
|
|
355
|
+
for (const result of failedResults) {
|
|
356
|
+
this.log(colors.error(` ✗ Epic ${result.epic.number}: ${result.epic.title}`));
|
|
357
|
+
this.log(colors.dim(` ${result.error}`));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
this.log('');
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Filter epics based on start and count flags
|
|
364
|
+
*/
|
|
365
|
+
filterEpics(epics, start, count) {
|
|
366
|
+
// Filter by start
|
|
367
|
+
let filtered = epics.filter((epic) => epic.number >= start);
|
|
368
|
+
// Apply count limit if specified
|
|
369
|
+
if (count !== undefined && count > 0) {
|
|
370
|
+
filtered = filtered.slice(0, count);
|
|
371
|
+
}
|
|
372
|
+
this.logger.debug({
|
|
373
|
+
count,
|
|
374
|
+
filteredCount: filtered.length,
|
|
375
|
+
start,
|
|
376
|
+
totalEpics: epics.length,
|
|
377
|
+
}, 'Filtered epics based on flags');
|
|
378
|
+
return filtered;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Generate epic filename from epic data
|
|
382
|
+
*/
|
|
383
|
+
generateEpicFileName(epic, prefix) {
|
|
384
|
+
// Convert title to slug (lowercase, replace spaces with hyphens, remove special chars)
|
|
385
|
+
const slug = epic.title
|
|
386
|
+
.toLowerCase()
|
|
387
|
+
.replaceAll(/[^\da-z\s-]/g, '')
|
|
388
|
+
.trim()
|
|
389
|
+
.replaceAll(/\s+/g, '-');
|
|
390
|
+
return `${prefix}epic-${epic.number}-${slug}.md`;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Generate scaffolded content for epic file
|
|
394
|
+
*/
|
|
395
|
+
generateScaffoldedContent(epic, prefix) {
|
|
396
|
+
return this.fileScaffolder.scaffoldEpic({
|
|
397
|
+
epicNumber: epic.number,
|
|
398
|
+
epicTitle: epic.title,
|
|
399
|
+
prefix,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Initialize service dependencies
|
|
404
|
+
*/
|
|
405
|
+
initializeServices(provider = 'claude') {
|
|
406
|
+
this.logger = createLogger({ namespace: 'commands:epics:create' });
|
|
407
|
+
this.logger.info({ provider }, 'Initializing services with AI provider');
|
|
408
|
+
this.fileManager = new FileManager(this.logger);
|
|
409
|
+
this.pathResolver = new PathResolver(this.fileManager, this.logger);
|
|
410
|
+
this.prdParser = new PrdParser(this.logger);
|
|
411
|
+
this.fileScaffolder = new FileScaffolder(this.logger);
|
|
412
|
+
this.agentRunner = createAgentRunner(provider, this.logger);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Parse PRD file and extract epics
|
|
416
|
+
*/
|
|
417
|
+
async parsePrd(prdPath) {
|
|
418
|
+
this.logger.info({ prdPath }, 'Parsing PRD file');
|
|
419
|
+
const spinner = createSpinner('Parsing PRD file...');
|
|
420
|
+
spinner.start();
|
|
421
|
+
try {
|
|
422
|
+
const prdContent = await this.fileManager.readFile(prdPath);
|
|
423
|
+
const epics = this.prdParser.parseEpics(prdContent, prdPath);
|
|
424
|
+
spinner.succeed(colors.success(`Found ${epics.length} epic(s) in PRD`));
|
|
425
|
+
this.logger.info({ epicCount: epics.length, prdPath }, 'PRD parsed successfully');
|
|
426
|
+
return epics;
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
spinner.fail(colors.error('Failed to parse PRD'));
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Validate PRD path exists
|
|
435
|
+
*/
|
|
436
|
+
async validatePrdPath(prdPath) {
|
|
437
|
+
const exists = await this.fileManager.fileExists(prdPath);
|
|
438
|
+
if (!exists) {
|
|
439
|
+
throw new ValidationError(`PRD file not found: ${prdPath}`, {
|
|
440
|
+
prdPath,
|
|
441
|
+
suggestion: 'Check the path and ensure the file exists',
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
this.logger.debug({ prdPath }, 'PRD path validation passed');
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Wait for specified interval with countdown
|
|
448
|
+
*/
|
|
449
|
+
async waitInterval(seconds) {
|
|
450
|
+
if (seconds <= 0)
|
|
451
|
+
return;
|
|
452
|
+
const spinner = createSpinner(`Waiting ${seconds} seconds before next epic...`);
|
|
453
|
+
spinner.start();
|
|
454
|
+
await new Promise((resolve) => {
|
|
455
|
+
setTimeout(resolve, seconds * 1000);
|
|
456
|
+
});
|
|
457
|
+
spinner.stop();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Epics List Command
|
|
3
|
+
*
|
|
4
|
+
* Lists all epic files with their metadata in a formatted table.
|
|
5
|
+
* Scans docs/epics/ directory for epic files and displays:
|
|
6
|
+
* - Epic number
|
|
7
|
+
* - Epic title
|
|
8
|
+
* - Story count
|
|
9
|
+
* - File path
|
|
10
|
+
*
|
|
11
|
+
* Supports JSON output mode for machine-readable consumption.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```bash
|
|
15
|
+
* # Display epics in table format
|
|
16
|
+
* bmad-workflow epics list
|
|
17
|
+
*
|
|
18
|
+
* # Output as JSON
|
|
19
|
+
* bmad-workflow epics list --json
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import { Command } from '@oclif/core';
|
|
23
|
+
/**
|
|
24
|
+
* Epics List Command
|
|
25
|
+
*
|
|
26
|
+
* Lists all epic files with metadata in table or JSON format.
|
|
27
|
+
*/
|
|
28
|
+
export default class EpicsList extends Command {
|
|
29
|
+
/**
|
|
30
|
+
* Command description
|
|
31
|
+
*/
|
|
32
|
+
static description: string;
|
|
33
|
+
/**
|
|
34
|
+
* Command examples
|
|
35
|
+
*/
|
|
36
|
+
static examples: string[];
|
|
37
|
+
/**
|
|
38
|
+
* Command flags
|
|
39
|
+
*/
|
|
40
|
+
static flags: {
|
|
41
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Logger instance
|
|
45
|
+
*/
|
|
46
|
+
private logger;
|
|
47
|
+
/**
|
|
48
|
+
* EpicParser instance
|
|
49
|
+
*/
|
|
50
|
+
private epicParser;
|
|
51
|
+
/**
|
|
52
|
+
* FileManager instance
|
|
53
|
+
*/
|
|
54
|
+
private fileManager;
|
|
55
|
+
/**
|
|
56
|
+
* PathResolver instance
|
|
57
|
+
*/
|
|
58
|
+
private pathResolver;
|
|
59
|
+
/**
|
|
60
|
+
* Run the command
|
|
61
|
+
*/
|
|
62
|
+
run(): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Discover epic files and extract metadata
|
|
65
|
+
*
|
|
66
|
+
* @returns Array of epic metadata objects
|
|
67
|
+
*/
|
|
68
|
+
private discoverEpics;
|
|
69
|
+
/**
|
|
70
|
+
* Extract epic number from filename
|
|
71
|
+
*
|
|
72
|
+
* @param filePath - Epic file path
|
|
73
|
+
* @returns Epic number
|
|
74
|
+
*/
|
|
75
|
+
private extractEpicNumber;
|
|
76
|
+
/**
|
|
77
|
+
* Extract epic title from content
|
|
78
|
+
*
|
|
79
|
+
* Looks for first # heading in content, or derives from filename.
|
|
80
|
+
*
|
|
81
|
+
* @param content - Epic file content
|
|
82
|
+
* @param filePath - Epic file path (for fallback title)
|
|
83
|
+
* @returns Epic title
|
|
84
|
+
*/
|
|
85
|
+
private extractEpicTitle;
|
|
86
|
+
/**
|
|
87
|
+
* Check if a file is an epic file
|
|
88
|
+
*
|
|
89
|
+
* @param filePath - File path to check
|
|
90
|
+
* @returns True if file matches epic pattern
|
|
91
|
+
*/
|
|
92
|
+
private isEpicFile;
|
|
93
|
+
/**
|
|
94
|
+
* Output epic metadata as JSON
|
|
95
|
+
*
|
|
96
|
+
* @param epicMetadata - Array of epic metadata
|
|
97
|
+
*/
|
|
98
|
+
private outputJson;
|
|
99
|
+
/**
|
|
100
|
+
* Output epic metadata as formatted table
|
|
101
|
+
*
|
|
102
|
+
* @param epicMetadata - Array of epic metadata
|
|
103
|
+
*/
|
|
104
|
+
private outputTable;
|
|
105
|
+
/**
|
|
106
|
+
* Parse epic file to extract metadata
|
|
107
|
+
*
|
|
108
|
+
* @param epicFile - Path to epic file
|
|
109
|
+
* @returns Epic metadata
|
|
110
|
+
*/
|
|
111
|
+
private parseEpicMetadata;
|
|
112
|
+
/**
|
|
113
|
+
* Truncate title if too long
|
|
114
|
+
*
|
|
115
|
+
* @param title - Title to truncate
|
|
116
|
+
* @param maxLength - Maximum length
|
|
117
|
+
* @returns Truncated title with ellipsis if needed
|
|
118
|
+
*/
|
|
119
|
+
private truncateTitle;
|
|
120
|
+
}
|