@hyperdrive.bot/bmad-workflow 1.0.22 → 1.0.24
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/assets/agents/dev-barry.md +69 -0
- package/assets/agents/dev.md +323 -0
- package/assets/agents/qa.md +92 -0
- package/assets/agents/sm-bob.md +65 -0
- package/assets/agents/sm.md +296 -0
- package/assets/config/default-config.yaml +6 -0
- package/assets/templates/epic-tmpl.yaml +277 -0
- package/assets/templates/prd-tmpl.yaml +261 -0
- package/assets/templates/qa-gate-tmpl.yaml +103 -0
- package/assets/templates/story-tmpl.yaml +138 -0
- package/dist/commands/eject.d.ts +76 -0
- package/dist/commands/eject.js +232 -0
- package/dist/commands/init.d.ts +47 -0
- package/dist/commands/init.js +265 -0
- package/dist/commands/stories/develop.js +1 -0
- package/dist/commands/stories/qa.d.ts +1 -0
- package/dist/commands/stories/qa.js +7 -0
- package/dist/commands/workflow.d.ts +6 -3
- package/dist/commands/workflow.js +106 -26
- package/dist/models/bmad-config-schema.d.ts +51 -0
- package/dist/models/bmad-config-schema.js +53 -0
- package/dist/services/agents/gemini-agent-runner.js +7 -2
- package/dist/services/agents/opencode-agent-runner.js +7 -2
- package/dist/services/file-system/asset-resolver.d.ts +117 -0
- package/dist/services/file-system/asset-resolver.js +234 -0
- package/dist/services/file-system/file-manager.d.ts +13 -0
- package/dist/services/file-system/file-manager.js +32 -0
- package/dist/services/file-system/path-resolver.d.ts +22 -1
- package/dist/services/file-system/path-resolver.js +36 -9
- package/dist/services/orchestration/dependency-graph-executor.js +1 -0
- package/dist/services/orchestration/workflow-orchestrator.d.ts +4 -0
- package/dist/services/orchestration/workflow-orchestrator.js +20 -6
- package/dist/utils/config-merge.d.ts +60 -0
- package/dist/utils/config-merge.js +52 -0
- package/package.json +4 -2
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AssetResolver Service
|
|
3
|
+
*
|
|
4
|
+
* Resolves agents, templates, and config through a three-level chain:
|
|
5
|
+
* 1. CLI flags (highest priority)
|
|
6
|
+
* 2. .bmad-workflow.yaml config file
|
|
7
|
+
* 3. Bundled defaults (shipped inside npm package)
|
|
8
|
+
*
|
|
9
|
+
* This enables the CLI to work both standalone (zero-config) and
|
|
10
|
+
* in existing BMAD setups with .bmad-core/.
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'fs-extra';
|
|
13
|
+
import { load as yamlLoad } from 'js-yaml';
|
|
14
|
+
import { dirname, join, resolve } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { parseDuration } from '../../utils/duration.js';
|
|
17
|
+
/**
|
|
18
|
+
* Valid AI provider names
|
|
19
|
+
*/
|
|
20
|
+
const VALID_PROVIDERS = ['claude', 'gemini', 'opencode'];
|
|
21
|
+
/**
|
|
22
|
+
* AssetResolver resolves asset paths using a three-level resolution chain.
|
|
23
|
+
*/
|
|
24
|
+
export class AssetResolver {
|
|
25
|
+
bundledPath;
|
|
26
|
+
config;
|
|
27
|
+
flags;
|
|
28
|
+
/**
|
|
29
|
+
* Create a new AssetResolver
|
|
30
|
+
*
|
|
31
|
+
* @param flags - CLI flag overrides for agent selection
|
|
32
|
+
*/
|
|
33
|
+
constructor(flags = {}) {
|
|
34
|
+
this.flags = flags;
|
|
35
|
+
this.bundledPath = this.computeBundledPath();
|
|
36
|
+
this.config = this.loadConfigFile();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve an agent file path.
|
|
40
|
+
*
|
|
41
|
+
* Resolution order: CLI flag → config file → bundled default
|
|
42
|
+
*
|
|
43
|
+
* @param role - Agent role (dev, sm, qa)
|
|
44
|
+
* @param flagValue - Optional CLI flag override
|
|
45
|
+
* @returns Resolved asset with path and source
|
|
46
|
+
*/
|
|
47
|
+
resolveAgent(role, flagValue) {
|
|
48
|
+
// Level 1: CLI flag
|
|
49
|
+
const flag = flagValue || this.getFlagForRole(role);
|
|
50
|
+
if (flag) {
|
|
51
|
+
return { path: this.resolveAgentValue(flag), source: 'flag' };
|
|
52
|
+
}
|
|
53
|
+
// Level 2: Config file
|
|
54
|
+
const configKey = `${role}_agent`;
|
|
55
|
+
const configValue = this.config?.[configKey];
|
|
56
|
+
if (configValue) {
|
|
57
|
+
return { path: this.resolveAgentValue(configValue), source: 'config' };
|
|
58
|
+
}
|
|
59
|
+
// Level 3: Bundled default
|
|
60
|
+
return {
|
|
61
|
+
path: join(this.bundledPath, 'agents', `${role}.md`),
|
|
62
|
+
source: 'bundled',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a template file path.
|
|
67
|
+
*
|
|
68
|
+
* Checks bmad_path config directory first, falls back to bundled.
|
|
69
|
+
*
|
|
70
|
+
* @param name - Template filename (e.g., 'epic-tmpl.yaml')
|
|
71
|
+
* @returns Resolved asset with path and source
|
|
72
|
+
*/
|
|
73
|
+
resolveTemplate(name) {
|
|
74
|
+
// Config bmad_path override (resolved relative to project root)
|
|
75
|
+
if (this.config?.bmad_path) {
|
|
76
|
+
const local = join(process.cwd(), this.config.bmad_path, 'templates', name);
|
|
77
|
+
if (fs.existsSync(local)) {
|
|
78
|
+
return { path: local, source: 'config' };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Bundled default
|
|
82
|
+
return {
|
|
83
|
+
path: join(this.bundledPath, 'templates', name),
|
|
84
|
+
source: 'bundled',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the config file path.
|
|
89
|
+
*
|
|
90
|
+
* Returns the absolute path to the bundled default-config.yaml.
|
|
91
|
+
* Callers (PathResolver) decide whether to prefer local .bmad-core/ over this.
|
|
92
|
+
*
|
|
93
|
+
* @returns Absolute path to bundled config file
|
|
94
|
+
*/
|
|
95
|
+
resolveConfig() {
|
|
96
|
+
return join(this.bundledPath, 'config', 'default-config.yaml');
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Extract execution defaults from the loaded config file.
|
|
100
|
+
*
|
|
101
|
+
* Parses and validates: parallel, timeout, provider, model, qa_enabled, pipeline.
|
|
102
|
+
* Invalid values are silently ignored (field omitted from result).
|
|
103
|
+
*
|
|
104
|
+
* @returns Parsed execution defaults, or empty object if no config file
|
|
105
|
+
*/
|
|
106
|
+
getExecutionDefaults() {
|
|
107
|
+
if (!this.config)
|
|
108
|
+
return {};
|
|
109
|
+
const defaults = {};
|
|
110
|
+
// parallel — must be positive integer
|
|
111
|
+
if (this.config.parallel !== undefined) {
|
|
112
|
+
const val = Number(this.config.parallel);
|
|
113
|
+
if (Number.isInteger(val) && val > 0) {
|
|
114
|
+
defaults.parallel = val;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
console.warn(`[bmad-workflow] Ignoring invalid config value parallel: ${this.config.parallel} (expected positive integer)`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// timeout — human-readable string or number, converted to ms
|
|
121
|
+
if (this.config.timeout !== undefined) {
|
|
122
|
+
try {
|
|
123
|
+
defaults.timeout = parseDuration(this.config.timeout);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
console.warn(`[bmad-workflow] Ignoring invalid config value timeout: ${this.config.timeout}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// provider — must be one of valid providers
|
|
130
|
+
if (this.config.provider !== undefined) {
|
|
131
|
+
const val = String(this.config.provider);
|
|
132
|
+
if (VALID_PROVIDERS.includes(val)) {
|
|
133
|
+
defaults.provider = val;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
console.warn(`[bmad-workflow] Ignoring invalid config value provider: ${val} (expected: ${VALID_PROVIDERS.join(', ')})`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// model — any non-empty string
|
|
140
|
+
if (this.config.model !== undefined && typeof this.config.model === 'string' && this.config.model.trim()) {
|
|
141
|
+
defaults.model = this.config.model.trim();
|
|
142
|
+
}
|
|
143
|
+
// qa_enabled — must be boolean
|
|
144
|
+
if (this.config.qa_enabled !== undefined) {
|
|
145
|
+
if (typeof this.config.qa_enabled === 'boolean') {
|
|
146
|
+
defaults.qa_enabled = this.config.qa_enabled;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
console.warn(`[bmad-workflow] Ignoring invalid config value qa_enabled: ${this.config.qa_enabled} (expected boolean)`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// pipeline — must be boolean
|
|
153
|
+
if (this.config.pipeline !== undefined) {
|
|
154
|
+
if (typeof this.config.pipeline === 'boolean') {
|
|
155
|
+
defaults.pipeline = this.config.pipeline;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.warn(`[bmad-workflow] Ignoring invalid config value pipeline: ${this.config.pipeline} (expected boolean)`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return defaults;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Compute the bundled assets root path.
|
|
165
|
+
*
|
|
166
|
+
* From compiled dist/services/file-system/asset-resolver.js,
|
|
167
|
+
* the assets directory is three levels up: ../../.. → <pkg>/assets/
|
|
168
|
+
*
|
|
169
|
+
* @returns Absolute path to bundled assets directory
|
|
170
|
+
*/
|
|
171
|
+
computeBundledPath() {
|
|
172
|
+
// Support both ESM (__dirname equivalent) and compiled contexts
|
|
173
|
+
try {
|
|
174
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
175
|
+
const currentDir = dirname(currentFile);
|
|
176
|
+
return join(currentDir, '..', '..', '..', 'assets');
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Fallback for environments where import.meta.url is not available
|
|
180
|
+
return join(__dirname, '..', '..', 'assets');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get CLI flag value for a given role.
|
|
185
|
+
*/
|
|
186
|
+
getFlagForRole(role) {
|
|
187
|
+
const map = {
|
|
188
|
+
dev: this.flags.devAgent,
|
|
189
|
+
qa: this.flags.qaAgent,
|
|
190
|
+
sm: this.flags.smAgent,
|
|
191
|
+
};
|
|
192
|
+
return map[role];
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Load .bmad-workflow.yaml from project root (if exists).
|
|
196
|
+
*
|
|
197
|
+
* @returns Parsed config or null if file absent
|
|
198
|
+
*/
|
|
199
|
+
loadConfigFile() {
|
|
200
|
+
const configPath = join(process.cwd(), '.bmad-workflow.yaml');
|
|
201
|
+
if (!fs.existsSync(configPath))
|
|
202
|
+
return null;
|
|
203
|
+
try {
|
|
204
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
205
|
+
return yamlLoad(content);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Resolve an agent value — could be a built-in name or a file path.
|
|
213
|
+
*
|
|
214
|
+
* Built-in names (no path separators, no .md extension) map to bundled agents.
|
|
215
|
+
* File paths are resolved relative to project root.
|
|
216
|
+
*
|
|
217
|
+
* @param value - Agent name or file path
|
|
218
|
+
* @returns Absolute path to agent file
|
|
219
|
+
*/
|
|
220
|
+
resolveAgentValue(value) {
|
|
221
|
+
// Built-in name (no path separators, no extension)
|
|
222
|
+
if (!value.includes('/') && !value.endsWith('.md')) {
|
|
223
|
+
// Check bmad_path first (resolved relative to project root)
|
|
224
|
+
if (this.config?.bmad_path) {
|
|
225
|
+
const local = join(process.cwd(), this.config.bmad_path, 'agents', `${value}.md`);
|
|
226
|
+
if (fs.existsSync(local))
|
|
227
|
+
return local;
|
|
228
|
+
}
|
|
229
|
+
return join(this.bundledPath, 'agents', `${value}.md`);
|
|
230
|
+
}
|
|
231
|
+
// File path — resolve relative to project root
|
|
232
|
+
return resolve(value);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -37,6 +37,19 @@ export declare class FileManager {
|
|
|
37
37
|
* await fileManager.createDirectory('docs/epics')
|
|
38
38
|
*/
|
|
39
39
|
createDirectory(path: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Copy a file from source to destination
|
|
42
|
+
*
|
|
43
|
+
* Creates parent directories if they don't exist.
|
|
44
|
+
* Overwrites destination file if it exists.
|
|
45
|
+
*
|
|
46
|
+
* @param source - Source file path
|
|
47
|
+
* @param dest - Destination file path
|
|
48
|
+
* @throws {FileSystemError} If file cannot be copied (source not found, permission denied, etc.)
|
|
49
|
+
* @example
|
|
50
|
+
* await fileManager.copyFile('assets/agents/dev.md', '.bmad-core/agents/dev.md')
|
|
51
|
+
*/
|
|
52
|
+
copyFile(source: string, dest: string): Promise<void>;
|
|
40
53
|
/**
|
|
41
54
|
* Check if a file or directory exists
|
|
42
55
|
*
|
|
@@ -56,6 +56,38 @@ export class FileManager {
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Copy a file from source to destination
|
|
61
|
+
*
|
|
62
|
+
* Creates parent directories if they don't exist.
|
|
63
|
+
* Overwrites destination file if it exists.
|
|
64
|
+
*
|
|
65
|
+
* @param source - Source file path
|
|
66
|
+
* @param dest - Destination file path
|
|
67
|
+
* @throws {FileSystemError} If file cannot be copied (source not found, permission denied, etc.)
|
|
68
|
+
* @example
|
|
69
|
+
* await fileManager.copyFile('assets/agents/dev.md', '.bmad-core/agents/dev.md')
|
|
70
|
+
*/
|
|
71
|
+
async copyFile(source, dest) {
|
|
72
|
+
this.logger.info('Copying file from %s to %s', source, dest);
|
|
73
|
+
try {
|
|
74
|
+
// Ensure destination directory exists
|
|
75
|
+
await fs.ensureDir(dirname(dest));
|
|
76
|
+
// Copy file
|
|
77
|
+
await fs.copy(source, dest, { overwrite: true });
|
|
78
|
+
this.logger.info('File copied successfully from %s to %s', source, dest);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const err = error;
|
|
82
|
+
this.logger.error('Error copying file from %s to %s: %O', source, dest, err);
|
|
83
|
+
throw new FileSystemError(`Failed to copy file from ${source} to ${dest}: ${err.message}`, {
|
|
84
|
+
dest,
|
|
85
|
+
operation: 'copyFile',
|
|
86
|
+
originalError: err.message,
|
|
87
|
+
source,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
59
91
|
/**
|
|
60
92
|
* Check if a file or directory exists
|
|
61
93
|
*
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type pino from 'pino';
|
|
8
8
|
import type { ReviewConfig } from '../validation/config-validator.js';
|
|
9
|
+
import { AssetResolver } from './asset-resolver.js';
|
|
9
10
|
import { FileManager } from './file-manager.js';
|
|
10
11
|
/**
|
|
11
12
|
* PathResolver service for resolving and validating file paths
|
|
@@ -15,10 +16,18 @@ import { FileManager } from './file-manager.js';
|
|
|
15
16
|
* are resolved relative to the project root and validated for existence.
|
|
16
17
|
*/
|
|
17
18
|
export declare class PathResolver {
|
|
19
|
+
/**
|
|
20
|
+
* AssetResolver for bundled asset fallback
|
|
21
|
+
*/
|
|
22
|
+
private readonly assetResolver;
|
|
18
23
|
/**
|
|
19
24
|
* Cached configuration to avoid repeated file reads
|
|
20
25
|
*/
|
|
21
26
|
private cachedConfig;
|
|
27
|
+
/**
|
|
28
|
+
* Cached path to the configuration file (local or bundled)
|
|
29
|
+
*/
|
|
30
|
+
private cachedConfigPath;
|
|
22
31
|
/**
|
|
23
32
|
* Cached resolved paths
|
|
24
33
|
*/
|
|
@@ -40,12 +49,13 @@ export declare class PathResolver {
|
|
|
40
49
|
*
|
|
41
50
|
* @param fileManager - FileManager instance for file operations
|
|
42
51
|
* @param logger - Pino logger instance for logging path operations
|
|
52
|
+
* @param assetResolver - Optional AssetResolver instance (DI for testability)
|
|
43
53
|
* @example
|
|
44
54
|
* const logger = createLogger({ namespace: 'services:path-resolver' })
|
|
45
55
|
* const fileManager = new FileManager(logger)
|
|
46
56
|
* const pathResolver = new PathResolver(fileManager, logger)
|
|
47
57
|
*/
|
|
48
|
-
constructor(fileManager: FileManager, logger: pino.Logger);
|
|
58
|
+
constructor(fileManager: FileManager, logger: pino.Logger, assetResolver?: AssetResolver);
|
|
49
59
|
/**
|
|
50
60
|
* Get all story directories for existence checks
|
|
51
61
|
*
|
|
@@ -170,6 +180,17 @@ export declare class PathResolver {
|
|
|
170
180
|
* @throws {ConfigurationError} If configuration structure is invalid
|
|
171
181
|
*/
|
|
172
182
|
private loadConfig;
|
|
183
|
+
/**
|
|
184
|
+
* Read, parse, and validate a config file at the given path.
|
|
185
|
+
*
|
|
186
|
+
* Sets cachedConfig and cachedConfigPath on success.
|
|
187
|
+
* Applies hardcoded defaults for missing fields.
|
|
188
|
+
*
|
|
189
|
+
* @param configPath - Absolute path to YAML config file
|
|
190
|
+
* @returns Parsed and validated configuration
|
|
191
|
+
* @throws {ConfigurationError} If configuration structure is invalid
|
|
192
|
+
*/
|
|
193
|
+
private readAndParseConfig;
|
|
173
194
|
/**
|
|
174
195
|
* Validate configuration structure
|
|
175
196
|
*
|
|
@@ -8,6 +8,7 @@ import fs from 'fs-extra';
|
|
|
8
8
|
import { load as yamlLoad } from 'js-yaml';
|
|
9
9
|
import { dirname, resolve } from 'node:path';
|
|
10
10
|
import { ConfigurationError, FileSystemError } from '../../utils/errors.js';
|
|
11
|
+
import { AssetResolver } from './asset-resolver.js';
|
|
11
12
|
/**
|
|
12
13
|
* Configuration file path relative to project root
|
|
13
14
|
*/
|
|
@@ -20,10 +21,18 @@ const CONFIG_FILE_PATH = '.bmad-core/core-config.yaml';
|
|
|
20
21
|
* are resolved relative to the project root and validated for existence.
|
|
21
22
|
*/
|
|
22
23
|
export class PathResolver {
|
|
24
|
+
/**
|
|
25
|
+
* AssetResolver for bundled asset fallback
|
|
26
|
+
*/
|
|
27
|
+
assetResolver;
|
|
23
28
|
/**
|
|
24
29
|
* Cached configuration to avoid repeated file reads
|
|
25
30
|
*/
|
|
26
31
|
cachedConfig = null;
|
|
32
|
+
/**
|
|
33
|
+
* Cached path to the configuration file (local or bundled)
|
|
34
|
+
*/
|
|
35
|
+
cachedConfigPath = null;
|
|
27
36
|
/**
|
|
28
37
|
* Cached resolved paths
|
|
29
38
|
*/
|
|
@@ -45,14 +54,16 @@ export class PathResolver {
|
|
|
45
54
|
*
|
|
46
55
|
* @param fileManager - FileManager instance for file operations
|
|
47
56
|
* @param logger - Pino logger instance for logging path operations
|
|
57
|
+
* @param assetResolver - Optional AssetResolver instance (DI for testability)
|
|
48
58
|
* @example
|
|
49
59
|
* const logger = createLogger({ namespace: 'services:path-resolver' })
|
|
50
60
|
* const fileManager = new FileManager(logger)
|
|
51
61
|
* const pathResolver = new PathResolver(fileManager, logger)
|
|
52
62
|
*/
|
|
53
|
-
constructor(fileManager, logger) {
|
|
63
|
+
constructor(fileManager, logger, assetResolver) {
|
|
54
64
|
this.fileManager = fileManager;
|
|
55
65
|
this.logger = logger;
|
|
66
|
+
this.assetResolver = assetResolver || new AssetResolver({});
|
|
56
67
|
this.projectRoot = process.cwd();
|
|
57
68
|
this.logger.debug('PathResolver initialized with project root: %s', this.projectRoot);
|
|
58
69
|
}
|
|
@@ -87,7 +98,11 @@ export class PathResolver {
|
|
|
87
98
|
* // Returns: '/path/to/project/.bmad-core/core-config.yaml'
|
|
88
99
|
*/
|
|
89
100
|
getConfigPath() {
|
|
90
|
-
|
|
101
|
+
// Trigger config load if not yet loaded (populates cachedConfigPath)
|
|
102
|
+
if (!this.cachedConfigPath) {
|
|
103
|
+
this.loadConfig();
|
|
104
|
+
}
|
|
105
|
+
const configPath = this.cachedConfigPath;
|
|
91
106
|
this.logger.debug('Getting config path: %s', configPath);
|
|
92
107
|
return configPath;
|
|
93
108
|
}
|
|
@@ -274,23 +289,35 @@ export class PathResolver {
|
|
|
274
289
|
}
|
|
275
290
|
const configPath = this.findConfigFile();
|
|
276
291
|
if (!configPath) {
|
|
277
|
-
|
|
278
|
-
this.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
operation: 'loadConfig',
|
|
282
|
-
});
|
|
292
|
+
// Fallback to bundled default config via AssetResolver
|
|
293
|
+
const bundledPath = this.assetResolver.resolveConfig();
|
|
294
|
+
this.logger.info('No local config found, falling back to bundled default: %s', bundledPath);
|
|
295
|
+
return this.readAndParseConfig(bundledPath);
|
|
283
296
|
}
|
|
284
297
|
this.logger.info('Loading configuration from: %s', configPath);
|
|
298
|
+
return this.readAndParseConfig(configPath);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Read, parse, and validate a config file at the given path.
|
|
302
|
+
*
|
|
303
|
+
* Sets cachedConfig and cachedConfigPath on success.
|
|
304
|
+
* Applies hardcoded defaults for missing fields.
|
|
305
|
+
*
|
|
306
|
+
* @param configPath - Absolute path to YAML config file
|
|
307
|
+
* @returns Parsed and validated configuration
|
|
308
|
+
* @throws {ConfigurationError} If configuration structure is invalid
|
|
309
|
+
*/
|
|
310
|
+
readAndParseConfig(configPath) {
|
|
285
311
|
try {
|
|
286
312
|
// Read configuration file
|
|
287
313
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
288
|
-
this.logger.debug('Configuration file read successfully');
|
|
314
|
+
this.logger.debug('Configuration file read successfully from: %s', configPath);
|
|
289
315
|
// Parse YAML
|
|
290
316
|
const config = yamlLoad(content);
|
|
291
317
|
// Validate required fields
|
|
292
318
|
this.validateConfig(config);
|
|
293
319
|
this.cachedConfig = config;
|
|
320
|
+
this.cachedConfigPath = configPath;
|
|
294
321
|
this.logger.info('Configuration loaded and validated successfully');
|
|
295
322
|
return config;
|
|
296
323
|
}
|
|
@@ -308,6 +308,7 @@ Use the file at the path above to document:
|
|
|
308
308
|
// Execute agent
|
|
309
309
|
const result = await this.agentRunner.runAgent(fullPrompt, {
|
|
310
310
|
agentType,
|
|
311
|
+
cwd: this.cwd,
|
|
311
312
|
model: this.model,
|
|
312
313
|
references: task.targetFiles, // Pass target files as references
|
|
313
314
|
timeout: task.estimatedMinutes * 60 * 1000 * 1.5, // 1.5x estimated time as buffer
|
|
@@ -47,6 +47,7 @@ import type pino from 'pino';
|
|
|
47
47
|
import type { InputDetectionResult, WorkflowCallbacks, WorkflowConfig, WorkflowResult } from '../../models/index.js';
|
|
48
48
|
import type { AIProviderRunner } from '../agents/agent-runner.js';
|
|
49
49
|
import type { WorkflowLogger } from '../logging/workflow-logger.js';
|
|
50
|
+
import { AssetResolver } from '../file-system/asset-resolver.js';
|
|
50
51
|
import { FileManager } from '../file-system/file-manager.js';
|
|
51
52
|
import { PathResolver } from '../file-system/path-resolver.js';
|
|
52
53
|
import { EpicParser } from '../parsers/epic-parser.js';
|
|
@@ -79,6 +80,8 @@ export interface InputDetector {
|
|
|
79
80
|
export interface WorkflowOrchestratorConfig {
|
|
80
81
|
/** Service to execute AI agents (Claude or Gemini) */
|
|
81
82
|
agentRunner: AIProviderRunner;
|
|
83
|
+
/** Optional AssetResolver for three-level path resolution (CLI flag → config → bundled) */
|
|
84
|
+
assetResolver?: AssetResolver;
|
|
82
85
|
/** Service to handle parallel batch processing */
|
|
83
86
|
batchProcessor: BatchProcessor;
|
|
84
87
|
/** Service to parse epic files and extract stories */
|
|
@@ -147,6 +150,7 @@ export interface StoryPromptOptions {
|
|
|
147
150
|
*/
|
|
148
151
|
export declare class WorkflowOrchestrator {
|
|
149
152
|
private readonly agentRunner;
|
|
153
|
+
private readonly assetResolver;
|
|
150
154
|
private readonly batchProcessor;
|
|
151
155
|
private readonly callbacks?;
|
|
152
156
|
private readonly epicParser;
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
import { isEpicStory } from '../../models/story.js';
|
|
47
47
|
import { ParserError, ValidationError } from '../../utils/errors.js';
|
|
48
48
|
import { runAgentWithRetry } from '../../utils/retry.js';
|
|
49
|
+
import { AssetResolver } from '../file-system/asset-resolver.js';
|
|
49
50
|
import { PrdFixer } from '../parsers/prd-fixer.js';
|
|
50
51
|
import { FileScaffolder } from '../scaffolding/file-scaffolder.js';
|
|
51
52
|
import { BatchProcessor } from './batch-processor.js';
|
|
@@ -60,6 +61,7 @@ import { ReviewQueue } from '../review/review-queue.js';
|
|
|
60
61
|
*/
|
|
61
62
|
export class WorkflowOrchestrator {
|
|
62
63
|
agentRunner;
|
|
64
|
+
assetResolver;
|
|
63
65
|
batchProcessor;
|
|
64
66
|
callbacks;
|
|
65
67
|
epicParser;
|
|
@@ -80,6 +82,7 @@ export class WorkflowOrchestrator {
|
|
|
80
82
|
* @param config - Configuration object containing all service dependencies
|
|
81
83
|
*/
|
|
82
84
|
constructor(config) {
|
|
85
|
+
this.assetResolver = config.assetResolver ?? new AssetResolver({});
|
|
83
86
|
this.inputDetector = config.inputDetector;
|
|
84
87
|
this.prdParser = config.prdParser;
|
|
85
88
|
this.epicParser = config.epicParser;
|
|
@@ -173,7 +176,9 @@ export class WorkflowOrchestrator {
|
|
|
173
176
|
const { cwd, outputPath, prdPath, references, smAgent } = options;
|
|
174
177
|
const referencesText = references.length > 0 ? `\nReferences: ${references.join(', ')}` : '';
|
|
175
178
|
const cwdText = cwd ? `\n\nWorking directory: ${cwd}` : '';
|
|
176
|
-
const
|
|
179
|
+
const resolved = this.assetResolver.resolveAgent('sm', smAgent);
|
|
180
|
+
const agentRef = `@${resolved.path}`;
|
|
181
|
+
const templateResolved = this.assetResolver.resolveTemplate('epic-tmpl.yaml');
|
|
177
182
|
return `${agentRef}${cwdText}
|
|
178
183
|
|
|
179
184
|
Create epic '${epic.number}: ${epic.title}' for PRD '${prdPath}'.${referencesText}.
|
|
@@ -182,7 +187,7 @@ IMPORTANT: The file at '${outputPath}' has been pre-scaffolded with structure an
|
|
|
182
187
|
- DO NOT modify the Epic Header section (Epic ID, Status: Draft, Created date are already set)
|
|
183
188
|
- DO NOT change the document structure or section headers
|
|
184
189
|
- ONLY populate the empty content sections marked with [AI Agent will populate]
|
|
185
|
-
- Follow the template structure at
|
|
190
|
+
- Follow the template structure at @${templateResolved.path} for content guidance
|
|
186
191
|
|
|
187
192
|
Write output to: '${outputPath}'`;
|
|
188
193
|
}
|
|
@@ -220,7 +225,9 @@ Write output to: '${outputPath}'`;
|
|
|
220
225
|
const { cwd, epicPath, outputPath, references, smAgent } = options;
|
|
221
226
|
const referencesText = references.length > 0 ? `\nReferences: ${references.join(', ')}` : '';
|
|
222
227
|
const cwdText = cwd ? `\n\nWorking directory: ${cwd}` : '';
|
|
223
|
-
const
|
|
228
|
+
const resolved = this.assetResolver.resolveAgent('sm', smAgent);
|
|
229
|
+
const agentRef = `@${resolved.path}`;
|
|
230
|
+
const templateResolved = this.assetResolver.resolveTemplate('story-tmpl.yaml');
|
|
224
231
|
return `${agentRef}${cwdText}
|
|
225
232
|
|
|
226
233
|
Create story '${story.fullNumber}: ${story.title}' for epic '${epicPath}'. ${referencesText}
|
|
@@ -230,7 +237,7 @@ IMPORTANT: The file at '${outputPath}' has been pre-scaffolded with structure an
|
|
|
230
237
|
- DO NOT modify the Created date in Change Log
|
|
231
238
|
- DO NOT change the document structure or section headers
|
|
232
239
|
- ONLY populate the empty content sections marked with [AI Agent will populate]
|
|
233
|
-
- Follow the template structure at
|
|
240
|
+
- Follow the template structure at @${templateResolved.path} for content guidance
|
|
234
241
|
|
|
235
242
|
Write output to: ${outputPath}`;
|
|
236
243
|
}
|
|
@@ -626,7 +633,8 @@ Write output to: ${outputPath}`;
|
|
|
626
633
|
// Build prompt with auto-detected references
|
|
627
634
|
const mcpPrefix = await this.getMcpPromptPrefix('dev', 'dev', config);
|
|
628
635
|
let prompt = mcpPrefix ? `${mcpPrefix}\n\n` : '';
|
|
629
|
-
|
|
636
|
+
const devResolved = this.assetResolver.resolveAgent('dev', config.devAgent);
|
|
637
|
+
prompt += `@${devResolved.path}\n\n`;
|
|
630
638
|
// Add working directory instruction if specified
|
|
631
639
|
if (config.cwd) {
|
|
632
640
|
prompt += `Working directory: ${config.cwd}\n\n`;
|
|
@@ -665,6 +673,7 @@ Write output to: ${outputPath}`;
|
|
|
665
673
|
// Execute dev agent with retry on timeout/killed
|
|
666
674
|
const result = await runAgentWithRetry(this.agentRunner, prompt, {
|
|
667
675
|
agentType: 'dev',
|
|
676
|
+
cwd: config.cwd,
|
|
668
677
|
references: config.references,
|
|
669
678
|
timeout: config.timeout ?? 2_700_000,
|
|
670
679
|
}, {
|
|
@@ -907,7 +916,8 @@ Write output to: ${outputPath}`;
|
|
|
907
916
|
// Build prompt with auto-detected references
|
|
908
917
|
const mcpPrefix = await this.getMcpPromptPrefix('dev', 'dev', config);
|
|
909
918
|
let prompt = mcpPrefix ? `${mcpPrefix}\n\n` : '';
|
|
910
|
-
|
|
919
|
+
const devResolved = this.assetResolver.resolveAgent('dev', config.devAgent);
|
|
920
|
+
prompt += `@${devResolved.path}\n\n`;
|
|
911
921
|
// Add working directory instruction if specified
|
|
912
922
|
if (config.cwd) {
|
|
913
923
|
prompt += `Working directory: ${config.cwd}\n\n`;
|
|
@@ -945,6 +955,7 @@ Write output to: ${outputPath}`;
|
|
|
945
955
|
});
|
|
946
956
|
const result = await runAgentWithRetry(this.agentRunner, prompt, {
|
|
947
957
|
agentType: 'dev',
|
|
958
|
+
cwd: config.cwd,
|
|
948
959
|
references: config.references,
|
|
949
960
|
timeout: config.timeout ?? 2_700_000,
|
|
950
961
|
}, {
|
|
@@ -1270,6 +1281,7 @@ Write output to: ${outputPath}`;
|
|
|
1270
1281
|
// Step 3: Run Claude agent to populate content sections
|
|
1271
1282
|
const result = await runAgentWithRetry(this.agentRunner, prompt, {
|
|
1272
1283
|
agentType: 'architect',
|
|
1284
|
+
cwd: config.cwd,
|
|
1273
1285
|
references: config.references,
|
|
1274
1286
|
timeout: config.timeout ?? 2_700_000,
|
|
1275
1287
|
}, {
|
|
@@ -2262,6 +2274,7 @@ Write output to: ${outputPath}`;
|
|
|
2262
2274
|
// Step 4: Run Claude agent to populate content sections
|
|
2263
2275
|
const result = await runAgentWithRetry(this.agentRunner, prompt, {
|
|
2264
2276
|
agentType: 'sm',
|
|
2277
|
+
cwd: config.cwd,
|
|
2265
2278
|
references: config.references,
|
|
2266
2279
|
timeout: config.timeout ?? 2_700_000,
|
|
2267
2280
|
}, {
|
|
@@ -2588,6 +2601,7 @@ Write output to: ${outputPath}`;
|
|
|
2588
2601
|
// Step 4: Run Claude agent to populate content sections
|
|
2589
2602
|
const result = await runAgentWithRetry(this.agentRunner, prompt, {
|
|
2590
2603
|
agentType: 'sm',
|
|
2604
|
+
cwd: config.cwd,
|
|
2591
2605
|
references: config.references,
|
|
2592
2606
|
timeout: config.timeout ?? 2_700_000,
|
|
2593
2607
|
}, {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Merge Utility
|
|
3
|
+
*
|
|
4
|
+
* Implements three-layer merge for execution config:
|
|
5
|
+
* CLI flags (highest) → .bmad-workflow.yaml → hardcoded defaults (lowest)
|
|
6
|
+
*
|
|
7
|
+
* Pure function — no side effects, easy to test.
|
|
8
|
+
*/
|
|
9
|
+
import type { ConfigExecutionDefaults } from '../services/file-system/asset-resolver.js';
|
|
10
|
+
/**
|
|
11
|
+
* Merged execution config with all fields resolved
|
|
12
|
+
*/
|
|
13
|
+
export interface MergedExecutionConfig {
|
|
14
|
+
epicInterval: number;
|
|
15
|
+
maxRetries: number;
|
|
16
|
+
model: string | undefined;
|
|
17
|
+
parallel: number;
|
|
18
|
+
pipeline: boolean;
|
|
19
|
+
prdInterval: number;
|
|
20
|
+
provider: 'claude' | 'gemini' | 'opencode';
|
|
21
|
+
qa: boolean;
|
|
22
|
+
retryBackoffMs: number;
|
|
23
|
+
storyInterval: number;
|
|
24
|
+
timeout: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* CLI flags that may be undefined when user didn't pass them.
|
|
28
|
+
* Only fields explicitly passed by the user should be present.
|
|
29
|
+
*/
|
|
30
|
+
export interface CliExecutionFlags {
|
|
31
|
+
epicInterval?: number;
|
|
32
|
+
maxRetries?: number;
|
|
33
|
+
model?: string;
|
|
34
|
+
parallel?: number;
|
|
35
|
+
pipeline?: boolean;
|
|
36
|
+
prdInterval?: number;
|
|
37
|
+
provider?: string;
|
|
38
|
+
qa?: boolean;
|
|
39
|
+
retryBackoffMs?: number;
|
|
40
|
+
storyInterval?: number;
|
|
41
|
+
timeout?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Hardcoded defaults — final fallback when neither CLI nor config provides a value
|
|
45
|
+
*/
|
|
46
|
+
export declare const EXECUTION_DEFAULTS: MergedExecutionConfig;
|
|
47
|
+
/**
|
|
48
|
+
* Merge execution config from three layers: CLI flags → config file → hardcoded defaults.
|
|
49
|
+
*
|
|
50
|
+
* For each field, the first defined value wins (left to right):
|
|
51
|
+
* 1. CLI flag (explicit user input)
|
|
52
|
+
* 2. Config file value (from .bmad-workflow.yaml)
|
|
53
|
+
* 3. Hardcoded default
|
|
54
|
+
*
|
|
55
|
+
* @param cliFlags - Flags explicitly passed via CLI (undefined = not passed)
|
|
56
|
+
* @param configFile - Execution defaults from .bmad-workflow.yaml
|
|
57
|
+
* @param defaults - Hardcoded fallback values
|
|
58
|
+
* @returns Fully resolved execution config
|
|
59
|
+
*/
|
|
60
|
+
export declare function mergeExecutionConfig(cliFlags: CliExecutionFlags, configFile: ConfigExecutionDefaults, defaults?: MergedExecutionConfig): MergedExecutionConfig;
|