@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,280 @@
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, Flags } from '@oclif/core';
23
+ import Table from 'cli-table3';
24
+ import { relative } from 'node:path';
25
+ import { FileManager } from '../../services/file-system/file-manager.js';
26
+ import { PathResolver } from '../../services/file-system/path-resolver.js';
27
+ import { EpicParser } from '../../services/parsers/epic-parser.js';
28
+ import * as Colors from '../../utils/colors.js';
29
+ import { createLogger } from '../../utils/logger.js';
30
+ /**
31
+ * Epics List Command
32
+ *
33
+ * Lists all epic files with metadata in table or JSON format.
34
+ */
35
+ export default class EpicsList extends Command {
36
+ /**
37
+ * Command description
38
+ */
39
+ static description = 'List all epics with metadata from docs/epics/ directory';
40
+ /**
41
+ * Command examples
42
+ */
43
+ static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --json'];
44
+ /**
45
+ * Command flags
46
+ */
47
+ static flags = {
48
+ json: Flags.boolean({
49
+ char: 'j',
50
+ default: false,
51
+ description: 'Output as JSON array',
52
+ }),
53
+ };
54
+ /**
55
+ * Logger instance
56
+ */
57
+ logger = createLogger({ namespace: 'bmad-workflow:commands:epics:list' });
58
+ /**
59
+ * EpicParser instance
60
+ */
61
+ epicParser = new EpicParser(this.logger);
62
+ /**
63
+ * FileManager instance
64
+ */
65
+ fileManager = new FileManager(this.logger);
66
+ /**
67
+ * PathResolver instance
68
+ */
69
+ pathResolver = new PathResolver(this.fileManager, this.logger);
70
+ /**
71
+ * Run the command
72
+ */
73
+ async run() {
74
+ const { flags } = await this.parse(EpicsList);
75
+ try {
76
+ // Discover epic files
77
+ const epicMetadata = await this.discoverEpics();
78
+ // Sort by epic number
79
+ epicMetadata.sort((a, b) => a.number - b.number);
80
+ // Output in requested format
81
+ if (flags.json) {
82
+ this.outputJson(epicMetadata);
83
+ }
84
+ else {
85
+ this.outputTable(epicMetadata);
86
+ }
87
+ }
88
+ catch (error) {
89
+ const err = error;
90
+ this.logger.error({ error: err }, 'Failed to list epics');
91
+ this.error(Colors.error(`Failed to list epics: ${err.message}`));
92
+ }
93
+ }
94
+ /**
95
+ * Discover epic files and extract metadata
96
+ *
97
+ * @returns Array of epic metadata objects
98
+ */
99
+ async discoverEpics() {
100
+ this.logger.info('Discovering epic files');
101
+ // Get epic directory path
102
+ const epicDir = this.pathResolver.getEpicDir();
103
+ this.logger.debug({ epicDir }, 'Epic directory resolved');
104
+ // Read directory contents
105
+ const files = await this.fileManager.listFiles(epicDir, '*.md');
106
+ this.logger.debug({ fileCount: files.length }, 'Epic files discovered');
107
+ // Filter files matching epic pattern
108
+ const epicFiles = files.filter((file) => this.isEpicFile(file));
109
+ this.logger.debug({ epicFileCount: epicFiles.length }, 'Epic files filtered');
110
+ if (epicFiles.length === 0) {
111
+ this.logger.warn('No epic files found');
112
+ this.warn(Colors.warning('No epic files found in epic directory'));
113
+ return [];
114
+ }
115
+ // Parse each epic file to extract metadata
116
+ const epicMetadata = await Promise.all(epicFiles.map(async (epicFile) => {
117
+ try {
118
+ return await this.parseEpicMetadata(epicFile);
119
+ }
120
+ catch (error) {
121
+ // Handle parsing errors gracefully
122
+ const err = error;
123
+ this.logger.error({ epicFile, error: err }, 'Failed to parse epic file');
124
+ // Extract epic number from filename
125
+ const epicNumber = this.extractEpicNumber(epicFile);
126
+ const relativePath = relative(process.cwd(), epicFile);
127
+ return {
128
+ filePath: relativePath,
129
+ number: epicNumber,
130
+ storyCount: 0,
131
+ title: 'Parse Error',
132
+ };
133
+ }
134
+ }));
135
+ this.logger.info({ epicCount: epicMetadata.length }, 'Epic metadata extracted');
136
+ return epicMetadata;
137
+ }
138
+ /**
139
+ * Extract epic number from filename
140
+ *
141
+ * @param filePath - Epic file path
142
+ * @returns Epic number
143
+ */
144
+ extractEpicNumber(filePath) {
145
+ const filename = filePath.split('/').pop() || filePath;
146
+ const match = filename.match(/epic-(\d+)-/i);
147
+ if (match && match[1]) {
148
+ return Number.parseInt(match[1], 10);
149
+ }
150
+ // Fallback to 0 if pattern doesn't match
151
+ this.logger.warn({ filename }, 'Could not extract epic number from filename');
152
+ return 0;
153
+ }
154
+ /**
155
+ * Extract epic title from content
156
+ *
157
+ * Looks for first # heading in content, or derives from filename.
158
+ *
159
+ * @param content - Epic file content
160
+ * @param filePath - Epic file path (for fallback title)
161
+ * @returns Epic title
162
+ */
163
+ extractEpicTitle(content, filePath) {
164
+ // Look for first # heading
165
+ const headingPattern = /^#\s+(.+?)$/m;
166
+ const match = content.match(headingPattern);
167
+ if (match && match[1]) {
168
+ return match[1].trim();
169
+ }
170
+ // Fallback: derive from filename
171
+ const filename = filePath.split('/').pop() || filePath;
172
+ const titlePart = filename
173
+ .replace(/^epic-\d+-/i, '') // Remove epic-N- prefix
174
+ .replace(/\.md$/, '') // Remove .md extension
175
+ // eslint-disable-next-line unicorn/prefer-string-replace-all
176
+ .replace(/-/g, ' ') // Replace all dashes with spaces
177
+ .split(' ')
178
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) // Title case
179
+ .join(' ');
180
+ this.logger.debug({ filename, title: titlePart }, 'Title derived from filename');
181
+ return titlePart;
182
+ }
183
+ /**
184
+ * Check if a file is an epic file
185
+ *
186
+ * @param filePath - File path to check
187
+ * @returns True if file matches epic pattern
188
+ */
189
+ isEpicFile(filePath) {
190
+ const filename = filePath.split('/').pop() || filePath;
191
+ // Pattern: epic-{n}-*.md or EPIC-{n}-*.md
192
+ const epicPattern = /^epic-\d+-.+\.md$/i;
193
+ return epicPattern.test(filename);
194
+ }
195
+ /**
196
+ * Output epic metadata as JSON
197
+ *
198
+ * @param epicMetadata - Array of epic metadata
199
+ */
200
+ outputJson(epicMetadata) {
201
+ const output = JSON.stringify(epicMetadata, null, 2);
202
+ this.log(output);
203
+ }
204
+ /**
205
+ * Output epic metadata as formatted table
206
+ *
207
+ * @param epicMetadata - Array of epic metadata
208
+ */
209
+ outputTable(epicMetadata) {
210
+ if (epicMetadata.length === 0) {
211
+ this.log(Colors.warning('No epics found'));
212
+ return;
213
+ }
214
+ // Create table
215
+ const table = new Table({
216
+ colWidths: [10, 50, 10, 50],
217
+ head: [
218
+ Colors.bold(Colors.highlight('Epic #')),
219
+ Colors.bold(Colors.highlight('Title')),
220
+ Colors.bold(Colors.highlight('Stories')),
221
+ Colors.bold(Colors.highlight('File Path')),
222
+ ],
223
+ wordWrap: true,
224
+ });
225
+ // Add rows
226
+ for (const epic of epicMetadata) {
227
+ table.push([
228
+ epic.number.toString(),
229
+ this.truncateTitle(epic.title, 48),
230
+ epic.storyCount.toString(),
231
+ epic.filePath,
232
+ ]);
233
+ }
234
+ // Display table
235
+ this.log(table.toString());
236
+ // Display summary
237
+ const totalStories = epicMetadata.reduce((sum, epic) => sum + epic.storyCount, 0);
238
+ const summary = `Found ${epicMetadata.length} epics with ${totalStories} total stories`;
239
+ this.log('');
240
+ this.log(Colors.success(summary));
241
+ }
242
+ /**
243
+ * Parse epic file to extract metadata
244
+ *
245
+ * @param epicFile - Path to epic file
246
+ * @returns Epic metadata
247
+ */
248
+ async parseEpicMetadata(epicFile) {
249
+ this.logger.debug({ epicFile }, 'Parsing epic metadata');
250
+ // Read file content
251
+ const content = await this.fileManager.readFile(epicFile);
252
+ // Extract epic number from filename
253
+ const epicNumber = this.extractEpicNumber(epicFile);
254
+ // Parse stories from content
255
+ const stories = this.epicParser.parseStories(content, epicFile);
256
+ // Extract title from first heading
257
+ const title = this.extractEpicTitle(content, epicFile);
258
+ // Build relative path
259
+ const relativePath = relative(process.cwd(), epicFile);
260
+ return {
261
+ filePath: relativePath,
262
+ number: epicNumber,
263
+ storyCount: stories.length,
264
+ title,
265
+ };
266
+ }
267
+ /**
268
+ * Truncate title if too long
269
+ *
270
+ * @param title - Title to truncate
271
+ * @param maxLength - Maximum length
272
+ * @returns Truncated title with ellipsis if needed
273
+ */
274
+ truncateTitle(title, maxLength) {
275
+ if (title.length <= maxLength) {
276
+ return title;
277
+ }
278
+ return title.slice(0, maxLength - 3) + '...';
279
+ }
280
+ }
@@ -0,0 +1,12 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Hello extends Command {
3
+ static args: {
4
+ person: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ from: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,34 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { createLogger, generateCorrelationId } from '../../utils/logger.js';
3
+ export default class Hello extends Command {
4
+ static args = {
5
+ person: Args.string({ description: 'Person to say hello to', required: true }),
6
+ };
7
+ static description = 'Say hello';
8
+ static examples = [
9
+ `<%= config.bin %> <%= command.id %> friend --from oclif
10
+ hello friend from oclif! (./src/commands/hello/index.ts)
11
+ `,
12
+ ];
13
+ static flags = {
14
+ from: Flags.string({ char: 'f', description: 'Who is saying hello', required: true }),
15
+ };
16
+ async run() {
17
+ const { args, flags } = await this.parse(Hello);
18
+ // Create logger with command context
19
+ const logger = createLogger({ namespace: 'commands:hello' });
20
+ const correlationId = generateCorrelationId();
21
+ // Log command execution with context
22
+ logger.info({
23
+ command: 'hello',
24
+ correlationId,
25
+ from: flags.from,
26
+ person: args.person,
27
+ }, 'Executing hello command');
28
+ // Demonstrate different log levels
29
+ logger.debug({ correlationId }, 'Debug: Command arguments parsed successfully');
30
+ logger.info({ correlationId }, `Greeting ${args.person} from ${flags.from}`);
31
+ this.log(`hello ${args.person} from ${flags.from}! (./src/commands/hello/index.ts)`);
32
+ logger.info({ correlationId }, 'Hello command completed successfully');
33
+ }
34
+ }
@@ -0,0 +1,8 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class World extends Command {
3
+ static args: {};
4
+ static description: string;
5
+ static examples: string[];
6
+ static flags: {};
7
+ run(): Promise<void>;
8
+ }
@@ -0,0 +1,24 @@
1
+ import { Command } from '@oclif/core';
2
+ import { createLogger, generateCorrelationId } from '../../utils/logger.js';
3
+ export default class World extends Command {
4
+ static args = {};
5
+ static description = 'Say hello world';
6
+ static examples = [
7
+ `<%= config.bin %> <%= command.id %>
8
+ hello world! (./src/commands/hello/world.ts)
9
+ `,
10
+ ];
11
+ static flags = {};
12
+ async run() {
13
+ // Create logger with command context
14
+ const logger = createLogger({ namespace: 'commands:hello:world' });
15
+ const correlationId = generateCorrelationId();
16
+ // Log command execution
17
+ logger.info({
18
+ command: 'hello:world',
19
+ correlationId,
20
+ }, 'Executing hello world command');
21
+ this.log('hello world! (./src/commands/hello/world.ts)');
22
+ logger.info({ correlationId }, 'Hello world command completed');
23
+ }
24
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * PRD Fix Command
3
+ *
4
+ * Uses AI to automatically reformat a PRD file to match the expected
5
+ * epic/story structure for workflow processing.
6
+ *
7
+ * @example
8
+ * ```bash
9
+ * # Fix PRD format
10
+ * bmad-workflow prd fix docs/PRD-feature.md
11
+ *
12
+ * # Fix with architecture reference for context
13
+ * bmad-workflow prd fix docs/PRD-feature.md --reference docs/architecture.md
14
+ * ```
15
+ */
16
+ import { Command } from '@oclif/core';
17
+ /**
18
+ * PRD Fix Command
19
+ *
20
+ * Reformats a PRD file to match expected epic/story patterns using AI.
21
+ */
22
+ export default class PrdFix extends Command {
23
+ static args: {
24
+ prdFile: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
25
+ };
26
+ static description: string;
27
+ static examples: string[];
28
+ static flags: {
29
+ provider: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
30
+ reference: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
31
+ };
32
+ private fileManager;
33
+ private logger;
34
+ private prdFixer;
35
+ private prdParser;
36
+ run(): Promise<void>;
37
+ private initializeServices;
38
+ private validateFixedContent;
39
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * PRD Fix Command
3
+ *
4
+ * Uses AI to automatically reformat a PRD file to match the expected
5
+ * epic/story structure for workflow processing.
6
+ *
7
+ * @example
8
+ * ```bash
9
+ * # Fix PRD format
10
+ * bmad-workflow prd fix docs/PRD-feature.md
11
+ *
12
+ * # Fix with architecture reference for context
13
+ * bmad-workflow prd fix docs/PRD-feature.md --reference docs/architecture.md
14
+ * ```
15
+ */
16
+ import { Args, Command, Flags } from '@oclif/core';
17
+ import { relative } from 'node:path';
18
+ import { createAgentRunner, isProviderSupported } from '../../services/agents/agent-runner-factory.js';
19
+ import { FileManager } from '../../services/file-system/file-manager.js';
20
+ import { PrdFixer } from '../../services/parsers/prd-fixer.js';
21
+ import { PrdParser } from '../../services/parsers/prd-parser.js';
22
+ import * as Colors from '../../utils/colors.js';
23
+ import { createLogger } from '../../utils/logger.js';
24
+ /**
25
+ * PRD Fix Command
26
+ *
27
+ * Reformats a PRD file to match expected epic/story patterns using AI.
28
+ */
29
+ export default class PrdFix extends Command {
30
+ static args = {
31
+ prdFile: Args.string({
32
+ description: 'Path to PRD markdown file to fix',
33
+ required: true,
34
+ }),
35
+ };
36
+ static description = 'Fix PRD format using AI to match expected epic/story structure';
37
+ static examples = [
38
+ '<%= config.bin %> <%= command.id %> docs/PRD-feature.md',
39
+ '<%= config.bin %> <%= command.id %> docs/PRD-feature.md --reference docs/architecture.md',
40
+ '<%= config.bin %> <%= command.id %> docs/PRD-feature.md --provider gemini',
41
+ ];
42
+ static flags = {
43
+ provider: Flags.string({
44
+ default: 'claude',
45
+ description: 'AI provider to use (claude or gemini)',
46
+ options: ['claude', 'gemini'],
47
+ }),
48
+ reference: Flags.string({
49
+ description: 'Reference files for context (can be used multiple times)',
50
+ multiple: true,
51
+ }),
52
+ };
53
+ fileManager;
54
+ logger = createLogger({ namespace: 'bmad-workflow:commands:prd:fix' });
55
+ prdFixer;
56
+ prdParser;
57
+ async run() {
58
+ const { args, flags } = await this.parse(PrdFix);
59
+ // Validate provider
60
+ if (!isProviderSupported(flags.provider)) {
61
+ this.error(`Unsupported provider: ${flags.provider}. Use 'claude' or 'gemini'.`, { exit: 1 });
62
+ }
63
+ // Initialize services
64
+ this.initializeServices(flags.provider);
65
+ const prdPath = args.prdFile;
66
+ const relativePath = relative(process.cwd(), prdPath) || prdPath;
67
+ try {
68
+ // Check if file exists
69
+ const exists = await this.fileManager.fileExists(prdPath);
70
+ if (!exists) {
71
+ this.error(Colors.error(`PRD file not found: ${prdPath}`), { exit: 1 });
72
+ }
73
+ // Read the PRD content
74
+ const originalContent = await this.fileManager.readFile(prdPath);
75
+ // First, check if the PRD already parses correctly
76
+ this.log(Colors.info(`\nAnalyzing PRD: ${relativePath}`));
77
+ let needsFix = false;
78
+ try {
79
+ const epics = this.prdParser.parseEpics(originalContent, prdPath);
80
+ this.log(Colors.success(`\n✓ PRD already valid - found ${epics.length} epic(s)`));
81
+ this.log(Colors.dim(' No changes needed.\n'));
82
+ // Show the epics found
83
+ for (const epic of epics) {
84
+ this.log(Colors.dim(` • Epic ${epic.number}: ${epic.title}`));
85
+ }
86
+ this.log('');
87
+ return;
88
+ }
89
+ catch {
90
+ needsFix = true;
91
+ this.log(Colors.warning('\n⚠ PRD format issues detected - attempting auto-fix...'));
92
+ }
93
+ if (needsFix) {
94
+ // Attempt to fix the PRD
95
+ const references = flags.reference || [];
96
+ if (references.length > 0) {
97
+ this.log(Colors.dim(` Using ${references.length} reference file(s) for context`));
98
+ }
99
+ this.log(Colors.info(' Running AI fixer...'));
100
+ const result = await this.prdFixer.fixPrd(prdPath, originalContent, references);
101
+ if (result.fixed) {
102
+ this.log(Colors.success('\n✓ PRD fixed successfully!'));
103
+ this.log(Colors.dim(` Backup saved to: ${relativePath}.bak`));
104
+ // Validate the fixed content
105
+ this.validateFixedContent(result.content, prdPath);
106
+ }
107
+ else {
108
+ this.log(Colors.error('\n✗ Failed to fix PRD'));
109
+ this.log(Colors.error(` Error: ${result.error}\n`));
110
+ this.error('PRD fix failed', { exit: 1 });
111
+ }
112
+ }
113
+ }
114
+ catch (error) {
115
+ const err = error;
116
+ this.logger.error({ error: err.message, prdPath }, 'PRD fix command failed');
117
+ this.error(Colors.error(`Fix failed: ${err.message}`), { exit: 1 });
118
+ }
119
+ }
120
+ initializeServices(provider) {
121
+ this.fileManager = new FileManager(this.logger);
122
+ this.prdParser = new PrdParser(this.logger);
123
+ const agentRunner = createAgentRunner(provider, this.logger);
124
+ this.prdFixer = new PrdFixer(agentRunner, this.fileManager, this.logger);
125
+ }
126
+ validateFixedContent(content, prdPath) {
127
+ try {
128
+ const epics = this.prdParser.parseEpics(content, prdPath);
129
+ this.log(Colors.success(` Found ${epics.length} epic(s) after fix:\n`));
130
+ for (const epic of epics) {
131
+ this.log(Colors.dim(` • Epic ${epic.number}: ${epic.title}`));
132
+ }
133
+ this.log('');
134
+ }
135
+ catch {
136
+ this.log(Colors.warning('\n⚠ Fixed file still has parsing issues.'));
137
+ this.log(Colors.dim(' You may need to manually review the PRD.\n'));
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * PRD Validate Command
3
+ *
4
+ * Validates a PRD file and previews the epic/story extraction that would
5
+ * occur during a workflow run. This is a dry-run inspection tool that:
6
+ * - Parses the PRD to extract epic definitions
7
+ * - Checks for existing epic files
8
+ * - Counts stories in existing epic files
9
+ * - Reports what would be generated by the workflow command
10
+ *
11
+ * @example
12
+ * ```bash
13
+ * # Validate PRD and show extraction preview
14
+ * bmad-workflow prd validate docs/PRD-feature.md
15
+ *
16
+ * # Output as JSON for scripting
17
+ * bmad-workflow prd validate docs/PRD-feature.md --json
18
+ * ```
19
+ */
20
+ import { Command } from '@oclif/core';
21
+ /**
22
+ * PRD Validate Command
23
+ *
24
+ * Parses a PRD file and reports what epics and stories would be
25
+ * extracted by the workflow command.
26
+ */
27
+ export default class PrdValidate extends Command {
28
+ static args: {
29
+ prdFile: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
30
+ };
31
+ static description: string;
32
+ static examples: string[];
33
+ static flags: {
34
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
35
+ };
36
+ /**
37
+ * EpicParser instance for parsing story content
38
+ */
39
+ private epicParser;
40
+ /**
41
+ * FileManager instance for file operations
42
+ */
43
+ private fileManager;
44
+ /**
45
+ * Logger instance
46
+ */
47
+ private logger;
48
+ /**
49
+ * PathResolver instance for resolving paths
50
+ */
51
+ private pathResolver;
52
+ /**
53
+ * PrdParser instance for parsing PRD content
54
+ */
55
+ private prdParser;
56
+ /**
57
+ * Run the validation command
58
+ */
59
+ run(): Promise<void>;
60
+ /**
61
+ * Calculate summary statistics from validation results
62
+ *
63
+ * @param results - Array of epic validation results
64
+ * @returns Summary statistics
65
+ */
66
+ private calculateSummary;
67
+ /**
68
+ * Find epic file matching the epic number
69
+ *
70
+ * Searches the epics directory for files matching pattern:
71
+ * - epic-{N}-*.md
72
+ * - *-epic-{N}.md
73
+ * - *-epic-{N}-*.md
74
+ *
75
+ * @param epicNumber - Epic number to search for
76
+ * @returns Path to epic file if found, null otherwise
77
+ */
78
+ private findEpicFile;
79
+ /**
80
+ * Initialize service instances
81
+ */
82
+ private initializeServices;
83
+ /**
84
+ * Output validation results as JSON
85
+ *
86
+ * @param output - Complete validation output
87
+ */
88
+ private outputJson;
89
+ /**
90
+ * Output validation results as formatted table
91
+ *
92
+ * @param output - Complete validation output
93
+ */
94
+ private outputTable;
95
+ /**
96
+ * Truncate text with ellipsis if too long
97
+ *
98
+ * @param text - Text to truncate
99
+ * @param maxLength - Maximum length
100
+ * @returns Truncated text
101
+ */
102
+ private truncateText;
103
+ /**
104
+ * Validate a single epic
105
+ *
106
+ * Checks if the epic file exists and counts stories if it does.
107
+ *
108
+ * @param epic - Epic information from PRD
109
+ * @returns Validation result for the epic
110
+ */
111
+ private validateEpic;
112
+ }