@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
@@ -1,6 +1,9 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
+ import { execSync } from 'node:child_process';
2
3
  import { basename } from 'node:path';
3
4
  import { parseDuration } from '../utils/duration.js';
5
+ import { AssetResolver } from '../services/file-system/asset-resolver.js';
6
+ import { mergeExecutionConfig } from '../utils/config-merge.js';
4
7
  import { createAgentRunner, isProviderSupported } from '../services/agents/agent-runner-factory.js';
5
8
  import { getRegisteredScannerNames } from '../services/review/scanner-factory.js';
6
9
  import { Severity } from '../services/review/types.js';
@@ -55,7 +58,21 @@ export default class Workflow extends Command {
55
58
  description: 'Auto-fix PRD format using AI when parsing fails',
56
59
  }),
57
60
  cwd: Flags.string({
58
- description: 'Working directory path to pass to AI agents. Agents will operate in this directory.',
61
+ description: 'Working directory path to pass to AI agents. Agents will operate in this directory. When using --worktree, this is set automatically.',
62
+ }),
63
+ worktree: Flags.boolean({
64
+ default: false,
65
+ description: 'Create an isolated gut worktree before running the workflow. Requires --gut-entities.',
66
+ helpGroup: 'Worktree Isolation',
67
+ }),
68
+ 'gut-entities': Flags.string({
69
+ description: 'Comma-separated gut entity names to focus before worktree creation (e.g., "serverless-api,sign"). Required with --worktree.',
70
+ helpGroup: 'Worktree Isolation',
71
+ }),
72
+ 'worktree-install': Flags.boolean({
73
+ default: false,
74
+ description: 'Run pnpm install in the worktree after creation',
75
+ helpGroup: 'Worktree Isolation',
59
76
  }),
