@hyperdrive.bot/bmad-workflow 1.0.22 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/assets/agents/dev-barry.md +69 -0
  2. package/assets/agents/dev.md +323 -0
  3. package/assets/agents/qa.md +92 -0
  4. package/assets/agents/sm-bob.md +65 -0
  5. package/assets/agents/sm.md +296 -0
  6. package/assets/config/default-config.yaml +6 -0
  7. package/assets/templates/epic-tmpl.yaml +277 -0
  8. package/assets/templates/prd-tmpl.yaml +261 -0
  9. package/assets/templates/qa-gate-tmpl.yaml +103 -0
  10. package/assets/templates/story-tmpl.yaml +138 -0
  11. package/dist/commands/eject.d.ts +76 -0
  12. package/dist/commands/eject.js +232 -0
  13. package/dist/commands/init.d.ts +47 -0
  14. package/dist/commands/init.js +265 -0
  15. package/dist/commands/stories/develop.js +1 -0
  16. package/dist/commands/stories/qa.d.ts +1 -0
  17. package/dist/commands/stories/qa.js +7 -0
  18. package/dist/commands/workflow.d.ts +6 -3
  19. package/dist/commands/workflow.js +106 -26
  20. package/dist/models/bmad-config-schema.d.ts +51 -0
  21. package/dist/models/bmad-config-schema.js +53 -0
  22. package/dist/services/agents/gemini-agent-runner.js +7 -2
  23. package/dist/services/agents/opencode-agent-runner.js +7 -2
  24. package/dist/services/file-system/asset-resolver.d.ts +117 -0
  25. package/dist/services/file-system/asset-resolver.js +234 -0
  26. package/dist/services/file-system/file-manager.d.ts +13 -0
  27. package/dist/services/file-system/file-manager.js +32 -0
  28. package/dist/services/file-system/path-resolver.d.ts +22 -1
  29. package/dist/services/file-system/path-resolver.js +36 -9
  30. package/dist/services/orchestration/dependency-graph-executor.js +1 -0
  31. package/dist/services/orchestration/workflow-orchestrator.d.ts +4 -0
  32. package/dist/services/orchestration/workflow-orchestrator.js +20 -6
  33. package/dist/utils/config-merge.d.ts +60 -0
  34. package/dist/utils/config-merge.js +52 -0
  35. 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
- const configPath = resolve(this.projectRoot, CONFIG_FILE_PATH);
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
- const searchStart = resolve(this.projectRoot, CONFIG_FILE_PATH);
278
- this.logger.error('Configuration file not found after searching up to 5 parent directories');
279
- throw new FileSystemError(`Configuration file not found: ${searchStart} (searched up to 5 parent directories)`, {
280
- configPath: searchStart,
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 agentRef = smAgent ? `@${smAgent}` : `@.bmad-core/agents/sm.md`;
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 @.bmad-core/templates/epic-tmpl.yaml for content guidance
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 agentRef = smAgent ? `@${smAgent}` : `@.bmad-core/agents/sm.md`;
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 @.bmad-core/templates/story-tmpl.yaml for content guidance
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
- prompt += config.devAgent ? `@${config.devAgent}\n\n` : `@.bmad-core/agents/dev.md\n\n`;
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
- prompt += config.devAgent ? `@${config.devAgent}\n\n` : `@.bmad-core/agents/dev.md\n\n`;
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;