@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,117 @@
1
+ /**
2
+ * Epic Parser Service
3
+ *
4
+ * Extracts story information from epic markdown files.
5
+ * Supports multiple story header formats for resilient parsing.
6
+ *
7
+ * Supported Formats:
8
+ * - ### Story 1.1: Initialize Project (full prefix with colon)
9
+ * - ### Story 1.1 - Initialize Project (full prefix with dash)
10
+ * - ### 1.1: Initialize Project (number only with colon)
11
+ * - ### 1.1. Initialize Project (number with period)
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const logger = createLogger({ namespace: 'parser' })
16
+ * const parser = new EpicParser(logger)
17
+ * const stories = parser.parseStories(epicContent, 'epic-1.md')
18
+ * ```
19
+ */
20
+ import type pino from 'pino';
21
+ import type { Story } from '../../models/story.js';
22
+ /**
23
+ * Epic Parser Service
24
+ *
25
+ * Parses epic markdown files to extract story information
26
+ * using multiple resilient patterns for different markdown formats.
27
+ */
28
+ export declare class EpicParser {
29
+ private readonly logger;
30
+ /**
31
+ * Story header patterns in order of specificity (most specific first)
32
+ */
33
+ private readonly patterns;
34
+ /**
35
+ * Create a new EpicParser instance
36
+ *
37
+ * @param logger - Logger instance for structured logging
38
+ */
39
+ constructor(logger: pino.Logger);
40
+ /**
41
+ * Extract epic number from filename
42
+ *
43
+ * Supports various filename formats:
44
+ * - `epic-2-parsing-services.md` → 2
45
+ * - `BMAD-ORCHESTRATOR-CLI-epic-2.md` → 2
46
+ * - `TENANT-LOGO-epic-1.md` → 1
47
+ *
48
+ * @param filename - Epic filename or path
49
+ * @returns Epic number extracted from filename
50
+ * @throws {ParserError} If no epic number can be extracted
51
+ * @example
52
+ * ```typescript
53
+ * const num = parser.parseEpicNumber('epic-2-parsing.md')
54
+ * // Returns: 2
55
+ * ```
56
+ */
57
+ parseEpicNumber(filename: string): number;
58
+ /**
59
+ * Parse stories from epic content
60
+ *
61
+ * Extracts story information using multiple pattern matching strategies.
62
+ * Returns stories sorted by their position in the document (natural order).
63
+ *
64
+ * Supported patterns:
65
+ * - `### Story 1.1: Initialize Project` (full prefix with colon)
66
+ * - `### Story 1.1 - Initialize Project` (full prefix with dash)
67
+ * - `### 1.1: Initialize Project` (number only with colon)
68
+ * - `### 1.1. Initialize Project` (number with period)
69
+ *
70
+ * @param epicContent - Full epic markdown content
71
+ * @param epicPath - Path to epic file (for error messages and epic number extraction)
72
+ * @returns Array of Story objects sorted by document position
73
+ * @throws {ParserError} When no Stories section is found or no stories are extracted
74
+ * @example
75
+ * ```typescript
76
+ * const stories = parser.parseStories(epicContent, 'docs/epics/epic-1-foundation.md')
77
+ * console.log(`Found ${stories.length} stories`)
78
+ * ```
79
+ */
80
+ parseStories(epicContent: string, epicPath: string): Story[];
81
+ /**
82
+ * Deduplicate matches that appear at the same position
83
+ *
84
+ * When multiple patterns match the same line, keep only the first match.
85
+ *
86
+ * @param matches - Array of story matches
87
+ * @returns Deduplicated array of Story objects
88
+ */
89
+ private deduplicateMatches;
90
+ /**
91
+ * Find the line number where a match appears
92
+ *
93
+ * @param lines - Array of lines from the document
94
+ * @param matchText - Text to find
95
+ * @returns Line number (0-indexed)
96
+ */
97
+ private findLineNumber;
98
+ /**
99
+ * Check if epic content has a Stories section
100
+ *
101
+ * @param content - Epic markdown content
102
+ * @returns True if Stories section exists
103
+ */
104
+ private hasStoriesSection;
105
+ /**
106
+ * Match a single pattern against epic content
107
+ *
108
+ * @param pattern - Pattern configuration with name and regex
109
+ * @param pattern.name - Pattern name
110
+ * @param pattern.regex - Pattern regular expression
111
+ * @param lines - Epic content split into lines
112
+ * @param content - Full epic content
113
+ * @param epicNumber - Epic number from filename
114
+ * @returns Array of matches for this pattern
115
+ */
116
+ private matchPattern;
117
+ }
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Epic Parser Service
3
+ *
4
+ * Extracts story information from epic markdown files.
5
+ * Supports multiple story header formats for resilient parsing.
6
+ *
7
+ * Supported Formats:
8
+ * - ### Story 1.1: Initialize Project (full prefix with colon)
9
+ * - ### Story 1.1 - Initialize Project (full prefix with dash)
10
+ * - ### 1.1: Initialize Project (number only with colon)
11
+ * - ### 1.1. Initialize Project (number with period)
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const logger = createLogger({ namespace: 'parser' })
16
+ * const parser = new EpicParser(logger)
17
+ * const stories = parser.parseStories(epicContent, 'epic-1.md')
18
+ * ```
19
+ */
20
+ import { ParserError } from '../../utils/errors.js';
21
+ /**
22
+ * Epic Parser Service
23
+ *
24
+ * Parses epic markdown files to extract story information
25
+ * using multiple resilient patterns for different markdown formats.
26
+ */
27
+ export class EpicParser {
28
+ logger;
29
+ /**
30
+ * Story header patterns in order of specificity (most specific first)
31
+ */
32
+ patterns = [
33
+ {
34
+ name: 'Story N.M: Title',
35
+ regex: /^###\s+Story\s+(\d+)\.(\d+)\s*:\s*(.+?)$/gim,
36
+ },
37
+ {
38
+ name: 'Story N.M - Title',
39
+ regex: /^###\s+Story\s+(\d+)\.(\d+)\s+-\s+(.+?)$/gim,
40
+ },
41
+ {
42
+ name: 'N.M: Title',
43
+ regex: /^###\s+(\d+)\.(\d+)\s*:\s*(.+?)$/gim,
44
+ },
45
+ {
46
+ name: 'N.M. Title',
47
+ regex: /^###\s+(\d+)\.(\d+)\s*\.\s+(.+?)$/gim,
48
+ },
49
+ ];
50
+ /**
51
+ * Create a new EpicParser instance
52
+ *
53
+ * @param logger - Logger instance for structured logging
54
+ */
55
+ constructor(logger) {
56
+ this.logger = logger;
57
+ }
58
+ /**
59
+ * Extract epic number from filename
60
+ *
61
+ * Supports various filename formats:
62
+ * - `epic-2-parsing-services.md` → 2
63
+ * - `BMAD-ORCHESTRATOR-CLI-epic-2.md` → 2
64
+ * - `TENANT-LOGO-epic-1.md` → 1
65
+ *
66
+ * @param filename - Epic filename or path
67
+ * @returns Epic number extracted from filename
68
+ * @throws {ParserError} If no epic number can be extracted
69
+ * @example
70
+ * ```typescript
71
+ * const num = parser.parseEpicNumber('epic-2-parsing.md')
72
+ * // Returns: 2
73
+ * ```
74
+ */
75
+ parseEpicNumber(filename) {
76
+ this.logger.debug({ filename }, 'Extracting epic number from filename');
77
+ // Extract just the filename if a path is provided
78
+ const basename = filename.split('/').pop() || filename;
79
+ // Pattern to match epic number: looks for "epic-N" or "epic N"
80
+ const epicPattern = /epic[-\s]?(\d+)/i;
81
+ const match = basename.match(epicPattern);
82
+ if (match && match[1]) {
83
+ const epicNumber = Number.parseInt(match[1], 10);
84
+ this.logger.debug({ epicNumber, filename }, 'Epic number extracted successfully');
85
+ return epicNumber;
86
+ }
87
+ // If no match, throw error
88
+ this.logger.error({ filename }, 'Could not extract epic number from filename');
89
+ throw new ParserError('Could not extract epic number from filename', {
90
+ filename,
91
+ suggestion: 'Epic filename should contain "epic-N" where N is the epic number',
92
+ });
93
+ }
94
+ /**
95
+ * Parse stories from epic content
96
+ *
97
+ * Extracts story information using multiple pattern matching strategies.
98
+ * Returns stories sorted by their position in the document (natural order).
99
+ *
100
+ * Supported patterns:
101
+ * - `### Story 1.1: Initialize Project` (full prefix with colon)
102
+ * - `### Story 1.1 - Initialize Project` (full prefix with dash)
103
+ * - `### 1.1: Initialize Project` (number only with colon)
104
+ * - `### 1.1. Initialize Project` (number with period)
105
+ *
106
+ * @param epicContent - Full epic markdown content
107
+ * @param epicPath - Path to epic file (for error messages and epic number extraction)
108
+ * @returns Array of Story objects sorted by document position
109
+ * @throws {ParserError} When no Stories section is found or no stories are extracted
110
+ * @example
111
+ * ```typescript
112
+ * const stories = parser.parseStories(epicContent, 'docs/epics/epic-1-foundation.md')
113
+ * console.log(`Found ${stories.length} stories`)
114
+ * ```
115
+ */
116
+ parseStories(epicContent, epicPath) {
117
+ this.logger.info({ epicPath }, 'Parsing stories from epic');
118
+ // Check if epic has a Stories section
119
+ if (!this.hasStoriesSection(epicContent)) {
120
+ this.logger.error({ epicPath }, 'No Stories section found in epic');
121
+ throw new ParserError('Epic file does not contain a Stories section. Expected section like: ## Stories', {
122
+ filePath: epicPath,
123
+ suggestion: 'Ensure epic has a stories section with headers like: ### Story 1.1: Title',
124
+ });
125
+ }
126
+ // Extract epic number from filename
127
+ const epicNumber = this.parseEpicNumber(epicPath);
128
+ this.logger.debug({ epicNumber, epicPath }, 'Extracted epic number from filename');
129
+ // Split content into lines for position tracking
130
+ const lines = epicContent.split('\n');
131
+ // Try each pattern and collect all matches
132
+ const allMatches = [];
133
+ for (const pattern of this.patterns) {
134
+ const matches = this.matchPattern(pattern, lines, epicContent, epicNumber);
135
+ allMatches.push(...matches);
136
+ if (matches.length > 0) {
137
+ this.logger.debug({ matches: matches.length, patternName: pattern.name }, 'Pattern matched stories');
138
+ }
139
+ }
140
+ // Check if any stories were found
141
+ if (allMatches.length === 0) {
142
+ this.logger.error({ epicPath }, 'No stories found in epic');
143
+ throw new ParserError('No stories found in epic. Expected format: `### Story 1.1: Title` or similar', {
144
+ filePath: epicPath,
145
+ suggestion: 'Ensure epic has story headers like: ### Story 1.1: Initialize Project',
146
+ });
147
+ }
148
+ // Sort by position (natural document order)
149
+ allMatches.sort((a, b) => a.position - b.position);
150
+ // Convert to Story interface and deduplicate by position
151
+ const stories = this.deduplicateMatches(allMatches);
152
+ this.logger.info({ epicPath, storyCount: stories.length }, 'Successfully parsed stories from epic');
153
+ return stories;
154
+ }
155
+ /**
156
+ * Deduplicate matches that appear at the same position
157
+ *
158
+ * When multiple patterns match the same line, keep only the first match.
159
+ *
160
+ * @param matches - Array of story matches
161
+ * @returns Deduplicated array of Story objects
162
+ */
163
+ deduplicateMatches(matches) {
164
+ const seenPositions = new Set();
165
+ const stories = [];
166
+ for (const match of matches) {
167
+ // Skip if we've already seen this position
168
+ if (seenPositions.has(match.position)) {
169
+ this.logger.debug({
170
+ fullNumber: match.fullNumber,
171
+ line: match.position + 1,
172
+ patternName: match.patternName,
173
+ }, 'Skipping duplicate match at same position');
174
+ continue;
175
+ }
176
+ seenPositions.add(match.position);
177
+ stories.push({
178
+ epicNumber: match.epicNumber,
179
+ fullNumber: match.fullNumber,
180
+ number: match.number,
181
+ title: match.title,
182
+ });
183
+ this.logger.debug({
184
+ fullNumber: match.fullNumber,
185
+ line: match.position + 1,
186
+ patternName: match.patternName,
187
+ title: match.title,
188
+ }, 'Story matched successfully');
189
+ }
190
+ return stories;
191
+ }
192
+ /**
193
+ * Find the line number where a match appears
194
+ *
195
+ * @param lines - Array of lines from the document
196
+ * @param matchText - Text to find
197
+ * @returns Line number (0-indexed)
198
+ */
199
+ findLineNumber(lines, matchText) {
200
+ for (const [i, line] of lines.entries()) {
201
+ if (line.includes(matchText.trim())) {
202
+ return i;
203
+ }
204
+ }
205
+ return 0; // Fallback to start of document
206
+ }
207
+ /**
208
+ * Check if epic content has a Stories section
209
+ *
210
+ * @param content - Epic markdown content
211
+ * @returns True if Stories section exists
212
+ */
213
+ hasStoriesSection(content) {
214
+ // Look for Stories section header (## Stories or similar)
215
+ const storiesSectionPattern = /^##\s+Stories?\s*$/im;
216
+ return storiesSectionPattern.test(content);
217
+ }
218
+ /**
219
+ * Match a single pattern against epic content
220
+ *
221
+ * @param pattern - Pattern configuration with name and regex
222
+ * @param pattern.name - Pattern name
223
+ * @param pattern.regex - Pattern regular expression
224
+ * @param lines - Epic content split into lines
225
+ * @param content - Full epic content
226
+ * @param epicNumber - Epic number from filename
227
+ * @returns Array of matches for this pattern
228
+ */
229
+ matchPattern(pattern, lines, content, epicNumber) {
230
+ const matches = [];
231
+ const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
232
+ let match;
233
+ // Reset regex state
234
+ regex.lastIndex = 0;
235
+ // Execute regex and collect all matches
236
+ while ((match = regex.exec(content)) !== null) {
237
+ // Extract epic number and story number from groups
238
+ // Groups are: [fullMatch, epicNum, storyNum, title]
239
+ const matchedEpicNum = Number.parseInt(match[1], 10);
240
+ const storyNum = Number.parseInt(match[2], 10);
241
+ const storyTitle = match[3].trim();
242
+ const fullMatch = match[0];
243
+ // Verify epic number matches (warn if different)
244
+ if (matchedEpicNum !== epicNumber) {
245
+ this.logger.warn({
246
+ expected: epicNumber,
247
+ found: matchedEpicNum,
248
+ storyTitle,
249
+ }, 'Story epic number does not match filename epic number');
250
+ }
251
+ // Find line number of this match
252
+ const position = this.findLineNumber(lines, fullMatch);
253
+ matches.push({
254
+ epicNumber: matchedEpicNum,
255
+ fullNumber: `${matchedEpicNum}.${storyNum}`,
256
+ number: storyNum,
257
+ patternName: pattern.name,
258
+ position,
259
+ title: storyTitle,
260
+ });
261
+ }
262
+ return matches;
263
+ }
264
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * PRD Fixer Service
3
+ *
4
+ * Uses AI to automatically reformat PRD documents that don't match
5
+ * the expected epic/story format. This allows the workflow to handle
6
+ * PRDs written in various formats by normalizing them before parsing.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const fixer = new PrdFixer(agentRunner, fileManager, logger)
11
+ * const result = await fixer.fixPrd('docs/PRD.md', content, ['docs/arch.md'])
12
+ * if (result.fixed) {
13
+ * // PRD was reformatted and saved
14
+ * }
15
+ * ```
16
+ */
17
+ import type pino from 'pino';
18
+ import type { AIProviderRunner } from '../agents/agent-runner.js';
19
+ import { FileManager } from '../file-system/file-manager.js';
20
+ /**
21
+ * Result of a PRD fix attempt
22
+ */
23
+ export interface PrdFixResult {
24
+ /**
25
+ * The content after fixing (or original if not fixed)
26
+ */
27
+ content: string;
28
+ /**
29
+ * Error message if fixing failed
30
+ */
31
+ error?: string;
32
+ /**
33
+ * Whether the PRD was successfully fixed
34
+ */
35
+ fixed: boolean;
36
+ }
37
+ /**
38
+ * PRD Fixer Service
39
+ *
40
+ * Automatically reformats PRD documents to match expected epic/story patterns
41
+ * using AI. Creates a backup before modifying the original file.
42
+ */
43
+ export declare class PrdFixer {
44
+ private readonly agentRunner;
45
+ private readonly fileManager;
46
+ private readonly logger;
47
+ /**
48
+ * Create a new PrdFixer instance
49
+ *
50
+ * @param agentRunner - AI provider runner for executing fix prompts
51
+ * @param fileManager - File manager for reading/writing files
52
+ * @param logger - Logger instance for structured logging
53
+ */
54
+ constructor(agentRunner: AIProviderRunner, fileManager: FileManager, logger: pino.Logger);
55
+ /**
56
+ * Fix a PRD document to match expected format
57
+ *
58
+ * Uses AI to analyze and reformat the PRD content to match the expected
59
+ * epic/story structure. Creates a backup of the original file before
60
+ * overwriting with the fixed content.
61
+ *
62
+ * @param prdPath - Path to the PRD file
63
+ * @param originalContent - Original PRD content that failed parsing
64
+ * @param references - Optional reference files for context (e.g., architecture docs)
65
+ * @returns PrdFixResult with fixed status, content, and any errors
66
+ */
67
+ fixPrd(prdPath: string, originalContent: string, references?: string[]): Promise<PrdFixResult>;
68
+ /**
69
+ * Build the prompt for fixing the PRD
70
+ *
71
+ * @param prdPath - Path to the PRD file
72
+ * @param content - Original PRD content
73
+ * @param references - Optional reference files
74
+ * @returns Complete prompt string for the AI agent
75
+ */
76
+ private buildFixPrompt;
77
+ /**
78
+ * Extract the fixed PRD content from the AI response
79
+ *
80
+ * Looks for content wrapped in markdown code blocks and extracts it.
81
+ *
82
+ * @param output - Raw AI response output
83
+ * @returns Extracted PRD content or null if not found
84
+ */
85
+ private extractFixedContent;
86
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * PRD Fixer Service
3
+ *
4
+ * Uses AI to automatically reformat PRD documents that don't match
5
+ * the expected epic/story format. This allows the workflow to handle
6
+ * PRDs written in various formats by normalizing them before parsing.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const fixer = new PrdFixer(agentRunner, fileManager, logger)
11
+ * const result = await fixer.fixPrd('docs/PRD.md', content, ['docs/arch.md'])
12
+ * if (result.fixed) {
13
+ * // PRD was reformatted and saved
14
+ * }
15
+ * ```
16
+ */
17
+ /**
18
+ * PRD Fixer Service
19
+ *
20
+ * Automatically reformats PRD documents to match expected epic/story patterns
21
+ * using AI. Creates a backup before modifying the original file.
22
+ */
23
+ export class PrdFixer {
24
+ agentRunner;
25
+ fileManager;
26
+ logger;
27
+ /**
28
+ * Create a new PrdFixer instance
29
+ *
30
+ * @param agentRunner - AI provider runner for executing fix prompts
31
+ * @param fileManager - File manager for reading/writing files
32
+ * @param logger - Logger instance for structured logging
33
+ */
34
+ constructor(agentRunner, fileManager, logger) {
35
+ this.agentRunner = agentRunner;
36
+ this.fileManager = fileManager;
37
+ this.logger = logger;
38
+ }
39
+ /**
40
+ * Fix a PRD document to match expected format
41
+ *
42
+ * Uses AI to analyze and reformat the PRD content to match the expected
43
+ * epic/story structure. Creates a backup of the original file before
44
+ * overwriting with the fixed content.
45
+ *
46
+ * @param prdPath - Path to the PRD file
47
+ * @param originalContent - Original PRD content that failed parsing
48
+ * @param references - Optional reference files for context (e.g., architecture docs)
49
+ * @returns PrdFixResult with fixed status, content, and any errors
50
+ */
51
+ async fixPrd(prdPath, originalContent, references) {
52
+ this.logger.info({ prdPath, references: references?.length ?? 0 }, 'Attempting to auto-fix PRD format');
53
+ try {
54
+ // Build the prompt for fixing the PRD
55
+ const prompt = this.buildFixPrompt(prdPath, originalContent, references);
56
+ // Execute the AI agent to fix the PRD
57
+ const result = await this.agentRunner.runAgent(prompt, {
58
+ agentType: 'prd-fixer',
59
+ timeout: 300_000, // 5 minutes should be enough for reformatting
60
+ });
61
+ if (!result.success) {
62
+ this.logger.error({ errors: result.errors, prdPath }, 'AI agent failed to fix PRD');
63
+ return {
64
+ content: originalContent,
65
+ error: `AI agent failed: ${result.errors}`,
66
+ fixed: false,
67
+ };
68
+ }
69
+ // Extract the fixed content from the AI response
70
+ const fixedContent = this.extractFixedContent(result.output);
71
+ if (!fixedContent) {
72
+ this.logger.error({ prdPath }, 'Could not extract fixed content from AI response');
73
+ return {
74
+ content: originalContent,
75
+ error: 'Could not extract fixed PRD content from AI response',
76
+ fixed: false,
77
+ };
78
+ }
79
+ // Create backup of original file
80
+ const backupPath = `${prdPath}.bak`;
81
+ await this.fileManager.writeFile(backupPath, originalContent);
82
+ this.logger.info({ backupPath }, 'Created backup of original PRD');
83
+ // Write the fixed content to the original path
84
+ await this.fileManager.writeFile(prdPath, fixedContent);
85
+ this.logger.info({ prdPath }, 'Wrote fixed PRD content');
86
+ return {
87
+ content: fixedContent,
88
+ fixed: true,
89
+ };
90
+ }
91
+ catch (error) {
92
+ const err = error;
93
+ this.logger.error({ error: err.message, prdPath }, 'Failed to fix PRD');
94
+ return {
95
+ content: originalContent,
96
+ error: err.message,
97
+ fixed: false,
98
+ };
99
+ }
100
+ }
101
+ /**
102
+ * Build the prompt for fixing the PRD
103
+ *
104
+ * @param prdPath - Path to the PRD file
105
+ * @param content - Original PRD content
106
+ * @param references - Optional reference files
107
+ * @returns Complete prompt string for the AI agent
108
+ */
109
+ buildFixPrompt(prdPath, content, references) {
110
+ // Build reference context if provided
111
+ let referenceContext = '';
112
+ if (references && references.length > 0) {
113
+ referenceContext = `
114
+
115
+ REFERENCE FILES FOR CONTEXT:
116
+ ${references.map((ref) => `- ${ref}`).join('\n')}
117
+
118
+ Read these reference files to understand the project context and patterns.`;
119
+ }
120
+ return `You are a PRD formatting expert. Your task is to reformat a Product Requirements Document (PRD) to match the expected structure for automated workflow processing.
121
+
122
+ TASK: Reformat the PRD file at "${prdPath}" to use the correct epic and story format.
123
+
124
+ EXPECTED FORMAT:
125
+ 1. Epic headers MUST use this exact format: ## Epic N: Title
126
+ - Examples: "## Epic 1: Foundation & Setup", "## Epic 2: Core Features"
127
+ - The "Epic" keyword is REQUIRED
128
+ - The number MUST be sequential (1, 2, 3...)
129
+ - A colon separates the number from the title
130
+
131
+ 2. Story sections within epics should use:
132
+ - ### Stories (as a section header)
133
+ - Bullet points: - Story N.M: Description
134
+ - Example: "- Story 1.1: Initialize project structure"
135
+
136
+ 3. Each epic should have:
137
+ - A description paragraph after the header
138
+ - A ### Goals section with bullet points
139
+ - A ### Stories section with story bullet points
140
+
141
+ CURRENT PRD CONTENT:
142
+ \`\`\`markdown
143
+ ${content}
144
+ \`\`\`
145
+ ${referenceContext}
146
+
147
+ INSTRUCTIONS:
148
+ 1. Analyze the current PRD structure
149
+ 2. Identify all epics/features/phases (they might be labeled differently)
150
+ 3. Reformat them to use "## Epic N: Title" format
151
+ 4. Ensure stories are properly formatted under each epic
152
+ 5. Preserve ALL original content and meaning - only change the formatting
153
+ 6. Number epics sequentially starting from 1
154
+ 7. Number stories as N.M where N is the epic number and M is the story number within that epic
155
+
156
+ OUTPUT FORMAT:
157
+ Output ONLY the reformatted PRD content wrapped in a markdown code block.
158
+ Do not include any explanation before or after the code block.
159
+ The output must be a complete, valid markdown document.
160
+
161
+ \`\`\`markdown
162
+ [Your reformatted PRD here]
163
+ \`\`\``;
164
+ }
165
+ /**
166
+ * Extract the fixed PRD content from the AI response
167
+ *
168
+ * Looks for content wrapped in markdown code blocks and extracts it.
169
+ *
170
+ * @param output - Raw AI response output
171
+ * @returns Extracted PRD content or null if not found
172
+ */
173
+ extractFixedContent(output) {
174
+ // Try to extract content from markdown code block
175
+ const codeBlockMatch = output.match(/```(?:markdown)?\s*\n([\s\S]*?)\n```/);
176
+ if (codeBlockMatch && codeBlockMatch[1]) {
177
+ const extracted = codeBlockMatch[1].trim();
178
+ // Validate it looks like a PRD (has at least one epic header)
179
+ if (/##\s+Epic\s+\d+/i.test(extracted)) {
180
+ return extracted;
181
+ }
182
+ }
183
+ // Fallback: if the output itself looks like a valid PRD
184
+ if (/##\s+Epic\s+\d+/i.test(output)) {
185
+ // Strip any leading/trailing explanation text
186
+ const lines = output.split('\n');
187
+ const prdStart = lines.findIndex((line) => line.startsWith('#'));
188
+ if (prdStart !== -1) {
189
+ return lines.slice(prdStart).join('\n').trim();
190
+ }
191
+ }
192
+ return null;
193
+ }
194
+ }