60
77
  'dev-agent': Flags.string({
61
78
  description: 'Absolute path to a custom dev agent file (default: .bmad-core/agents/dev.md)',
@@ -72,13 +89,11 @@ export default class Workflow extends Command {
72
89
  description: 'Seconds between story creation batches',
73
90
  }),
74
91
  parallel: Flags.integer({
75
- default: 3,
76
- description: 'Max concurrent epic/story creations',
92
+ description: 'Max concurrent epic/story creations (default: 3, overridden by .bmad-workflow.yaml)',
77
93
  }),
78
94
  pipeline: Flags.boolean({
79
95
  allowNo: true,
80
- default: true,
81
- description: 'Enable pipelined workflow (start dev on stories sequentially as they are created)',
96
+ description: 'Enable pipelined workflow (default: true, overridden by .bmad-workflow.yaml)',
82
97
  }),
83
98
  'prd-interval': Flags.integer({
84
99
  default: 60,
@@ -95,8 +110,7 @@ export default class Workflow extends Command {
95
110
  description: 'Session directory prefix (default: derived from input filename, e.g., PRD-feature.md → feature)',
96
111
  }),
97
112
  provider: Flags.string({
98
- default: 'claude',
99
- description: 'AI provider to use (claude, gemini, or opencode)',
113
+ description: 'AI provider to use (default: claude, overridden by .bmad-workflow.yaml)',
100
114
  options: ['claude', 'gemini', 'opencode'],
101
115
  }),
102
116
  mcp: Flags.boolean({
@@ -131,8 +145,8 @@ export default class Workflow extends Command {
131
145
  multiple: true,
132
146
  }),
133
147
  qa: Flags.boolean({
134
- default: false,
135
- description: 'Run QA workflow after development completes',
148
+ allowNo: true,
149
+ description: 'Run QA workflow after development completes (default: false, overridden by qa_enabled in .bmad-workflow.yaml)',
136
150
  helpGroup: 'QA Workflow',
137
151
  }),
138
152
  'qa-prompt': Flags.string({
@@ -167,8 +181,7 @@ export default class Workflow extends Command {
167
181
  timeout: Flags.custom({
168
182
  parse: async (input) => parseDuration(input),
169
183
  })({
170
- default: 2_700_000,
171
- description: 'Agent execution timeout — accepts durations like 30s, 5m, 1h, 90m, or raw milliseconds (default: 45m)',
184
+ description: 'Agent execution timeout — accepts durations like 30s, 5m, 1h, 90m, or raw milliseconds (default: 45m, overridden by .bmad-workflow.yaml)',
172
185
  }),
173
186
  'review-timeout': Flags.custom({
174
187
  parse: async (input) => parseDuration(input),
@@ -212,12 +225,79 @@ export default class Workflow extends Command {
212
225
  this.error(`File not found: ${inputPath}`, { exit: 1 });
213
226
  }
214
227
  }
228
+ // Worktree isolation: validate flags and create worktree if requested
229
+ if (flags.worktree) {
230
+ if (!flags['gut-entities']) {
231
+ this.error('--gut-entities is required when using --worktree. Specify comma-separated entity names (e.g., --gut-entities "serverless-api,sign").', { exit: 1 });
232
+ }
233
+ if (flags.cwd) {
234
+ this.error('--cwd and --worktree are mutually exclusive. --worktree sets cwd automatically.', { exit: 1 });
235
+ }
236
+ const entities = flags['gut-entities'].split(',').map((e) => e.trim()).filter(Boolean);
237
+ const branchSlug = `workflow/${basename(inputPath, '.md').replace(/^PRD-/, '').toLowerCase()}-${Date.now()}`;
238
+ const installFlag = flags['worktree-install'] ? '--install' : '';
239
+ this.log(colors.info(`Creating isolated worktree: ${branchSlug}`));
240
+ this.log(colors.info(`Entities: ${entities.join(', ')}`));
241
+ try {
242
+ // Focus entities
243
+ for (const entity of entities) {
244
+ execSync(`gut focus ${entity}`, { cwd: process.cwd(), stdio: 'pipe' });
245
+ }
246
+ // Create worktree
247
+ const wtOutput = execSync(`gut worktree create ${branchSlug} ${installFlag}`.trim(), { cwd: process.cwd(), encoding: 'utf8', stdio: 'pipe' });
248
+ // Extract worktree path from output (gut prints "Path: /tmp/gut-worktrees/...")
249
+ const pathMatch = wtOutput.match(/Path:\s+(.+)/);
250
+ if (!pathMatch) {
251
+ this.error('Failed to parse worktree path from gut output. Run gut worktree create manually.', { exit: 1 });
252
+ }
253
+ flags.cwd = pathMatch[1].trim();
254
+ this.log(colors.info(`Worktree created at: ${flags.cwd}`));
255
+ // Copy PRD and reference files to worktree (they may not exist in the worktree branch)
256
+ const { copyFileSync, mkdirSync } = await import('node:fs');
257
+ const { dirname, join } = await import('node:path');
258
+ const filesToCopy = [inputPath, ...(flags.reference || [])];
259
+ for (const file of filesToCopy) {
260
+ const destPath = join(flags.cwd, file);
261
+ try {
262
+ mkdirSync(dirname(destPath), { recursive: true });
263
+ copyFileSync(file, destPath);
264
+ }
265
+ catch {
266
+ // File may already exist in worktree — ignore
267
+ }
268
+ }
269
+ }
270
+ catch (error) {
271
+ const message = error instanceof Error ? error.message : String(error);
272
+ this.error(`Worktree creation failed: ${message}`, { exit: 1 });
273
+ }
274
+ }
275
+ // Three-layer config merge: CLI flags → .bmad-workflow.yaml → hardcoded defaults
276
+ const assetResolver = new AssetResolver({
277
+ devAgent: flags['dev-agent'],
278
+ smAgent: flags['sm-agent'],
279
+ });
280
+ const configDefaults = assetResolver.getExecutionDefaults();
281
+ const cliFlags = {
282
+ epicInterval: flags['epic-interval'],
283
+ maxRetries: flags['max-retries'],
284
+ model: flags.model,
285
+ parallel: flags.parallel,
286
+ pipeline: flags.pipeline,
287
+ prdInterval: flags['prd-interval'],
288
+ provider: flags.provider,
289
+ qa: flags.qa,
290
+ retryBackoffMs: flags['retry-backoff'],
291
+ storyInterval: flags['story-interval'],
292
+ timeout: flags.timeout,
293
+ };
294
+ const merged = mergeExecutionConfig(cliFlags, configDefaults);
215
295
  // Validate provider
216
- if (!isProviderSupported(flags.provider)) {
217
- this.error(`Unsupported provider: ${flags.provider}. Use 'claude', 'gemini', or 'opencode'.`, { exit: 1 });
296
+ if (!isProviderSupported(merged.provider)) {
297
+ this.error(`Unsupported provider: ${merged.provider}. Use 'claude', 'gemini', or 'opencode'.`, { exit: 1 });
218
298
  }
219
299
  // Initialize services with parallel concurrency and provider
220
- await this.initializeServices(flags.parallel, flags.provider, flags.verbose);
300
+ await this.initializeServices(merged.parallel, merged.provider, flags.verbose);
221
301
  // Register signal handlers
222
302
  this.registerSignalHandlers();
223
303
  // Initialize session scaffolder and create session structure
@@ -255,29 +335,29 @@ export default class Workflow extends Command {
255
335
  low: Severity.LOW,
256
336
  medium: Severity.MEDIUM,
257
337
  };
258
- // Build workflow configuration
338
+ // Build workflow configuration using merged execution defaults
259
339
  const config = {
260
340
  autoFix: flags['auto-fix'],
261
341
  cwd: flags.cwd,
262
342
  devAgent: flags['dev-agent'],
263
343
  dryRun: flags['dry-run'],
264
- epicInterval: flags['epic-interval'],
344
+ epicInterval: merged.epicInterval,
265
345
  input: args.input,
266
- maxRetries: flags['max-retries'],
346
+ maxRetries: merged.maxRetries,
267
347
  mcp: flags.mcp || undefined,
268
348
  mcpPhases: flags['mcp-phases'] ? flags['mcp-phases'].split(',').map((s) => s.trim()) : undefined,
269
349
  mcpPreset: flags['mcp-preset'],
270
- model: flags.model,
271
- parallel: flags.parallel,
272
- pipeline: flags.pipeline,
273
- prdInterval: flags['prd-interval'],
350
+ model: merged.model,
351
+ parallel: merged.parallel,
352
+ pipeline: merged.pipeline,
353
+ prdInterval: merged.prdInterval,
274
354
  prefix: flags.prefix,
275
- provider: flags.provider,
276
- qa: flags.qa,
355
+ provider: merged.provider,
356
+ qa: merged.qa,
277
357
  qaPrompt: flags['qa-prompt'],
278
358
  qaRetries: flags['qa-retries'],
279
359
  references: flags.reference || [],
280
- retryBackoffMs: flags['retry-backoff'],
360
+ retryBackoffMs: merged.retryBackoffMs,
281
361
  review: flags.review,
282
362
  reviewBlockOn: flags['review-block-on'] ? severityMap[flags['review-block-on']] : undefined,
283
363
  reviewMaxFix: flags['review-max-fix'],
@@ -287,8 +367,8 @@ export default class Workflow extends Command {
287
367
  smAgent: flags['sm-agent'],
288
368
  skipEpics: flags['skip-epics'],
289
369
  skipStories: flags['skip-stories'],
290
- storyInterval: flags['story-interval'],
291
- timeout: flags.timeout,
370
+ storyInterval: merged.storyInterval,
371
+ timeout: merged.timeout,
292
372
  verbose: flags.verbose,
293
373
  };
294
374
  // Validate --mcp-phases when --mcp is enabled
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Zod schema for .bmad-workflow.yaml configuration file.
3
+ *
4
+ * Defines validation, defaults, and type inference for the CLI config.
5
+ */
6
+ import { z } from 'zod';
7
+ /**
8
+ * Built-in agent names grouped by role
9
+ */
10
+ export declare const BUILT_IN_AGENTS: {
11
+ readonly dev: readonly ["pirlo", "barry"];
12
+ readonly qa: readonly ["quinn"];
13
+ readonly sm: readonly ["lena", "bob"];
14
+ };
15
+ /**
16
+ * All valid built-in agent names (flat list)
17
+ */
18
+ export declare const ALL_BUILT_IN_AGENTS: readonly ["pirlo", "barry", "lena", "bob", "quinn"];
19
+ /**
20
+ * Agent descriptions for interactive menus
21
+ */
22
+ export declare const AGENT_DESCRIPTIONS: Record<string, string>;
23
+ /**
24
+ * Zod schema for .bmad-workflow.yaml
25
+ */
26
+ export declare const BmadConfigSchema: z.ZodObject<{
27
+ bmad_path: z.ZodOptional<z.ZodString>;
28
+ communication_language: z.ZodDefault<z.ZodString>;
29
+ dev_agent: z.ZodDefault<z.ZodString>;
30
+ document_output_language: z.ZodDefault<z.ZodString>;
31
+ epic_location: z.ZodDefault<z.ZodString>;
32
+ model: z.ZodOptional<z.ZodString>;
33
+ output_folder: z.ZodDefault<z.ZodString>;
34
+ parallel: z.ZodDefault<z.ZodNumber>;
35
+ provider: z.ZodDefault<z.ZodEnum<{
36
+ claude: "claude";
37
+ gemini: "gemini";
38
+ opencode: "opencode";
39
+ }>>;
40
+ qa_agent: z.ZodDefault<z.ZodString>;
41
+ qa_enabled: z.ZodDefault<z.ZodBoolean>;
42
+ qa_location: z.ZodDefault<z.ZodString>;
43
+ sm_agent: z.ZodDefault<z.ZodString>;
44
+ story_location: z.ZodDefault<z.ZodString>;
45
+ timeout: z.ZodDefault<z.ZodString>;
46
+ user_name: z.ZodDefault<z.ZodString>;
47
+ }, z.core.$strip>;
48
+ /**
49
+ * Inferred TypeScript type from the schema
50
+ */
51
+ export type BmadConfig = z.infer<typeof BmadConfigSchema>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Zod schema for .bmad-workflow.yaml configuration file.
3
+ *
4
+ * Defines validation, defaults, and type inference for the CLI config.
5
+ */
6
+ import { z } from 'zod';
7
+ /**
8
+ * Built-in agent names grouped by role
9
+ */
10
+ export const BUILT_IN_AGENTS = {
11
+ dev: ['pirlo', 'barry'],
12
+ qa: ['quinn'],
13
+ sm: ['lena', 'bob'],
14
+ };
15
+ /**
16
+ * All valid built-in agent names (flat list)
17
+ */
18
+ export const ALL_BUILT_IN_AGENTS = [
19
+ ...BUILT_IN_AGENTS.dev,
20
+ ...BUILT_IN_AGENTS.sm,
21
+ ...BUILT_IN_AGENTS.qa,
22
+ ];
23
+ /**
24
+ * Agent descriptions for interactive menus
25
+ */
26
+ export const AGENT_DESCRIPTIONS = {
27
+ barry: 'Quick Flow Solo Dev — lean, fast, minimal ceremony',
28
+ bob: 'Classic SM — standard agile stories',
29
+ lena: 'Pirlo-Aware SM — deploy subtasks, real-infra ACs',
30
+ pirlo: 'Full-Stack Orchestrator — backend, deploy, E2E',
31
+ quinn: 'QA Engineer — rapid test coverage',
32
+ };
33
+ /**
34
+ * Zod schema for .bmad-workflow.yaml
35
+ */
36
+ export const BmadConfigSchema = z.object({
37
+ bmad_path: z.string().optional(),
38
+ communication_language: z.string().default('English'),
39
+ dev_agent: z.string().default('pirlo'),
40
+ document_output_language: z.string().default('English'),
41
+ epic_location: z.string().default('docs/epics'),
42
+ model: z.string().optional(),
43
+ output_folder: z.string().default('./docs'),
44
+ parallel: z.number().default(3),
45
+ provider: z.enum(['claude', 'gemini', 'opencode']).default('claude'),
46
+ qa_agent: z.string().default('quinn'),
47
+ qa_enabled: z.boolean().default(true),
48
+ qa_location: z.string().default('docs/qa/stories'),
49
+ sm_agent: z.string().default('lena'),
50
+ story_location: z.string().default('docs/stories'),
51
+ timeout: z.string().default('45m'),
52
+ user_name: z.string().default(''),
53
+ });
@@ -161,12 +161,17 @@ export class GeminiAgentRunner {
161
161
  let stderrData = '';
162
162
  try {
163
163
  // Use exec instead of spawn for better shell compatibility
164
- const { stderr, stdout } = await execAsync(command, {
164
+ const execOptions = {
165
165
  env: process.env,
166
166
  maxBuffer: 10 * 1024 * 1024, // 10MB buffer
167
167
  shell: process.env.SHELL || '/bin/bash',
168
168
  timeout,
169
- });
169
+ };
170
+ if (options.cwd) {
171
+ execOptions.cwd = options.cwd;
172
+ this.logger.info({ cwd: options.cwd }, 'Setting working directory for Gemini agent');
173
+ }
174
+ const { stderr, stdout } = await execAsync(command, execOptions);
170
175
  stdoutData = stdout;
171
176
  stderrData = stderr;
172
177
  const duration = Date.now() - startTime;
@@ -167,12 +167,17 @@ export class OpenCodeAgentRunner {
167
167
  tempFile,
168
168
  }, 'Executing command with temp file');
169
169
  // Use exec for better shell compatibility
170
- const { stderr, stdout } = await execAsync(command, {
170
+ const execOptions = {
171
171
  env: process.env,
172
172
  maxBuffer: 10 * 1024 * 1024, // 10MB buffer
173
173
  shell: process.env.SHELL || '/bin/bash',
174
174
  timeout,
175
- });
175
+ };
176
+ if (options.cwd) {
177
+ execOptions.cwd = options.cwd;
178
+ this.logger.info({ cwd: options.cwd }, 'Setting working directory for OpenCode agent');
179
+ }
180
+ const { stderr, stdout } = await execAsync(command, execOptions);
176
181
  stdoutData = stdout;
177
182
  stderrData = stderr;
178
183
  const duration = Date.now() - startTime;
@@ -0,0 +1,117 @@
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
+ /**
13
+ * Resolved asset with its source
14
+ */
15
+ export interface ResolvedAsset {
16
+ path: string;
17
+ source: 'bundled' | 'config' | 'flag';
18
+ }
19
+ /**
20
+ * CLI flags for agent overrides
21
+ */
22
+ export interface AssetResolverFlags {
23
+ devAgent?: string;
24
+ qaAgent?: string;
25
+ smAgent?: string;
26
+ }
27
+ /**
28
+ * Execution defaults parsed from .bmad-workflow.yaml
29
+ */
30
+ export interface ConfigExecutionDefaults {
31
+ model?: string;
32
+ parallel?: number;
33
+ pipeline?: boolean;
34
+ provider?: 'claude' | 'gemini' | 'opencode';
35
+ qa_enabled?: boolean;
36
+ timeout?: number;
37
+ }
38
+ /**
39
+ * AssetResolver resolves asset paths using a three-level resolution chain.
40
+ */
41
+ export declare class AssetResolver {
42
+ private readonly bundledPath;
43
+ private readonly config;
44
+ private readonly flags;
45
+ /**
46
+ * Create a new AssetResolver
47
+ *
48
+ * @param flags - CLI flag overrides for agent selection
49
+ */
50
+ constructor(flags?: Partial<AssetResolverFlags>);
51
+ /**
52
+ * Resolve an agent file path.
53
+ *
54
+ * Resolution order: CLI flag → config file → bundled default
55
+ *
56
+ * @param role - Agent role (dev, sm, qa)
57
+ * @param flagValue - Optional CLI flag override
58
+ * @returns Resolved asset with path and source
59
+ */
60
+ resolveAgent(role: 'dev' | 'qa' | 'sm', flagValue?: string): ResolvedAsset;
61
+ /**
62
+ * Resolve a template file path.
63
+ *
64
+ * Checks bmad_path config directory first, falls back to bundled.
65
+ *
66
+ * @param name - Template filename (e.g., 'epic-tmpl.yaml')
67
+ * @returns Resolved asset with path and source
68
+ */
69
+ resolveTemplate(name: string): ResolvedAsset;
70
+ /**
71
+ * Resolve the config file path.
72
+ *
73
+ * Returns the absolute path to the bundled default-config.yaml.
74
+ * Callers (PathResolver) decide whether to prefer local .bmad-core/ over this.
75
+ *
76
+ * @returns Absolute path to bundled config file
77
+ */
78
+ resolveConfig(): string;
79
+ /**
80
+ * Extract execution defaults from the loaded config file.
81
+ *
82
+ * Parses and validates: parallel, timeout, provider, model, qa_enabled, pipeline.
83
+ * Invalid values are silently ignored (field omitted from result).
84
+ *
85
+ * @returns Parsed execution defaults, or empty object if no config file
86
+ */
87
+ getExecutionDefaults(): ConfigExecutionDefaults;
88
+ /**
89
+ * Compute the bundled assets root path.
90
+ *
91
+ * From compiled dist/services/file-system/asset-resolver.js,
92
+ * the assets directory is three levels up: ../../.. → <pkg>/assets/
93
+ *
94
+ * @returns Absolute path to bundled assets directory
95
+ */
96
+ private computeBundledPath;
97
+ /**
98
+ * Get CLI flag value for a given role.
99
+ */
100
+ private getFlagForRole;
101
+ /**
102
+ * Load .bmad-workflow.yaml from project root (if exists).
103
+ *
104
+ * @returns Parsed config or null if file absent
105
+ */
106
+ private loadConfigFile;
107
+ /**
108
+ * Resolve an agent value — could be a built-in name or a file path.
109
+ *
110
+ * Built-in names (no path separators, no .md extension) map to bundled agents.
111
+ * File paths are resolved relative to project root.
112
+ *
113
+ * @param value - Agent name or file path
114
+ * @returns Absolute path to agent file
115
+ */
116
+ private resolveAgentValue;
117
+ }