@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,255 @@
1
+ /**
2
+ * StandaloneStoryParser Service
3
+ *
4
+ * Parses standalone story files that don't follow the epic.story numbering pattern.
5
+ * Supports stories like JIRA tickets, bug fixes, and ad-hoc tasks.
6
+ *
7
+ * Examples:
8
+ * - JIRA-SIGN-10.md
9
+ * - bugfix-auth-timeout.md
10
+ * - feature-dark-mode.md
11
+ */
12
+ import { basename } from 'node:path';
13
+ import { ParserError } from '../../utils/errors.js';
14
+ /**
15
+ * StandaloneStoryParser service for parsing non-epic stories
16
+ *
17
+ * Provides methods to parse standalone story files that don't follow
18
+ * the epic.story numbering convention. Uses filename as unique ID and
19
+ * optionally extracts category/prefix.
20
+ */
21
+ export class StandaloneStoryParser {
22
+ /**
23
+ * FileManager instance for file operations
24
+ */
25
+ fileManager;
26
+ /**
27
+ * Logger instance for parsing operations
28
+ */
29
+ logger;
30
+ /**
31
+ * Create a new StandaloneStoryParser instance
32
+ *
33
+ * @param fileManager - FileManager service for file operations
34
+ * @param logger - Pino logger instance for logging parsing operations
35
+ * @example
36
+ * const logger = createLogger({ namespace: 'services:parsers:standalone-story' })
37
+ * const fileManager = new FileManager(logger)
38
+ * const parser = new StandaloneStoryParser(fileManager, logger)
39
+ */
40
+ constructor(fileManager, logger) {
41
+ this.fileManager = fileManager;
42
+ this.logger = logger;
43
+ }
44
+ /**
45
+ * Parse standalone story metadata from a story file
46
+ *
47
+ * Extracts ID from filename, optional category/prefix, title from H1 header,
48
+ * and status from either inline or section-based format. Does not require
49
+ * epic.story numbering pattern.
50
+ *
51
+ * @param storyPath - Path to the story markdown file
52
+ * @returns Standalone story metadata with ID, title, status, and optional category
53
+ * @throws If file cannot be read
54
+ * @throws {ParserError} If story structure is invalid (missing title, missing status)
55
+ * @example
56
+ * const metadata = await parser.parseStoryMetadata('docs/stories/JIRA-SIGN-10.md')
57
+ * console.log(metadata.id) // "JIRA-SIGN-10"
58
+ * console.log(metadata.category) // "JIRA-SIGN"
59
+ * console.log(metadata.type) // "standalone"
60
+ */
61
+ async parseStoryMetadata(storyPath) {
62
+ this.logger.info('Parsing standalone story metadata from: %s', storyPath);
63
+ // Read story file content
64
+ const content = await this.fileManager.readFile(storyPath);
65
+ // Extract ID and optional category from filename
66
+ const { category, id } = this.extractIdAndCategory(storyPath);
67
+ // Extract title from H1 header
68
+ const title = this.extractTitle(content, storyPath);
69
+ // Extract status using multiple pattern strategies
70
+ const status = this.extractStatus(content, storyPath);
71
+ this.logger.info('Successfully parsed standalone story metadata: %s (Category: %s, Status: %s)', id, category || 'none', status);
72
+ return {
73
+ category,
74
+ filePath: storyPath,
75
+ id,
76
+ status,
77
+ title,
78
+ type: 'standalone',
79
+ };
80
+ }
81
+ /**
82
+ * Update story status in a standalone story file
83
+ *
84
+ * Reads the story file, detects the current status format (inline or section-based),
85
+ * and updates the status value while preserving the original format.
86
+ *
87
+ * @param storyPath - Path to the story markdown file
88
+ * @param newStatus - New status value to set
89
+ * @throws If file cannot be read or written
90
+ * @throws {ParserError} If status format cannot be detected
91
+ * @example
92
+ * await parser.updateStoryStatus('docs/stories/JIRA-SIGN-10.md', 'Done')
93
+ */
94
+ async updateStoryStatus(storyPath, newStatus) {
95
+ this.logger.info('Updating standalone story status in %s to: %s', storyPath, newStatus);
96
+ // Read current story file content
97
+ const content = await this.fileManager.readFile(storyPath);
98
+ // Detect current status and format
99
+ const { format, oldStatus } = this.detectStatusFormat(content, storyPath);
100
+ // Update status based on detected format
101
+ let updatedContent;
102
+ if (format === 'inline') {
103
+ updatedContent = content.replace(/\*\*Status\*\*:\s*\w+/, `**Status**: ${newStatus}`);
104
+ }
105
+ else if (format === 'section-bold') {
106
+ updatedContent = content.replace(/^## Status\s*\n+\s*\*\*\w+\*\*/m, `## Status\n\n**${newStatus}**`);
107
+ }
108
+ else {
109
+ updatedContent = content.replace(/^## Status\s*\n+\s*\w+/m, `## Status\n\n${newStatus}`);
110
+ }
111
+ // Write updated content back to file
112
+ await this.fileManager.writeFile(storyPath, updatedContent);
113
+ this.logger.info('Standalone story status updated successfully: %s → %s (format: %s)', oldStatus, newStatus, format);
114
+ }
115
+ /**
116
+ * Detect status format (inline vs section-based) and extract current status
117
+ *
118
+ * @param content - Story file content
119
+ * @param storyPath - Path to story file (for error context)
120
+ * @returns Object with format type and old status value
121
+ * @throws {ParserError} If status format cannot be detected
122
+ */
123
+ detectStatusFormat(content, storyPath) {
124
+ // Check for inline format: **Status**: Draft
125
+ const inlineMatch = /\*\*Status\*\*:\s*(\w+)/.exec(content);
126
+ if (inlineMatch) {
127
+ return { format: 'inline', oldStatus: inlineMatch[1].trim() };
128
+ }
129
+ // Check for section with bold: ## Status\n\n**Draft**
130
+ const boldSectionMatch = /^## Status\s*\n+\s*\*\*(\w+)\*\*/m.exec(content);
131
+ if (boldSectionMatch) {
132
+ return { format: 'section-bold', oldStatus: boldSectionMatch[1].trim() };
133
+ }
134
+ // Check for section plain: ## Status\n\nDraft or ## Status\nDraft
135
+ const plainSectionMatch = /^## Status\s*\n+\s*(?!\*\*)(\w+)/m.exec(content);
136
+ if (plainSectionMatch) {
137
+ return { format: 'section', oldStatus: plainSectionMatch[1].trim() };
138
+ }
139
+ // Status format not detected
140
+ this.logger.error('Unable to detect status format in: %s', storyPath);
141
+ throw new ParserError('Unable to detect status format', {
142
+ filePath: storyPath,
143
+ suggestion: 'Ensure story has valid Status section in one of the supported formats',
144
+ });
145
+ }
146
+ /**
147
+ * Extract unique ID and optional category from filename
148
+ *
149
+ * Parses filename to create a unique ID (filename without extension)
150
+ * and optionally extracts category prefix. Supports patterns like:
151
+ * - JIRA-SIGN-10.md → id: "JIRA-SIGN-10", category: "JIRA-SIGN"
152
+ * - bugfix-auth.md → id: "bugfix-auth", category: "bugfix"
153
+ * - simple-task.md → id: "simple-task", category: undefined
154
+ *
155
+ * @param storyPath - Path to the story file
156
+ * @returns Object with id and optional category
157
+ */
158
+ extractIdAndCategory(storyPath) {
159
+ const filename = basename(storyPath, '.md');
160
+ // Try to extract category from common patterns:
161
+ // 1. PREFIX-NUMBER (e.g., JIRA-SIGN-10 → category: JIRA-SIGN)
162
+ // 2. PREFIX-TYPE-word (e.g., HTTP-TENANT-FIX → category: HTTP-TENANT)
163
+ // 3. word-word-... (e.g., bugfix-auth → category: bugfix)
164
+ // Pattern 1: PREFIX-PREFIX-NUMBER (e.g., JIRA-SIGN-10)
165
+ const prefixNumberMatch = /^([A-Z]+-[A-Z]+)-\d+$/.exec(filename);
166
+ if (prefixNumberMatch) {
167
+ this.logger.debug('Extracted category (prefix-number): %s', prefixNumberMatch[1]);
168
+ return { category: prefixNumberMatch[1], id: filename };
169
+ }
170
+ // Pattern 2: PREFIX-TYPE-word (e.g., HTTP-TENANT-FIX-2)
171
+ const multiPrefixMatch = /^([A-Z]+-[A-Z]+(?:-[A-Z]+)?)-/.exec(filename);
172
+ if (multiPrefixMatch) {
173
+ this.logger.debug('Extracted category (multi-prefix): %s', multiPrefixMatch[1]);
174
+ return { category: multiPrefixMatch[1], id: filename };
175
+ }
176
+ // Pattern 3: Known lowercase category prefixes (e.g., bugfix-auth, feature-dark-mode)
177
+ // Only match specific known categories to avoid false positives with generic names
178
+ const knownCategoryPrefixes = ['bugfix', 'bug', 'feature', 'hotfix', 'chore', 'refactor', 'docs', 'test', 'fix'];
179
+ const singlePrefixMatch = /^([a-z]+)-/.exec(filename);
180
+ if (singlePrefixMatch && knownCategoryPrefixes.includes(singlePrefixMatch[1])) {
181
+ this.logger.debug('Extracted category (known-prefix): %s', singlePrefixMatch[1]);
182
+ return { category: singlePrefixMatch[1], id: filename };
183
+ }
184
+ // No category detected
185
+ this.logger.debug('No category detected, using filename as ID: %s', filename);
186
+ return { id: filename };
187
+ }
188
+ /**
189
+ * Extract story status using multiple pattern strategies
190
+ *
191
+ * @param content - Story file content
192
+ * @param storyPath - Path to story file (for error context)
193
+ * @returns Extracted status value
194
+ * @throws {ParserError} If status cannot be found
195
+ */
196
+ extractStatus(content, storyPath) {
197
+ // Pattern 1: Inline format - **Status**: Draft
198
+ const inlineMatch = /\*\*Status\*\*:\s*(\w+)/.exec(content);
199
+ if (inlineMatch) {
200
+ const status = inlineMatch[1].trim();
201
+ this.logger.debug('Extracted status (inline format): %s', status);
202
+ return status;
203
+ }
204
+ // Pattern 2: Section with bold - ## Status\n\n**Draft**
205
+ const boldSectionMatch = /^## Status\s*\n+\s*\*\*(\w+)\*\*/m.exec(content);
206
+ if (boldSectionMatch) {
207
+ const status = boldSectionMatch[1].trim();
208
+ this.logger.debug('Extracted status (section bold format): %s', status);
209
+ return status;
210
+ }
211
+ // Pattern 3: Section plain - ## Status\n\nDraft
212
+ const plainSectionMatch = /^## Status\s*\n+\s*(?!\*\*)(\w+)/m.exec(content);
213
+ if (plainSectionMatch) {
214
+ const status = plainSectionMatch[1].trim();
215
+ this.logger.debug('Extracted status (section plain format): %s', status);
216
+ return status;
217
+ }
218
+ // Pattern 4: Fallback - any text after ## Status
219
+ const fallbackMatch = /^## Status\s*\n+\s*(.+?)(?:\n|$)/m.exec(content);
220
+ if (fallbackMatch) {
221
+ const rawStatus = fallbackMatch[1].replaceAll('**', '').replaceAll(/[_*]/g, '').replaceAll('---', '').trim();
222
+ if (rawStatus && /^\w+$/.test(rawStatus)) {
223
+ const status = rawStatus;
224
+ this.logger.debug('Extracted status (fallback format): %s', status);
225
+ return status;
226
+ }
227
+ }
228
+ // Status not found
229
+ this.logger.error('Story file missing Status section or field: %s', storyPath);
230
+ throw new ParserError('Story file missing Status section or field', {
231
+ filePath: storyPath,
232
+ suggestion: 'Ensure story has "## Status" section followed by status value',
233
+ });
234
+ }
235
+ /**
236
+ * Extract story title from H1 header
237
+ *
238
+ * @param content - Story file content
239
+ * @param storyPath - Path to story file (for error context)
240
+ * @returns Extracted title
241
+ * @throws {ParserError} If H1 header is not found
242
+ */
243
+ extractTitle(content, storyPath) {
244
+ const match = /^# (.+)$/m.exec(content);
245
+ if (!match) {
246
+ this.logger.error('Story file missing H1 title header: %s', storyPath);
247
+ throw new ParserError('Story file missing H1 title header', {
248
+ filePath: storyPath,
249
+ });
250
+ }
251
+ const title = match[1].trim();
252
+ this.logger.debug('Extracted title: %s', title);
253
+ return title;
254
+ }
255
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * StoryParserFactory
3
+ *
4
+ * Factory for creating appropriate story parser based on file naming pattern.
5
+ * Automatically detects whether a story follows epic.story numbering or is standalone.
6
+ */
7
+ import type pino from 'pino';
8
+ import type { StoryMetadata } from '../../models/story.js';
9
+ import type { FileManager } from '../file-system/file-manager.js';
10
+ /**
11
+ * Factory for creating story parsers
12
+ *
13
+ * Determines which parser to use based on filename pattern and provides
14
+ * a unified interface for parsing both epic-based and standalone stories.
15
+ */
16
+ export declare class StoryParserFactory {
17
+ /**
18
+ * Cached parser instances
19
+ */
20
+ private epicParser?;
21
+ /**
22
+ * FileManager instance for file operations
23
+ */
24
+ private readonly fileManager;
25
+ /**
26
+ * Logger instance
27
+ */
28
+ private readonly logger;
29
+ private standaloneParser?;
30
+ /**
31
+ * Create a new StoryParserFactory
32
+ *
33
+ * @param fileManager - FileManager service
34
+ * @param logger - Pino logger instance
35
+ */
36
+ constructor(fileManager: FileManager, logger: pino.Logger);
37
+ /**
38
+ * Determine if a story file is epic-based
39
+ *
40
+ * Checks filename for epic.story numbering pattern (e.g., 2.3, 1.5).
41
+ * Returns true if pattern is found, false otherwise.
42
+ *
43
+ * @param storyPath - Path to story file
44
+ * @returns True if epic-based, false if standalone
45
+ * @example
46
+ * factory.isEpicBasedStory('BMAD-2.3-story.md') // true
47
+ * factory.isEpicBasedStory('JIRA-SIGN-10.md') // false
48
+ * factory.isEpicBasedStory('bugfix-auth.md') // false
49
+ */
50
+ isEpicBasedStory(storyPath: string): boolean;
51
+ /**
52
+ * Parse story metadata from any story file
53
+ *
54
+ * Automatically detects story type and uses appropriate parser.
55
+ * Returns unified StoryMetadata union type with discriminator.
56
+ *
57
+ * @param storyPath - Path to story markdown file
58
+ * @returns StoryMetadata (EpicStoryMetadata | StandaloneStoryMetadata)
59
+ * @example
60
+ * // Epic story
61
+ * const epic = await factory.parseStory('BMAD-2.3-story.md')
62
+ * if (epic.type === 'epic-based') {
63
+ * console.log(epic.epicNumber, epic.storyNumber)
64
+ * }
65
+ *
66
+ * // Standalone story
67
+ * const standalone = await factory.parseStory('JIRA-SIGN-10.md')
68
+ * if (standalone.type === 'standalone') {
69
+ * console.log(standalone.category)
70
+ * }
71
+ */
72
+ parseStory(storyPath: string): Promise<StoryMetadata>;
73
+ /**
74
+ * Get or create epic story parser instance
75
+ */
76
+ private getEpicParser;
77
+ /**
78
+ * Get or create standalone story parser instance
79
+ */
80
+ private getStandaloneParser;
81
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * StoryParserFactory
3
+ *
4
+ * Factory for creating appropriate story parser based on file naming pattern.
5
+ * Automatically detects whether a story follows epic.story numbering or is standalone.
6
+ */
7
+ import { basename } from 'node:path';
8
+ import { StandaloneStoryParser } from './standalone-story-parser.js';
9
+ import { StoryParser } from './story-parser.js';
10
+ /**
11
+ * Factory for creating story parsers
12
+ *
13
+ * Determines which parser to use based on filename pattern and provides
14
+ * a unified interface for parsing both epic-based and standalone stories.
15
+ */
16
+ export class StoryParserFactory {
17
+ /**
18
+ * Cached parser instances
19
+ */
20
+ epicParser;
21
+ /**
22
+ * FileManager instance for file operations
23
+ */
24
+ fileManager;
25
+ /**
26
+ * Logger instance
27
+ */
28
+ logger;
29
+ standaloneParser;
30
+ /**
31
+ * Create a new StoryParserFactory
32
+ *
33
+ * @param fileManager - FileManager service
34
+ * @param logger - Pino logger instance
35
+ */
36
+ constructor(fileManager, logger) {
37
+ this.fileManager = fileManager;
38
+ this.logger = logger;
39
+ }
40
+ /**
41
+ * Determine if a story file is epic-based
42
+ *
43
+ * Checks filename for epic.story numbering pattern (e.g., 2.3, 1.5).
44
+ * Returns true if pattern is found, false otherwise.
45
+ *
46
+ * @param storyPath - Path to story file
47
+ * @returns True if epic-based, false if standalone
48
+ * @example
49
+ * factory.isEpicBasedStory('BMAD-2.3-story.md') // true
50
+ * factory.isEpicBasedStory('JIRA-SIGN-10.md') // false
51
+ * factory.isEpicBasedStory('bugfix-auth.md') // false
52
+ */
53
+ isEpicBasedStory(storyPath) {
54
+ const filename = basename(storyPath);
55
+ const hasEpicStoryPattern = /\d+\.\d+/.test(filename);
56
+ this.logger.debug('Checking if %s is epic-based: %s', filename, hasEpicStoryPattern ? 'yes' : 'no');
57
+ return hasEpicStoryPattern;
58
+ }
59
+ /**
60
+ * Parse story metadata from any story file
61
+ *
62
+ * Automatically detects story type and uses appropriate parser.
63
+ * Returns unified StoryMetadata union type with discriminator.
64
+ *
65
+ * @param storyPath - Path to story markdown file
66
+ * @returns StoryMetadata (EpicStoryMetadata | StandaloneStoryMetadata)
67
+ * @example
68
+ * // Epic story
69
+ * const epic = await factory.parseStory('BMAD-2.3-story.md')
70
+ * if (epic.type === 'epic-based') {
71
+ * console.log(epic.epicNumber, epic.storyNumber)
72
+ * }
73
+ *
74
+ * // Standalone story
75
+ * const standalone = await factory.parseStory('JIRA-SIGN-10.md')
76
+ * if (standalone.type === 'standalone') {
77
+ * console.log(standalone.category)
78
+ * }
79
+ */
80
+ async parseStory(storyPath) {
81
+ if (this.isEpicBasedStory(storyPath)) {
82
+ this.logger.debug('Detected epic-based story: %s', storyPath);
83
+ const parser = this.getEpicParser();
84
+ return parser.parseStoryMetadata(storyPath);
85
+ }
86
+ this.logger.debug('Detected standalone story: %s', storyPath);
87
+ const parser = this.getStandaloneParser();
88
+ return parser.parseStoryMetadata(storyPath);
89
+ }
90
+ /**
91
+ * Get or create epic story parser instance
92
+ */
93
+ getEpicParser() {
94
+ if (!this.epicParser) {
95
+ this.epicParser = new StoryParser(this.fileManager, this.logger);
96
+ }
97
+ return this.epicParser;
98
+ }
99
+ /**
100
+ * Get or create standalone story parser instance
101
+ */
102
+ getStandaloneParser() {
103
+ if (!this.standaloneParser) {
104
+ this.standaloneParser = new StandaloneStoryParser(this.fileManager, this.logger);
105
+ }
106
+ return this.standaloneParser;
107
+ }
108
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * StoryParser Service
3
+ *
4
+ * Extracts metadata from story files and provides utilities for updating story status.
5
+ * Supports multiple status formats (inline and section-based) with resilient parsing.
6
+ */
7
+ import type pino from 'pino';
8
+ import type { EpicStoryMetadata, StoryStatus } from '../../models/story.js';
9
+ import type { FileManager } from '../file-system/file-manager.js';
10
+ /**
11
+ * StoryParser service for extracting metadata from story files
12
+ *
13
+ * Provides methods to parse story files and extract key metadata including
14
+ * story number, title, and status. Supports resilient parsing with multiple
15
+ * status format patterns.
16
+ */
17
+ export declare class StoryParser {
18
+ /**
19
+ * FileManager instance for file operations
20
+ */
21
+ private readonly fileManager;
22
+ /**
23
+ * Logger instance for parsing operations
24
+ */
25
+ private readonly logger;
26
+ /**
27
+ * Create a new StoryParser instance
28
+ *
29
+ * @param fileManager - FileManager service for file operations
30
+ * @param logger - Pino logger instance for logging parsing operations
31
+ * @example
32
+ * const logger = createLogger({ namespace: 'services:parsers:story' })
33
+ * const fileManager = new FileManager(logger)
34
+ * const parser = new StoryParser(fileManager, logger)
35
+ */
36
+ constructor(fileManager: FileManager, logger: pino.Logger);
37
+ /**
38
+ * Parse epic-based story metadata from a story file
39
+ *
40
+ * Extracts story number from filename, title from H1 header, and status
41
+ * from either inline format (**Status**: Draft) or section-based format
42
+ * (## Status\n\nDraft). Validates story file structure and throws errors
43
+ * for malformed files.
44
+ *
45
+ * @param storyPath - Path to the story markdown file
46
+ * @returns Epic story metadata with number, title, status, and file path
47
+ * @throws If file cannot be read
48
+ * @throws {ParserError} If story structure is invalid (missing title, missing status, missing epic.story number)
49
+ * @example
50
+ * const metadata = await parser.parseStoryMetadata('docs/stories/BMAD-2.3-story.md')
51
+ * console.log(metadata.number) // "2.3"
52
+ * console.log(metadata.status) // "Draft"
53
+ * console.log(metadata.type) // "epic-based"
54
+ */
55
+ parseStoryMetadata(storyPath: string): Promise<EpicStoryMetadata>;
56
+ /**
57
+ * Update story status in a story file
58
+ *
59
+ * Reads the story file, detects the current status format (inline or section-based),
60
+ * and updates the status value while preserving the original format. This ensures
61
+ * that inline status remains inline and section-based status remains section-based.
62
+ *
63
+ * @param storyPath - Path to the story markdown file
64
+ * @param newStatus - New status value to set
65
+ * @throws If file cannot be read or written
66
+ * @throws {ParserError} If status format cannot be detected
67
+ * @example
68
+ * await parser.updateStoryStatus('docs/stories/BMAD-2.3-story.md', 'Ready')
69
+ */
70
+ updateStoryStatus(storyPath: string, newStatus: StoryStatus): Promise<void>;
71
+ /**
72
+ * Detect status format (inline vs section-based) and extract current status
73
+ *
74
+ * Used by updateStoryStatus to determine how to update the status value
75
+ * while preserving the original format.
76
+ *
77
+ * @param content - Story file content
78
+ * @param storyPath - Path to story file (for error context)
79
+ * @returns Object with format type and old status value
80
+ * @throws {ParserError} If status format cannot be detected
81
+ */
82
+ private detectStatusFormat;
83
+ /**
84
+ * Extract story status using multiple pattern strategies
85
+ *
86
+ * Tries multiple patterns to extract status (in order of specificity):
87
+ * 1. Inline format: **Status**: Draft
88
+ * 2. Section with bold: ## Status\n\n**Draft**
89
+ * 3. Section plain: ## Status\n\nDraft
90
+ * 4. Fallback: any word after Status header
91
+ *
92
+ * @param content - Story file content
93
+ * @param storyPath - Path to story file (for error context)
94
+ * @returns Extracted status value
95
+ * @throws {ParserError} If status cannot be found in any supported format
96
+ */
97
+ private extractStatus;
98
+ /**
99
+ * Extract story number from filename
100
+ *
101
+ * Parses filename to extract epic and story numbers. Supports patterns like:
102
+ * - BMAD-2.3-story.md → epicNumber: 2, storyNumber: 3
103
+ * - PREFIX-1.5-description.md → epicNumber: 1, storyNumber: 5
104
+ *
105
+ * @param storyPath - Path to the story file
106
+ * @returns Object with epicNumber, storyNumber, and full number string
107
+ * @throws {ParserError} If filename does not contain valid story number pattern
108
+ */
109
+ private extractStoryNumber;
110
+ /**
111
+ * Extract story title from H1 header
112
+ *
113
+ * Finds the first H1 header in the content and extracts the title.
114
+ * Removes the "Story X.Y:" prefix if present.
115
+ *
116
+ * @param content - Story file content
117
+ * @param storyPath - Path to story file (for error context)
118
+ * @returns Extracted title without "Story X.Y:" prefix
119
+ * @throws {ParserError} If H1 header is not found
120
+ */
121
+ private extractTitle;
122
+ }