@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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1017 -0
  3. package/bin/dev +5 -0
  4. package/bin/dev.cmd +3 -0
  5. package/bin/dev.js +5 -0
  6. package/bin/run +5 -0
  7. package/bin/run.cmd +3 -0
  8. package/bin/run.js +5 -0
  9. package/dist/commands/config/show.d.ts +34 -0
  10. package/dist/commands/config/show.js +108 -0
  11. package/dist/commands/config/validate.d.ts +29 -0
  12. package/dist/commands/config/validate.js +131 -0
  13. package/dist/commands/decompose.d.ts +79 -0
  14. package/dist/commands/decompose.js +327 -0
  15. package/dist/commands/demo.d.ts +18 -0
  16. package/dist/commands/demo.js +107 -0
  17. package/dist/commands/epics/create.d.ts +123 -0
  18. package/dist/commands/epics/create.js +459 -0
  19. package/dist/commands/epics/list.d.ts +120 -0
  20. package/dist/commands/epics/list.js +280 -0
  21. package/dist/commands/hello/index.d.ts +12 -0
  22. package/dist/commands/hello/index.js +34 -0
  23. package/dist/commands/hello/world.d.ts +8 -0
  24. package/dist/commands/hello/world.js +24 -0
  25. package/dist/commands/prd/fix.d.ts +39 -0
  26. package/dist/commands/prd/fix.js +140 -0
  27. package/dist/commands/prd/validate.d.ts +112 -0
  28. package/dist/commands/prd/validate.js +302 -0
  29. package/dist/commands/stories/create.d.ts +95 -0
  30. package/dist/commands/stories/create.js +431 -0
  31. package/dist/commands/stories/develop.d.ts +91 -0
  32. package/dist/commands/stories/develop.js +460 -0
  33. package/dist/commands/stories/list.d.ts +84 -0
  34. package/dist/commands/stories/list.js +291 -0
  35. package/dist/commands/stories/move.d.ts +66 -0
  36. package/dist/commands/stories/move.js +273 -0
  37. package/dist/commands/stories/qa.d.ts +99 -0
  38. package/dist/commands/stories/qa.js +530 -0
  39. package/dist/commands/workflow.d.ts +97 -0
  40. package/dist/commands/workflow.js +390 -0
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.js +1 -0
  43. package/dist/models/agent-options.d.ts +50 -0
  44. package/dist/models/agent-options.js +1 -0
  45. package/dist/models/agent-result.d.ts +29 -0
  46. package/dist/models/agent-result.js +1 -0
  47. package/dist/models/index.d.ts +10 -0
  48. package/dist/models/index.js +10 -0
  49. package/dist/models/phase-result.d.ts +65 -0
  50. package/dist/models/phase-result.js +7 -0
  51. package/dist/models/provider.d.ts +28 -0
  52. package/dist/models/provider.js +18 -0
  53. package/dist/models/story.d.ts +154 -0
  54. package/dist/models/story.js +18 -0
  55. package/dist/models/workflow-config.d.ts +148 -0
  56. package/dist/models/workflow-config.js +1 -0
  57. package/dist/models/workflow-result.d.ts +164 -0
  58. package/dist/models/workflow-result.js +7 -0
  59. package/dist/services/agents/agent-runner-factory.d.ts +31 -0
  60. package/dist/services/agents/agent-runner-factory.js +44 -0
  61. package/dist/services/agents/agent-runner.d.ts +46 -0
  62. package/dist/services/agents/agent-runner.js +29 -0
  63. package/dist/services/agents/claude-agent-runner.d.ts +81 -0
  64. package/dist/services/agents/claude-agent-runner.js +332 -0
  65. package/dist/services/agents/gemini-agent-runner.d.ts +82 -0
  66. package/dist/services/agents/gemini-agent-runner.js +350 -0
  67. package/dist/services/agents/index.d.ts +7 -0
  68. package/dist/services/agents/index.js +7 -0
  69. package/dist/services/file-system/file-manager.d.ts +110 -0
  70. package/dist/services/file-system/file-manager.js +223 -0
  71. package/dist/services/file-system/glob-matcher.d.ts +75 -0
  72. package/dist/services/file-system/glob-matcher.js +126 -0
  73. package/dist/services/file-system/path-resolver.d.ts +183 -0
  74. package/dist/services/file-system/path-resolver.js +400 -0
  75. package/dist/services/logging/workflow-logger.d.ts +232 -0
  76. package/dist/services/logging/workflow-logger.js +552 -0
  77. package/dist/services/orchestration/batch-processor.d.ts +113 -0
  78. package/dist/services/orchestration/batch-processor.js +187 -0
  79. package/dist/services/orchestration/dependency-graph-executor.d.ts +60 -0
  80. package/dist/services/orchestration/dependency-graph-executor.js +447 -0
  81. package/dist/services/orchestration/index.d.ts +10 -0
  82. package/dist/services/orchestration/index.js +8 -0
  83. package/dist/services/orchestration/input-detector.d.ts +125 -0
  84. package/dist/services/orchestration/input-detector.js +381 -0
  85. package/dist/services/orchestration/story-queue.d.ts +94 -0
  86. package/dist/services/orchestration/story-queue.js +170 -0
  87. package/dist/services/orchestration/story-type-detector.d.ts +80 -0
  88. package/dist/services/orchestration/story-type-detector.js +258 -0
  89. package/dist/services/orchestration/task-decomposition-service.d.ts +67 -0
  90. package/dist/services/orchestration/task-decomposition-service.js +607 -0
  91. package/dist/services/orchestration/workflow-orchestrator.d.ts +659 -0
  92. package/dist/services/orchestration/workflow-orchestrator.js +2201 -0
  93. package/dist/services/parsers/epic-parser.d.ts +117 -0
  94. package/dist/services/parsers/epic-parser.js +264 -0
  95. package/dist/services/parsers/prd-fixer.d.ts +86 -0
  96. package/dist/services/parsers/prd-fixer.js +194 -0
  97. package/dist/services/parsers/prd-parser.d.ts +123 -0
  98. package/dist/services/parsers/prd-parser.js +286 -0
  99. package/dist/services/parsers/standalone-story-parser.d.ts +114 -0
  100. package/dist/services/parsers/standalone-story-parser.js +255 -0
  101. package/dist/services/parsers/story-parser-factory.d.ts +81 -0
  102. package/dist/services/parsers/story-parser-factory.js +108 -0
  103. package/dist/services/parsers/story-parser.d.ts +122 -0
  104. package/dist/services/parsers/story-parser.js +262 -0
  105. package/dist/services/scaffolding/decompose-session-scaffolder.d.ts +74 -0
  106. package/dist/services/scaffolding/decompose-session-scaffolder.js +315 -0
  107. package/dist/services/scaffolding/file-scaffolder.d.ts +94 -0
  108. package/dist/services/scaffolding/file-scaffolder.js +314 -0
  109. package/dist/services/validation/config-validator.d.ts +88 -0
  110. package/dist/services/validation/config-validator.js +167 -0
  111. package/dist/types/task-graph.d.ts +142 -0
  112. package/dist/types/task-graph.js +5 -0
  113. package/dist/utils/colors.d.ts +49 -0
  114. package/dist/utils/colors.js +50 -0
  115. package/dist/utils/error-formatter.d.ts +64 -0
  116. package/dist/utils/error-formatter.js +279 -0
  117. package/dist/utils/errors.d.ts +170 -0
  118. package/dist/utils/errors.js +233 -0
  119. package/dist/utils/formatters.d.ts +84 -0
  120. package/dist/utils/formatters.js +162 -0
  121. package/dist/utils/logger.d.ts +63 -0
  122. package/dist/utils/logger.js +78 -0
  123. package/dist/utils/progress.d.ts +104 -0
  124. package/dist/utils/progress.js +161 -0
  125. package/dist/utils/retry.d.ts +114 -0
  126. package/dist/utils/retry.js +160 -0
  127. package/dist/utils/shared-flags.d.ts +28 -0
  128. package/dist/utils/shared-flags.js +43 -0
  129. 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
+ }