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