@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.
- package/LICENSE +21 -0
- package/README.md +1017 -0
- package/bin/dev +5 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/config/show.d.ts +34 -0
- package/dist/commands/config/show.js +108 -0
- package/dist/commands/config/validate.d.ts +29 -0
- package/dist/commands/config/validate.js +131 -0
- package/dist/commands/decompose.d.ts +79 -0
- package/dist/commands/decompose.js +327 -0
- package/dist/commands/demo.d.ts +18 -0
- package/dist/commands/demo.js +107 -0
- package/dist/commands/epics/create.d.ts +123 -0
- package/dist/commands/epics/create.js +459 -0
- package/dist/commands/epics/list.d.ts +120 -0
- package/dist/commands/epics/list.js +280 -0
- package/dist/commands/hello/index.d.ts +12 -0
- package/dist/commands/hello/index.js +34 -0
- package/dist/commands/hello/world.d.ts +8 -0
- package/dist/commands/hello/world.js +24 -0
- package/dist/commands/prd/fix.d.ts +39 -0
- package/dist/commands/prd/fix.js +140 -0
- package/dist/commands/prd/validate.d.ts +112 -0
- package/dist/commands/prd/validate.js +302 -0
- package/dist/commands/stories/create.d.ts +95 -0
- package/dist/commands/stories/create.js +431 -0
- package/dist/commands/stories/develop.d.ts +91 -0
- package/dist/commands/stories/develop.js +460 -0
- package/dist/commands/stories/list.d.ts +84 -0
- package/dist/commands/stories/list.js +291 -0
- package/dist/commands/stories/move.d.ts +66 -0
- package/dist/commands/stories/move.js +273 -0
- package/dist/commands/stories/qa.d.ts +99 -0
- package/dist/commands/stories/qa.js +530 -0
- package/dist/commands/workflow.d.ts +97 -0
- package/dist/commands/workflow.js +390 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models/agent-options.d.ts +50 -0
- package/dist/models/agent-options.js +1 -0
- package/dist/models/agent-result.d.ts +29 -0
- package/dist/models/agent-result.js +1 -0
- package/dist/models/index.d.ts +10 -0
- package/dist/models/index.js +10 -0
- package/dist/models/phase-result.d.ts +65 -0
- package/dist/models/phase-result.js +7 -0
- package/dist/models/provider.d.ts +28 -0
- package/dist/models/provider.js +18 -0
- package/dist/models/story.d.ts +154 -0
- package/dist/models/story.js +18 -0
- package/dist/models/workflow-config.d.ts +148 -0
- package/dist/models/workflow-config.js +1 -0
- package/dist/models/workflow-result.d.ts +164 -0
- package/dist/models/workflow-result.js +7 -0
- package/dist/services/agents/agent-runner-factory.d.ts +31 -0
- package/dist/services/agents/agent-runner-factory.js +44 -0
- package/dist/services/agents/agent-runner.d.ts +46 -0
- package/dist/services/agents/agent-runner.js +29 -0
- package/dist/services/agents/claude-agent-runner.d.ts +81 -0
- package/dist/services/agents/claude-agent-runner.js +332 -0
- package/dist/services/agents/gemini-agent-runner.d.ts +82 -0
- package/dist/services/agents/gemini-agent-runner.js +350 -0
- package/dist/services/agents/index.d.ts +7 -0
- package/dist/services/agents/index.js +7 -0
- package/dist/services/file-system/file-manager.d.ts +110 -0
- package/dist/services/file-system/file-manager.js +223 -0
- package/dist/services/file-system/glob-matcher.d.ts +75 -0
- package/dist/services/file-system/glob-matcher.js +126 -0
- package/dist/services/file-system/path-resolver.d.ts +183 -0
- package/dist/services/file-system/path-resolver.js +400 -0
- package/dist/services/logging/workflow-logger.d.ts +232 -0
- package/dist/services/logging/workflow-logger.js +552 -0
- package/dist/services/orchestration/batch-processor.d.ts +113 -0
- package/dist/services/orchestration/batch-processor.js +187 -0
- package/dist/services/orchestration/dependency-graph-executor.d.ts +60 -0
- package/dist/services/orchestration/dependency-graph-executor.js +447 -0
- package/dist/services/orchestration/index.d.ts +10 -0
- package/dist/services/orchestration/index.js +8 -0
- package/dist/services/orchestration/input-detector.d.ts +125 -0
- package/dist/services/orchestration/input-detector.js +381 -0
- package/dist/services/orchestration/story-queue.d.ts +94 -0
- package/dist/services/orchestration/story-queue.js +170 -0
- package/dist/services/orchestration/story-type-detector.d.ts +80 -0
- package/dist/services/orchestration/story-type-detector.js +258 -0
- package/dist/services/orchestration/task-decomposition-service.d.ts +67 -0
- package/dist/services/orchestration/task-decomposition-service.js +607 -0
- package/dist/services/orchestration/workflow-orchestrator.d.ts +659 -0
- package/dist/services/orchestration/workflow-orchestrator.js +2201 -0
- package/dist/services/parsers/epic-parser.d.ts +117 -0
- package/dist/services/parsers/epic-parser.js +264 -0
- package/dist/services/parsers/prd-fixer.d.ts +86 -0
- package/dist/services/parsers/prd-fixer.js +194 -0
- package/dist/services/parsers/prd-parser.d.ts +123 -0
- package/dist/services/parsers/prd-parser.js +286 -0
- package/dist/services/parsers/standalone-story-parser.d.ts +114 -0
- package/dist/services/parsers/standalone-story-parser.js +255 -0
- package/dist/services/parsers/story-parser-factory.d.ts +81 -0
- package/dist/services/parsers/story-parser-factory.js +108 -0
- package/dist/services/parsers/story-parser.d.ts +122 -0
- package/dist/services/parsers/story-parser.js +262 -0
- package/dist/services/scaffolding/decompose-session-scaffolder.d.ts +74 -0
- package/dist/services/scaffolding/decompose-session-scaffolder.js +315 -0
- package/dist/services/scaffolding/file-scaffolder.d.ts +94 -0
- package/dist/services/scaffolding/file-scaffolder.js +314 -0
- package/dist/services/validation/config-validator.d.ts +88 -0
- package/dist/services/validation/config-validator.js +167 -0
- package/dist/types/task-graph.d.ts +142 -0
- package/dist/types/task-graph.js +5 -0
- package/dist/utils/colors.d.ts +49 -0
- package/dist/utils/colors.js +50 -0
- package/dist/utils/error-formatter.d.ts +64 -0
- package/dist/utils/error-formatter.js +279 -0
- package/dist/utils/errors.d.ts +170 -0
- package/dist/utils/errors.js +233 -0
- package/dist/utils/formatters.d.ts +84 -0
- package/dist/utils/formatters.js +162 -0
- package/dist/utils/logger.d.ts +63 -0
- package/dist/utils/logger.js +78 -0
- package/dist/utils/progress.d.ts +104 -0
- package/dist/utils/progress.js +161 -0
- package/dist/utils/retry.d.ts +114 -0
- package/dist/utils/retry.js +160 -0
- package/dist/utils/shared-flags.d.ts +28 -0
- package/dist/utils/shared-flags.js +43 -0
- 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
|
+
}
|