@hyperdrive.bot/bmad-workflow 1.0.21 → 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 +11 -0
  32. package/dist/services/orchestration/workflow-orchestrator.js +79 -10
  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;
@@ -730,6 +734,13 @@ export declare class WorkflowOrchestrator {
730
734
  * @private
731
735
  */
732
736
  private sleep;
737
+ /**
738
+ * Parse the `### Dev Context` section from a story file's content.
739
+ *
740
+ * Returns sidecar reference paths and an optional deploy target string.
741
+ * If the section is missing or empty, returns empty arrays / undefined (backward compatible).
742
+ */
743
+ private parseDevContext;
733
744
  /**
734
745
  * Update story status in file
735
746
  *