@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,262 @@
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 { basename } from 'node:path';
8
+ import { ParserError } from '../../utils/errors.js';
9
+ /**
10
+ * StoryParser service for extracting metadata from story files
11
+ *
12
+ * Provides methods to parse story files and extract key metadata including
13
+ * story number, title, and status. Supports resilient parsing with multiple
14
+ * status format patterns.
15
+ */
16
+ export class StoryParser {
17
+ /**
18
+ * FileManager instance for file operations
19
+ */
20
+ fileManager;
21
+ /**
22
+ * Logger instance for parsing operations
23
+ */
24
+ logger;
25
+ /**
26
+ * Create a new StoryParser instance
27
+ *
28
+ * @param fileManager - FileManager service for file operations
29
+ * @param logger - Pino logger instance for logging parsing operations
30
+ * @example
31
+ * const logger = createLogger({ namespace: 'services:parsers:story' })
32
+ * const fileManager = new FileManager(logger)
33
+ * const parser = new StoryParser(fileManager, logger)
34
+ */
35
+ constructor(fileManager, logger) {
36
+ this.fileManager = fileManager;
37
+ this.logger = logger;
38
+ }
39
+ /**
40
+ * Parse epic-based story metadata from a story file
41
+ *
42
+ * Extracts story number from filename, title from H1 header, and status
43
+ * from either inline format (**Status**: Draft) or section-based format
44
+ * (## Status\n\nDraft). Validates story file structure and throws errors
45
+ * for malformed files.
46
+ *
47
+ * @param storyPath - Path to the story markdown file
48
+ * @returns Epic story metadata with number, title, status, and file path
49
+ * @throws If file cannot be read
50
+ * @throws {ParserError} If story structure is invalid (missing title, missing status, missing epic.story number)
51
+ * @example
52
+ * const metadata = await parser.parseStoryMetadata('docs/stories/BMAD-2.3-story.md')
53
+ * console.log(metadata.number) // "2.3"
54
+ * console.log(metadata.status) // "Draft"
55
+ * console.log(metadata.type) // "epic-based"
56
+ */
57
+ async parseStoryMetadata(storyPath) {
58
+ this.logger.info('Parsing story metadata from: %s', storyPath);
59
+ // Read story file content
60
+ const content = await this.fileManager.readFile(storyPath);
61
+ // Extract story number from filename
62
+ const { epicNumber, number, storyNumber } = this.extractStoryNumber(storyPath);
63
+ // Extract title from H1 header
64
+ const title = this.extractTitle(content, storyPath);
65
+ // Extract status using multiple pattern strategies
66
+ const status = this.extractStatus(content, storyPath);
67
+ this.logger.info('Successfully parsed story metadata: %s (Epic: %d, Story: %d, Status: %s)', number, epicNumber, storyNumber, status);
68
+ return {
69
+ epicNumber,
70
+ filePath: storyPath,
71
+ id: number,
72
+ number,
73
+ status,
74
+ storyNumber,
75
+ title,
76
+ type: 'epic-based',
77
+ };
78
+ }
79
+ /**
80
+ * Update story status in a story file
81
+ *
82
+ * Reads the story file, detects the current status format (inline or section-based),
83
+ * and updates the status value while preserving the original format. This ensures
84
+ * that inline status remains inline and section-based status remains section-based.
85
+ *
86
+ * @param storyPath - Path to the story markdown file
87
+ * @param newStatus - New status value to set
88
+ * @throws If file cannot be read or written
89
+ * @throws {ParserError} If status format cannot be detected
90
+ * @example
91
+ * await parser.updateStoryStatus('docs/stories/BMAD-2.3-story.md', 'Ready')
92
+ */
93
+ async updateStoryStatus(storyPath, newStatus) {
94
+ this.logger.info('Updating story status in %s to: %s', storyPath, newStatus);
95
+ // Read current story file content
96
+ const content = await this.fileManager.readFile(storyPath);
97
+ // Detect current status and format
98
+ const { format, oldStatus } = this.detectStatusFormat(content, storyPath);
99
+ // Update status based on detected format
100
+ let updatedContent;
101
+ if (format === 'inline') {
102
+ // Inline format: **Status**: Draft
103
+ updatedContent = content.replace(/\*\*Status\*\*:\s*\w+/, `**Status**: ${newStatus}`);
104
+ }
105
+ else if (format === 'section-bold') {
106
+ // Section with bold: ## Status\n\n**Draft**
107
+ updatedContent = content.replace(/^## Status\s*\n\s*\n\s*\*\*\w+\*\*/m, `## Status\n\n**${newStatus}**`);
108
+ }
109
+ else {
110
+ // Section plain: ## Status\n\nDraft
111
+ updatedContent = content.replace(/^## Status\s*\n\s*\n\s*\w+/m, `## Status\n\n${newStatus}`);
112
+ }
113
+ // Write updated content back to file
114
+ await this.fileManager.writeFile(storyPath, updatedContent);
115
+ this.logger.info('Story status updated successfully: %s → %s (format: %s)', oldStatus, newStatus, format);
116
+ }
117
+ /**
118
+ * Detect status format (inline vs section-based) and extract current status
119
+ *
120
+ * Used by updateStoryStatus to determine how to update the status value
121
+ * while preserving the original format.
122
+ *
123
+ * @param content - Story file content
124
+ * @param storyPath - Path to story file (for error context)
125
+ * @returns Object with format type and old status value
126
+ * @throws {ParserError} If status format cannot be detected
127
+ */
128
+ detectStatusFormat(content, storyPath) {
129
+ // Check for inline format: **Status**: Draft
130
+ const inlineMatch = /\*\*Status\*\*:\s*(\w+)/.exec(content);
131
+ if (inlineMatch) {
132
+ return { format: 'inline', oldStatus: inlineMatch[1].trim() };
133
+ }
134
+ // Check for section with bold: ## Status\n\n**Draft**
135
+ const boldSectionMatch = /^## Status\s*\n\s*\n\s*\*\*(\w+)\*\*/m.exec(content);
136
+ if (boldSectionMatch) {
137
+ return { format: 'section-bold', oldStatus: boldSectionMatch[1].trim() };
138
+ }
139
+ // Check for section plain: ## Status\n\nDraft (no bold markers)
140
+ const plainSectionMatch = /^## Status\s*\n\s*\n\s*(?!\*\*)(\w+)/m.exec(content);
141
+ if (plainSectionMatch) {
142
+ return { format: 'section', oldStatus: plainSectionMatch[1].trim() };
143
+ }
144
+ // Status format not detected
145
+ this.logger.error('Unable to detect status format in: %s', storyPath);
146
+ throw new ParserError('Unable to detect status format', {
147
+ filePath: storyPath,
148
+ suggestion: 'Ensure story has valid Status section in one of the supported formats',
149
+ });
150
+ }
151
+ /**
152
+ * Extract story status using multiple pattern strategies
153
+ *
154
+ * Tries multiple patterns to extract status (in order of specificity):
155
+ * 1. Inline format: **Status**: Draft
156
+ * 2. Section with bold: ## Status\n\n**Draft**
157
+ * 3. Section plain: ## Status\n\nDraft
158
+ * 4. Fallback: any word after Status header
159
+ *
160
+ * @param content - Story file content
161
+ * @param storyPath - Path to story file (for error context)
162
+ * @returns Extracted status value
163
+ * @throws {ParserError} If status cannot be found in any supported format
164
+ */
165
+ extractStatus(content, storyPath) {
166
+ // Pattern 1: Inline format - **Status**: Draft
167
+ const inlineMatch = /\*\*Status\*\*:\s*(\w+)/.exec(content);
168
+ if (inlineMatch) {
169
+ const status = inlineMatch[1].trim();
170
+ this.logger.debug('Extracted status (inline format): %s', status);
171
+ return status;
172
+ }
173
+ // Pattern 2: Section with bold - ## Status\n\n**Draft**
174
+ const boldSectionMatch = /^## Status\s*\n\s*\n\s*\*\*(\w+)\*\*/m.exec(content);
175
+ if (boldSectionMatch) {
176
+ const status = boldSectionMatch[1].trim();
177
+ this.logger.debug('Extracted status (section bold format): %s', status);
178
+ return status;
179
+ }
180
+ // Pattern 3: Section plain - ## Status\n\nDraft (no bold markers)
181
+ const plainSectionMatch = /^## Status\s*\n\s*\n\s*(?!\*\*)(\w+)/m.exec(content);
182
+ if (plainSectionMatch) {
183
+ const status = plainSectionMatch[1].trim();
184
+ this.logger.debug('Extracted status (section plain format): %s', status);
185
+ return status;
186
+ }
187
+ // Pattern 4: Fallback - any text after ## Status (more lenient)
188
+ const fallbackMatch = /^## Status\s*\n+\s*(.+?)(?:\n|$)/m.exec(content);
189
+ if (fallbackMatch) {
190
+ // Clean up the match: remove markdown formatting, trim whitespace
191
+ const rawStatus = fallbackMatch[1]
192
+ .replaceAll('**', '') // Remove bold markers
193
+ .replaceAll(/[_*]/g, '') // Remove italic/emphasis markers
194
+ .replaceAll('---', '') // Remove horizontal rules
195
+ .trim();
196
+ if (rawStatus && /^\w+$/.test(rawStatus)) {
197
+ const status = rawStatus;
198
+ this.logger.debug('Extracted status (fallback format): %s', status);
199
+ return status;
200
+ }
201
+ }
202
+ // Status not found in any supported format
203
+ this.logger.error('Story file missing Status section or field: %s', storyPath);
204
+ throw new ParserError('Story file missing Status section or field', {
205
+ filePath: storyPath,
206
+ suggestion: 'Ensure story has "## Status" section followed by status value (Draft, Ready, In Progress, etc.)',
207
+ });
208
+ }
209
+ /**
210
+ * Extract story number from filename
211
+ *
212
+ * Parses filename to extract epic and story numbers. Supports patterns like:
213
+ * - BMAD-2.3-story.md → epicNumber: 2, storyNumber: 3
214
+ * - PREFIX-1.5-description.md → epicNumber: 1, storyNumber: 5
215
+ *
216
+ * @param storyPath - Path to the story file
217
+ * @returns Object with epicNumber, storyNumber, and full number string
218
+ * @throws {ParserError} If filename does not contain valid story number pattern
219
+ */
220
+ extractStoryNumber(storyPath) {
221
+ const filename = basename(storyPath);
222
+ const match = /(\d+)\.(\d+)/.exec(filename);
223
+ if (!match) {
224
+ this.logger.error('Invalid story filename format: %s', filename);
225
+ throw new ParserError('Story filename must contain epic.story number (e.g., 2.3)', {
226
+ filename,
227
+ filePath: storyPath,
228
+ });
229
+ }
230
+ const epicNumber = Number.parseInt(match[1], 10);
231
+ const storyNumber = Number.parseInt(match[2], 10);
232
+ const number = `${epicNumber}.${storyNumber}`;
233
+ this.logger.debug('Extracted story number: %s (epic: %d, story: %d)', number, epicNumber, storyNumber);
234
+ return { epicNumber, number, storyNumber };
235
+ }
236
+ /**
237
+ * Extract story title from H1 header
238
+ *
239
+ * Finds the first H1 header in the content and extracts the title.
240
+ * Removes the "Story X.Y:" prefix if present.
241
+ *
242
+ * @param content - Story file content
243
+ * @param storyPath - Path to story file (for error context)
244
+ * @returns Extracted title without "Story X.Y:" prefix
245
+ * @throws {ParserError} If H1 header is not found
246
+ */
247
+ extractTitle(content, storyPath) {
248
+ // Match first H1 header: # Story 2.3: Create Story Parser Service
249
+ const match = /^# (.+)$/m.exec(content);
250
+ if (!match) {
251
+ this.logger.error('Story file missing H1 title header: %s', storyPath);
252
+ throw new ParserError('Story file missing H1 title header', {
253
+ filePath: storyPath,
254
+ });
255
+ }
256
+ const rawTitle = match[1].trim();
257
+ // Remove "Story X.Y:" prefix if present
258
+ const title = rawTitle.replace(/^Story\s+\d+\.\d+:\s*/, '');
259
+ this.logger.debug('Extracted title: %s', title);
260
+ return title;
261
+ }
262
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * DecomposeSessionScaffolder
3
+ *
4
+ * Creates the directory structure and initial files for a decompose session.
5
+ * Organizes task graphs, prompts, outputs, and reports in a structured format.
6
+ */
7
+ import type pino from 'pino';
8
+ import type { DecomposeSessionConfig, GraphExecutionResult, TaskGraph } from '../../types/task-graph.js';
9
+ import type { FileManager } from '../file-system/file-manager.js';
10
+ /**
11
+ * Scaffolder for decompose session directory structure
12
+ */
13
+ export declare class DecomposeSessionScaffolder {
14
+ private readonly fileManager;
15
+ private readonly logger;
16
+ constructor(fileManager: FileManager, logger: pino.Logger);
17
+ /**
18
+ * Create the session directory structure
19
+ *
20
+ * Creates:
21
+ * - Session root directory
22
+ * - prompts/ subdirectory for task-specific prompts
23
+ * - outputs/ or stories/ subdirectory for task execution results (depends on story mode)
24
+ *
25
+ * @param sessionDir - Root directory for the session
26
+ * @param storyFormat - Whether to create stories/ instead of outputs/
27
+ * @returns Session directory path
28
+ */
29
+ createSessionStructure(sessionDir: string, storyFormat?: boolean): Promise<string>;
30
+ /**
31
+ * Write the execution report file (after execution completes)
32
+ *
33
+ * @param sessionDir - Session directory
34
+ * @param taskGraph - Complete task graph
35
+ * @param executionResult - Result of graph execution
36
+ */
37
+ writeExecutionReport(sessionDir: string, taskGraph: TaskGraph, executionResult: GraphExecutionResult): Promise<void>;
38
+ /**
39
+ * Write the goal description file
40
+ *
41
+ * @param sessionDir - Session directory
42
+ * @param config - Session configuration
43
+ */
44
+ writeGoalFile(sessionDir: string, config: DecomposeSessionConfig): Promise<void>;
45
+ /**
46
+ * Write the master prompt file
47
+ *
48
+ * @param sessionDir - Session directory
49
+ * @param taskGraph - Complete task graph
50
+ */
51
+ writeMasterPromptFile(sessionDir: string, taskGraph: TaskGraph): Promise<void>;
52
+ /**
53
+ * Create a README for the session
54
+ *
55
+ * @param sessionDir - Session directory
56
+ * @param taskGraph - Complete task graph
57
+ * @param storyFormat - Whether this is story format mode
58
+ */
59
+ writeSessionReadme(sessionDir: string, taskGraph: TaskGraph, storyFormat?: boolean): Promise<void>;
60
+ /**
61
+ * Write the task graph YAML file
62
+ *
63
+ * @param sessionDir - Session directory
64
+ * @param taskGraph - Complete task graph
65
+ */
66
+ writeTaskGraphFile(sessionDir: string, taskGraph: TaskGraph): Promise<void>;
67
+ /**
68
+ * Write individual task prompt files
69
+ *
70
+ * @param sessionDir - Session directory
71
+ * @param taskGraph - Complete task graph
72
+ */
73
+ writeTaskPromptFiles(sessionDir: string, taskGraph: TaskGraph): Promise<void>;
74
+ }
@@ -0,0 +1,315 @@
1
+ /**
2
+ * DecomposeSessionScaffolder
3
+ *
4
+ * Creates the directory structure and initial files for a decompose session.
5
+ * Organizes task graphs, prompts, outputs, and reports in a structured format.
6
+ */
7
+ import * as yaml from 'js-yaml';
8
+ import { join } from 'node:path';
9
+ /**
10
+ * Scaffolder for decompose session directory structure
11
+ */
12
+ export class DecomposeSessionScaffolder {
13
+ fileManager;
14
+ logger;
15
+ constructor(fileManager, logger) {
16
+ this.fileManager = fileManager;
17
+ this.logger = logger;
18
+ }
19
+ /**
20
+ * Create the session directory structure
21
+ *
22
+ * Creates:
23
+ * - Session root directory
24
+ * - prompts/ subdirectory for task-specific prompts
25
+ * - outputs/ or stories/ subdirectory for task execution results (depends on story mode)
26
+ *
27
+ * @param sessionDir - Root directory for the session
28
+ * @param storyFormat - Whether to create stories/ instead of outputs/
29
+ * @returns Session directory path
30
+ */
31
+ async createSessionStructure(sessionDir, storyFormat = false) {
32
+ this.logger.info({ sessionDir, storyFormat }, 'Creating decompose session structure');
33
+ // Create root session directory
34
+ await this.fileManager.createDirectory(sessionDir);
35
+ // Create subdirectories
36
+ await this.fileManager.createDirectory(join(sessionDir, 'prompts'));
37
+ // Create either stories/ or outputs/ directory depending on mode
38
+ const subDir = storyFormat ? 'stories' : 'outputs';
39
+ await this.fileManager.createDirectory(join(sessionDir, subDir));
40
+ this.logger.info({ sessionDir, storyFormat }, 'Session structure created');
41
+ return sessionDir;
42
+ }
43
+ /**
44
+ * Write the execution report file (after execution completes)
45
+ *
46
+ * @param sessionDir - Session directory
47
+ * @param taskGraph - Complete task graph
48
+ * @param executionResult - Result of graph execution
49
+ */
50
+ async writeExecutionReport(sessionDir, taskGraph, executionResult) {
51
+ const reportPath = join(sessionDir, 'execution-report.yaml');
52
+ const yamlContent = yaml.dump({
53
+ completedAt: new Date().toISOString(),
54
+ completedTasks: executionResult.completedTasks,
55
+ failedTasks: executionResult.failedTasks,
56
+ goal: taskGraph.goal,
57
+ layerResults: executionResult.executionSummary.layerResults,
58
+ sessionId: taskGraph.session.id,
59
+ skippedTasks: executionResult.skippedTasks,
60
+ startedAt: taskGraph.session.createdAt,
61
+ success: executionResult.success,
62
+ taskResults: executionResult.taskResults.map((r) => ({
63
+ duration: r.duration,
64
+ errors: r.errors,
65
+ exitCode: r.exitCode,
66
+ success: r.success,
67
+ taskId: r.taskId,
68
+ })),
69
+ totalDuration: executionResult.totalDuration,
70
+ totalTasks: executionResult.totalTasks,
71
+ }, {
72
+ indent: 2,
73
+ lineWidth: 120,
74
+ noRefs: true,
75
+ });
76
+ await this.fileManager.writeFile(reportPath, yamlContent);
77
+ this.logger.info({ reportPath }, 'Execution report written');
78
+ }
79
+ /**
80
+ * Write the goal description file
81
+ *
82
+ * @param sessionDir - Session directory
83
+ * @param config - Session configuration
84
+ */
85
+ async writeGoalFile(sessionDir, config) {
86
+ const goalPath = join(sessionDir, 'goal.md');
87
+ const content = `# Goal
88
+
89
+ **Session ID:** ${config.sessionId}
90
+ **Created:** ${config.createdAt.toISOString()}
91
+ **Working Directory:** ${config.options.cwd ?? 'current directory'}
92
+
93
+ ## Objective
94
+
95
+ ${config.goal}
96
+
97
+ ## Options
98
+
99
+ - **Per-File Mode:** ${config.options.perFile ? 'Enabled' : 'Disabled'}
100
+ ${config.options.filePattern ? `- **File Pattern:** \`${config.options.filePattern}\`` : ''}
101
+ - **Max Parallel:** ${config.options.maxParallel ?? 3}
102
+ - **Plan Only:** ${config.options.planOnly ? 'Yes' : 'No'}
103
+
104
+ ## Context Files
105
+
106
+ ${config.options.contextFiles && config.options.contextFiles.length > 0 ? config.options.contextFiles.map((f) => `- \`${f}\``).join('\n') : '_None provided_'}
107
+
108
+ ---
109
+
110
+ <!-- Powered by BMAD™ Core -->
111
+ `;
112
+ await this.fileManager.writeFile(goalPath, content);
113
+ this.logger.debug({ goalPath }, 'Goal file written');
114
+ }
115
+ /**
116
+ * Write the master prompt file
117
+ *
118
+ * @param sessionDir - Session directory
119
+ * @param taskGraph - Complete task graph
120
+ */
121
+ async writeMasterPromptFile(sessionDir, taskGraph) {
122
+ const masterPath = join(sessionDir, 'master-prompt.md');
123
+ const content = `# Master Prompt Template
124
+
125
+ **Session ID:** ${taskGraph.session.id}
126
+ **Goal:** ${taskGraph.goal}
127
+
128
+ ## Reusable Template
129
+
130
+ This prompt template applies to all tasks in this session and can be reused for similar goals.
131
+
132
+ \`\`\`
133
+ ${taskGraph.masterPrompt}
134
+ \`\`\`
135
+
136
+ ## Usage
137
+
138
+ This master prompt is automatically included in each task execution. It provides:
139
+ - Common guidelines and best practices
140
+ - Project-specific coding standards
141
+ - Quality expectations
142
+ - General instructions that apply to all tasks
143
+
144
+ When executing tasks individually, prepend this master prompt to the task-specific prompt.
145
+
146
+ ---
147
+
148
+ <!-- Powered by BMAD™ Core -->
149
+ `;
150
+ await this.fileManager.writeFile(masterPath, content);
151
+ this.logger.debug({ masterPath }, 'Master prompt file written');
152
+ }
153
+ /**
154
+ * Create a README for the session
155
+ *
156
+ * @param sessionDir - Session directory
157
+ * @param taskGraph - Complete task graph
158
+ * @param storyFormat - Whether this is story format mode
159
+ */
160
+ async writeSessionReadme(sessionDir, taskGraph, storyFormat = false) {
161
+ const readmePath = join(sessionDir, 'SESSION_README.md');
162
+ const outputDirName = storyFormat ? 'stories' : 'outputs';
163
+ const outputDescription = storyFormat ? 'Story files (BMAD format)' : 'Task execution results';
164
+ const content = `# Decompose Session: ${taskGraph.session.id}
165
+
166
+ **Goal:** ${taskGraph.goal}
167
+ **Created:** ${taskGraph.session.createdAt}
168
+ **Total Tasks:** ${taskGraph.metadata.totalTasks}
169
+ **Estimated Duration:** ${taskGraph.metadata.estimatedDuration} minutes
170
+ **Execution Layers:** ${taskGraph.metadata.executionLayers.length}
171
+ ${storyFormat ? `**Format:** Story Mode (BMAD Stories)\n` : ''}
172
+
173
+ ## Directory Structure
174
+
175
+ \`\`\`
176
+ ${taskGraph.session.id}/
177
+ ├── SESSION_README.md # This file
178
+ ├── goal.md # Original goal and options
179
+ ├── master-prompt.md # Reusable prompt template
180
+ ├── task-graph.yaml # Complete task dependency graph
181
+ ├── execution-report.yaml # Execution results (after completion)
182
+ ├── prompts/ # Individual ${storyFormat ? 'story' : 'task'} prompts
183
+ │ ├── ${storyFormat ? `${taskGraph.tasks[0]?.id || 'STORY-001'}` : 'task-001'}-prompt.md
184
+ │ ├── ${storyFormat ? `${taskGraph.tasks[1]?.id || 'STORY-002'}` : 'task-002'}-prompt.md
185
+ │ └── ...
186
+ └── ${outputDirName}/ # ${outputDescription}
187
+ ├── ${storyFormat ? `${taskGraph.tasks[0]?.id || 'STORY-001'}` : 'task-001-output'}.md
188
+ ├── ${storyFormat ? `${taskGraph.tasks[1]?.id || 'STORY-002'}` : 'task-002-output'}.md
189
+ └── ...
190
+ \`\`\`
191
+
192
+ ## Execution Layers
193
+
194
+ ${taskGraph.metadata.executionLayers
195
+ .map((layer, idx) => `### Layer ${idx + 1} (${layer.length} task${layer.length > 1 ? 's' : ''} in parallel)
196
+
197
+ ${layer.map((taskId) => `- \`${taskId}\``).join('\n')}
198
+ `)
199
+ .join('\n')}
200
+
201
+ ## Task Overview
202
+
203
+ ${taskGraph.tasks
204
+ .map((task) => `### ${task.id}: ${task.title}
205
+
206
+ - **Estimated Time:** ${task.estimatedMinutes} min
207
+ - **Dependencies:** ${task.dependencies.length > 0 ? task.dependencies.join(', ') : 'None'}
208
+ - **Agent:** ${task.agentType ?? 'dev'}
209
+ `)
210
+ .join('\n')}
211
+
212
+ ## How to Use This Session
213
+
214
+ 1. **Review the task graph:** Check \`task-graph.yaml\` for the complete plan
215
+ 2. **Execute tasks:** Run \`bmad-cli decompose --execute\` or execute manually
216
+ 3. **Check outputs:** Review task results in \`outputs/\` directory
217
+ 4. **Reuse prompts:** Use \`master-prompt.md\` for similar projects
218
+
219
+ ---
220
+
221
+ <!-- Powered by BMAD™ Core -->
222
+ `;
223
+ await this.fileManager.writeFile(readmePath, content);
224
+ this.logger.info({ readmePath }, 'Session README written');
225
+ }
226
+ /**
227
+ * Write the task graph YAML file
228
+ *
229
+ * @param sessionDir - Session directory
230
+ * @param taskGraph - Complete task graph
231
+ */
232
+ async writeTaskGraphFile(sessionDir, taskGraph) {
233
+ const graphPath = join(sessionDir, 'task-graph.yaml');
234
+ // Convert task graph to YAML-friendly format
235
+ const yamlContent = yaml.dump({
236
+ goal: taskGraph.goal,
237
+ masterPrompt: taskGraph.masterPrompt,
238
+ metadata: taskGraph.metadata,
239
+ session: taskGraph.session,
240
+ tasks: taskGraph.tasks.map((t) => ({
241
+ agentType: t.agentType,
242
+ dependencies: t.dependencies,
243
+ description: t.description,
244
+ estimatedMinutes: t.estimatedMinutes,
245
+ id: t.id,
246
+ outputFile: t.outputFile,
247
+ parallelizable: t.parallelizable,
248
+ prompt: t.prompt,
249
+ targetFiles: t.targetFiles,
250
+ title: t.title,
251
+ })),
252
+ }, {
253
+ indent: 2,
254
+ lineWidth: 120,
255
+ noRefs: true,
256
+ });
257
+ await this.fileManager.writeFile(graphPath, yamlContent);
258
+ this.logger.info({ graphPath, totalTasks: taskGraph.tasks.length }, 'Task graph YAML written');
259
+ }
260
+ /**
261
+ * Write individual task prompt files
262
+ *
263
+ * @param sessionDir - Session directory
264
+ * @param taskGraph - Complete task graph
265
+ */
266
+ async writeTaskPromptFiles(sessionDir, taskGraph) {
267
+ this.logger.info({ taskCount: taskGraph.tasks.length }, 'Writing individual task prompt files');
268
+ // Write task files sequentially to avoid overwhelming file system
269
+ for (const task of taskGraph.tasks) {
270
+ const promptPath = join(sessionDir, 'prompts', `${task.id}-prompt.md`);
271
+ const content = `# ${task.title}
272
+
273
+ **Task ID:** ${task.id}
274
+ **Estimated Time:** ${task.estimatedMinutes} minutes
275
+ **Agent Type:** ${task.agentType ?? 'dev'}
276
+ **Parallelizable:** ${task.parallelizable ? 'Yes' : 'No'}
277
+
278
+ ## Description
279
+
280
+ ${task.description}
281
+
282
+ ## Dependencies
283
+
284
+ ${task.dependencies.length > 0 ? task.dependencies.map((d) => `- ${d}`).join('\n') : '_None - can run immediately_'}
285
+
286
+ ## Target Files
287
+
288
+ ${task.targetFiles && task.targetFiles.length > 0 ? task.targetFiles.map((f) => `- \`${f}\``).join('\n') : '_No specific files_'}
289
+
290
+ ## Prompt
291
+
292
+ \`\`\`
293
+ ${task.prompt}
294
+ \`\`\`
295
+
296
+ ## Master Prompt Context
297
+
298
+ \`\`\`
299
+ ${taskGraph.masterPrompt}
300
+ \`\`\`
301
+
302
+ ## Output File
303
+
304
+ \`${task.outputFile}\`
305
+
306
+ ---
307
+
308
+ <!-- Powered by BMAD™ Core -->
309
+ `;
310
+ // eslint-disable-next-line no-await-in-loop -- sequential file writes prevent overwhelming filesystem
311
+ await this.fileManager.writeFile(promptPath, content);
312
+ }
313
+ this.logger.info({ taskCount: taskGraph.tasks.length }, 'Task prompt files written');
314
+ }
315
+ }