@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.
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,232 @@
1
+ /**
2
+ * Eject Command
3
+ *
4
+ * Copies bundled assets (agents, templates, config) into a local project directory
5
+ * for full customization. After eject, the CLI resolves assets from the local path
6
+ * instead of bundled defaults.
7
+ *
8
+ * @example
9
+ * ```bash
10
+ * bmad-workflow eject
11
+ * bmad-workflow eject --target ./my-assets
12
+ * bmad-workflow eject --agents-only
13
+ * bmad-workflow eject --templates-only
14
+ * bmad-workflow eject --force
15
+ * ```
16
+ */
17
+ import { Command, Flags } from '@oclif/core';
18
+ import fs from 'fs-extra';
19
+ import { dump as yamlDump, load as yamlLoad } from 'js-yaml';
20
+ import { dirname, join, relative, resolve } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { FileManager } from '../services/file-system/file-manager.js';
23
+ import { ValidationError } from '../utils/errors.js';
24
+ import { createLogger } from '../utils/logger.js';
25
+ /**
26
+ * Resolve the bundled assets directory path.
27
+ *
28
+ * From dist/commands/eject.js → ../../assets
29
+ */
30
+ export function getAssetsDir() {
31
+ const currentDir = dirname(fileURLToPath(import.meta.url));
32
+ return resolve(currentDir, '..', '..', 'assets');
33
+ }
34
+ /**
35
+ * Determine which subdirectories to eject based on flags
36
+ */
37
+ export function getSubdirs(agentsOnly, templatesOnly) {
38
+ if (agentsOnly) {
39
+ return [{ name: 'agents' }];
40
+ }
41
+ if (templatesOnly) {
42
+ return [{ name: 'templates' }];
43
+ }
44
+ // Default: all three
45
+ return [
46
+ { name: 'agents' },
47
+ { name: 'templates' },
48
+ { name: 'config' },
49
+ ];
50
+ }
51
+ /**
52
+ * Copy assets from source to target with skip/force logic
53
+ */
54
+ export async function copyAssets(assetsDir, targetPath, subdirs, force, fileManager) {
55
+ const summary = {
56
+ copied: new Map(),
57
+ skipped: [],
58
+ targetPath,
59
+ };
60
+ for (const subdir of subdirs) {
61
+ const sourceDir = join(assetsDir, subdir.name);
62
+ if (!fs.existsSync(sourceDir)) {
63
+ continue;
64
+ }
65
+ const files = fs.readdirSync(sourceDir).filter((f) => !f.startsWith('.'));
66
+ let copiedCount = 0;
67
+ for (const file of files) {
68
+ const sourcePath = join(sourceDir, file);
69
+ // Special case: config/default-config.yaml → core-config.yaml at target root
70
+ let destPath;
71
+ if (subdir.name === 'config' && file === 'default-config.yaml') {
72
+ destPath = join(targetPath, 'core-config.yaml');
73
+ }
74
+ else {
75
+ const targetSubdir = subdir.targetName || subdir.name;
76
+ destPath = join(targetPath, targetSubdir, file);
77
+ }
78
+ // Check if target exists
79
+ const exists = await fileManager.fileExists(destPath);
80
+ if (exists && !force) {
81
+ const relativePath = relative(process.cwd(), destPath);
82
+ summary.skipped.push(relativePath);
83
+ continue;
84
+ }
85
+ // Copy file
86
+ await fileManager.copyFile(sourcePath, destPath);
87
+ copiedCount++;
88
+ }
89
+ const label = subdir.targetName || subdir.name;
90
+ summary.copied.set(label, (summary.copied.get(label) || 0) + copiedCount);
91
+ }
92
+ return summary;
93
+ }
94
+ /**
95
+ * Create or update .bmad-workflow.yaml with bmad_path
96
+ */
97
+ export function updateConfig(targetPath, projectRoot) {
98
+ const root = projectRoot || process.cwd();
99
+ const configPath = join(root, '.bmad-workflow.yaml');
100
+ const bmadPath = `./${relative(root, targetPath)}`;
101
+ let config = {};
102
+ if (fs.existsSync(configPath)) {
103
+ try {
104
+ const content = fs.readFileSync(configPath, 'utf8');
105
+ const parsed = yamlLoad(content);
106
+ if (parsed && typeof parsed === 'object') {
107
+ config = parsed;
108
+ }
109
+ }
110
+ catch {
111
+ // If existing file is invalid YAML, start fresh
112
+ }
113
+ }
114
+ // Set only bmad_path — preserve all other fields
115
+ config.bmad_path = bmadPath;
116
+ const header = '# bmad-workflow configuration\n# Generated by bmad-workflow eject\n\n';
117
+ const yamlContent = header + yamlDump(config, { lineWidth: -1 });
118
+ fs.writeFileSync(configPath, yamlContent, 'utf8');
119
+ }
120
+ /**
121
+ * Format summary output lines
122
+ */
123
+ export function formatSummary(summary) {
124
+ const lines = [];
125
+ const totalCopied = [...summary.copied.values()].reduce((a, b) => a + b, 0);
126
+ if (totalCopied === 0 && summary.skipped.length > 0) {
127
+ lines.push('');
128
+ lines.push('⚠️ All files already exist. Use --force to overwrite.');
129
+ lines.push('');
130
+ lines.push('Skipped files:');
131
+ for (const file of summary.skipped) {
132
+ lines.push(` - ${file}`);
133
+ }
134
+ return lines;
135
+ }
136
+ lines.push('');
137
+ lines.push(`✅ Ejected assets to ${summary.targetPath}/`);
138
+ lines.push('');
139
+ for (const [dir, count] of summary.copied.entries()) {
140
+ if (count > 0) {
141
+ if (dir === 'config') {
142
+ lines.push(` core-config.yaml (${count} file)`);
143
+ }
144
+ else {
145
+ lines.push(` ${dir}/ (${count} files)`);
146
+ }
147
+ }
148
+ }
149
+ if (summary.skipped.length > 0) {
150
+ lines.push('');
151
+ lines.push('Skipped (already exist):');
152
+ for (const file of summary.skipped) {
153
+ lines.push(` - ${file}`);
154
+ }
155
+ }
156
+ lines.push('');
157
+ return lines;
158
+ }
159
+ /**
160
+ * Eject Command
161
+ *
162
+ * Copies bundled assets into a local project directory for customization.
163
+ */
164
+ export default class EjectCommand extends Command {
165
+ static description = 'Eject bundled assets into your project for customization';
166
+ static examples = [
167
+ {
168
+ command: '<%= config.bin %> <%= command.id %>',
169
+ description: 'Eject all assets to .bmad-core/',
170
+ },
171
+ {
172
+ command: '<%= config.bin %> <%= command.id %> --target ./my-assets',
173
+ description: 'Eject to custom directory',
174
+ },
175
+ {
176
+ command: '<%= config.bin %> <%= command.id %> --agents-only',
177
+ description: 'Eject only agent files',
178
+ },
179
+ {
180
+ command: '<%= config.bin %> <%= command.id %> --force',
181
+ description: 'Overwrite existing files',
182
+ },
183
+ ];
184
+ static flags = {
185
+ 'agents-only': Flags.boolean({
186
+ default: false,
187
+ description: 'Eject only agent files (no templates or config)',
188
+ exclusive: ['templates-only'],
189
+ }),
190
+ force: Flags.boolean({
191
+ default: false,
192
+ description: 'Overwrite existing files without prompting',
193
+ }),
194
+ target: Flags.string({
195
+ default: '.bmad-core',
196
+ description: 'Target directory for ejected assets',
197
+ }),
198
+ 'templates-only': Flags.boolean({
199
+ default: false,
200
+ description: 'Eject only template files (no agents or config)',
201
+ exclusive: ['agents-only'],
202
+ }),
203
+ };
204
+ async run() {
205
+ const { flags } = await this.parse(EjectCommand);
206
+ const logger = createLogger({ namespace: 'commands:eject' });
207
+ const fileManager = new FileManager(logger);
208
+ // Validate mutual exclusivity (oclif handles this via exclusive, but belt-and-suspenders)
209
+ if (flags['agents-only'] && flags['templates-only']) {
210
+ throw new ValidationError('Cannot use --agents-only and --templates-only together', { flags: ['agents-only', 'templates-only'] }, 'Use --agents-only OR --templates-only, not both');
211
+ }
212
+ // Resolve paths
213
+ const assetsDir = getAssetsDir();
214
+ const targetPath = resolve(process.cwd(), flags.target);
215
+ logger.info({ assetsDir, force: flags.force, targetPath }, 'Starting eject');
216
+ // Verify bundled assets exist
217
+ if (!fs.existsSync(assetsDir)) {
218
+ this.error('Bundled assets directory not found. Your CLI installation may be corrupted.', { exit: 1 });
219
+ }
220
+ // Determine which subdirectories to copy
221
+ const subdirs = getSubdirs(flags['agents-only'], flags['templates-only']);
222
+ // Copy files
223
+ const summary = await copyAssets(assetsDir, targetPath, subdirs, flags.force, fileManager);
224
+ // Update .bmad-workflow.yaml
225
+ updateConfig(targetPath);
226
+ // Print summary
227
+ const lines = formatSummary(summary);
228
+ for (const line of lines) {
229
+ this.log(line);
230
+ }
231
+ }
232
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Init Command
3
+ *
4
+ * Generates .bmad-workflow.yaml config file with agent preferences,
5
+ * output paths, and personalization settings. Supports both interactive
6
+ * and non-interactive modes.
7
+ */
8
+ import { Command } from '@oclif/core';
9
+ export default class Init extends Command {
10
+ static description: string;
11
+ static examples: string[];
12
+ static flags: {
13
+ dev: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ language: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
16
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ 'no-interactive': import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
19
+ qa: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
+ sm: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ };
22
+ run(): Promise<void>;
23
+ /**
24
+ * Build config object from CLI flags (non-interactive mode).
25
+ */
26
+ private buildConfigFromFlags;
27
+ /**
28
+ * Generate YAML string with comment header.
29
+ */
30
+ private generateYaml;
31
+ /**
32
+ * Run interactive prompts and collect config values.
33
+ */
34
+ private runInteractivePrompts;
35
+ /**
36
+ * Prompt user to select an agent for a role.
37
+ */
38
+ private promptAgentSelection;
39
+ /**
40
+ * Prompt for a custom agent file path and validate it exists.
41
+ */
42
+ private promptCustomAgentPath;
43
+ /**
44
+ * Validate an agent value — must be a built-in name or an existing file path.
45
+ */
46
+ private validateAgentValue;
47
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Init Command
3
+ *
4
+ * Generates .bmad-workflow.yaml config file with agent preferences,
5
+ * output paths, and personalization settings. Supports both interactive
6
+ * and non-interactive modes.
7
+ */
8
+ import { Command, Flags } from '@oclif/core';
9
+ import { confirm, input, select } from '@inquirer/prompts';
10
+ import { dump as yamlDump } from 'js-yaml';
11
+ import fs from 'fs-extra';
12
+ import { join, resolve } from 'node:path';
13
+ import { AGENT_DESCRIPTIONS, BmadConfigSchema, BUILT_IN_AGENTS, } from '../models/bmad-config-schema.js';
14
+ import { createLogger } from '../utils/logger.js';
15
+ import * as colors from '../utils/colors.js';
16
+ import { ValidationError } from '../utils/errors.js';
17
+ const CONFIG_FILENAME = '.bmad-workflow.yaml';
18
+ const YAML_HEADER = `# bmad-workflow configuration
19
+ # Generated by: bmad-workflow init
20
+ # Docs: https://github.com/AiREPO/bmad-workflow
21
+ #
22
+ # Agent selection — built-in names: pirlo, barry, lena, bob, quinn
23
+ # Or: path to a custom agent .md file (relative to project root)
24
+ `;
25
+ /**
26
+ * All valid built-in agent names for validation
27
+ */
28
+ const ALL_VALID_NAMES = new Set([
29
+ ...BUILT_IN_AGENTS.dev,
30
+ ...BUILT_IN_AGENTS.sm,
31
+ ...BUILT_IN_AGENTS.qa,
32
+ ]);
33
+ export default class Init extends Command {
34
+ static description = 'Initialize bmad-workflow configuration';
35
+ static examples = [
36
+ '<%= config.bin %> init',
37
+ '<%= config.bin %> init --dev pirlo --sm lena --qa quinn --no-interactive',
38
+ '<%= config.bin %> init --dev ./my-agent.md --no-interactive',
39
+ '<%= config.bin %> init --force',
40
+ ];
41
+ static flags = {
42
+ dev: Flags.string({ description: 'Dev agent (pirlo, barry, or path)', helpGroup: 'Agent Selection' }),
43
+ force: Flags.boolean({ default: false, description: 'Overwrite existing config' }),
44
+ language: Flags.string({ default: 'English', description: 'Communication language' }),
45
+ name: Flags.string({ description: 'Your name' }),
46
+ 'no-interactive': Flags.boolean({ description: 'Skip interactive prompts' }),
47
+ output: Flags.string({ default: './docs', description: 'Output folder for docs' }),
48
+ qa: Flags.string({ description: 'QA agent (quinn, none, or path)', helpGroup: 'Agent Selection' }),
49
+ sm: Flags.string({ description: 'SM agent (lena, bob, or path)', helpGroup: 'Agent Selection' }),
50
+ };
51
+ async run() {
52
+ const { flags } = await this.parse(Init);
53
+ const logger = createLogger({ namespace: 'commands:init' });
54
+ const configPath = join(process.cwd(), CONFIG_FILENAME);
55
+ const isNonInteractive = flags['no-interactive'] === true;
56
+ logger.info('Starting init command');
57
+ // --- Overwrite protection (Task 6) ---
58
+ if (fs.existsSync(configPath)) {
59
+ if (flags.force) {
60
+ this.log(colors.warning(`Overwriting existing ${CONFIG_FILENAME}`));
61
+ }
62
+ else if (isNonInteractive) {
63
+ this.error(`${CONFIG_FILENAME} already exists. Use --force to overwrite.`, { exit: 1 });
64
+ }
65
+ else {
66
+ const overwrite = await confirm({
67
+ default: false,
68
+ message: `${CONFIG_FILENAME} already exists. Overwrite?`,
69
+ });
70
+ if (!overwrite) {
71
+ this.log('Aborted.');
72
+ return;
73
+ }
74
+ }
75
+ }
76
+ let config;
77
+ if (isNonInteractive) {
78
+ // --- Non-interactive mode (Task 4) ---
79
+ config = this.buildConfigFromFlags(flags);
80
+ }
81
+ else {
82
+ // --- Interactive mode (Task 3) ---
83
+ config = await this.runInteractivePrompts(flags);
84
+ }
85
+ // --- Validate and write (Task 5) ---
86
+ const parsed = BmadConfigSchema.parse(config);
87
+ const yamlContent = this.generateYaml(parsed);
88
+ await fs.ensureDir(join(configPath, '..'));
89
+ await fs.writeFile(configPath, yamlContent, 'utf8');
90
+ this.log(colors.success(`Created ${CONFIG_FILENAME}`));
91
+ logger.info({ configPath }, 'Config file written');
92
+ }
93
+ /**
94
+ * Build config object from CLI flags (non-interactive mode).
95
+ */
96
+ buildConfigFromFlags(flags) {
97
+ const config = {};
98
+ if (flags.dev) {
99
+ config.dev_agent = this.validateAgentValue(flags.dev, 'dev');
100
+ }
101
+ if (flags.sm) {
102
+ config.sm_agent = this.validateAgentValue(flags.sm, 'sm');
103
+ }
104
+ if (flags.qa) {
105
+ const qaVal = flags.qa;
106
+ if (qaVal === 'none') {
107
+ config.qa_agent = 'quinn';
108
+ config.qa_enabled = false;
109
+ }
110
+ else {
111
+ config.qa_agent = this.validateAgentValue(qaVal, 'qa');
112
+ }
113
+ }
114
+ if (flags.output)
115
+ config.output_folder = flags.output;
116
+ if (flags.language)
117
+ config.communication_language = flags.language;
118
+ if (flags.name)
119
+ config.user_name = flags.name;
120
+ return config;
121
+ }
122
+ /**
123
+ * Generate YAML string with comment header.
124
+ */
125
+ generateYaml(config) {
126
+ // Build a clean object for YAML output, omitting optional undefined fields
127
+ const output = {
128
+ dev_agent: config.dev_agent,
129
+ sm_agent: config.sm_agent,
130
+ qa_agent: config.qa_agent,
131
+ qa_enabled: config.qa_enabled,
132
+ output_folder: config.output_folder,
133
+ story_location: config.story_location,
134
+ epic_location: config.epic_location,
135
+ qa_location: config.qa_location,
136
+ communication_language: config.communication_language,
137
+ document_output_language: config.document_output_language,
138
+ user_name: config.user_name,
139
+ parallel: config.parallel,
140
+ timeout: config.timeout,
141
+ provider: config.provider,
142
+ };
143
+ if (config.model)
144
+ output.model = config.model;
145
+ const yamlStr = yamlDump(output, { lineWidth: -1, quotingType: "'", forceQuotes: false });
146
+ // Add commented-out bmad_path if not set
147
+ let bmadPathLine = '';
148
+ if (!config.bmad_path) {
149
+ bmadPathLine = '\n# Optional: path to local BMAD assets (overrides bundled defaults)\n# bmad_path: ./_bmad-core\n';
150
+ }
151
+ return YAML_HEADER + '\n' + yamlStr + bmadPathLine;
152
+ }
153
+ /**
154
+ * Run interactive prompts and collect config values.
155
+ */
156
+ async runInteractivePrompts(flags) {
157
+ const config = {};
158
+ // Dev agent selection
159
+ config.dev_agent = flags.dev
160
+ ? this.validateAgentValue(flags.dev, 'dev')
161
+ : await this.promptAgentSelection('dev', BUILT_IN_AGENTS.dev, 'pirlo');
162
+ // SM agent selection
163
+ config.sm_agent = flags.sm
164
+ ? this.validateAgentValue(flags.sm, 'sm')
165
+ : await this.promptAgentSelection('sm', BUILT_IN_AGENTS.sm, 'lena');
166
+ // QA agent selection
167
+ if (flags.qa) {
168
+ const qaVal = flags.qa;
169
+ if (qaVal === 'none') {
170
+ config.qa_agent = 'quinn';
171
+ config.qa_enabled = false;
172
+ }
173
+ else {
174
+ config.qa_agent = this.validateAgentValue(qaVal, 'qa');
175
+ }
176
+ }
177
+ else {
178
+ const qaResult = await this.promptAgentSelection('qa', BUILT_IN_AGENTS.qa, 'quinn', true);
179
+ if (qaResult === '__none__') {
180
+ config.qa_enabled = false;
181
+ }
182
+ else {
183
+ config.qa_agent = qaResult;
184
+ }
185
+ }
186
+ // Output folder
187
+ config.output_folder = flags.output || await input({
188
+ default: './docs',
189
+ message: 'Output folder for docs:',
190
+ });
191
+ // BMAD assets path
192
+ const bmadPath = await input({
193
+ default: '',
194
+ message: 'Custom BMAD assets path (leave empty for none):',
195
+ });
196
+ if (bmadPath)
197
+ config.bmad_path = bmadPath;
198
+ // Communication language
199
+ config.communication_language = flags.language || await input({
200
+ default: 'English',
201
+ message: 'Communication language:',
202
+ });
203
+ // User name
204
+ config.user_name = flags.name ?? await input({
205
+ default: '',
206
+ message: 'Your name:',
207
+ });
208
+ return config;
209
+ }
210
+ /**
211
+ * Prompt user to select an agent for a role.
212
+ */
213
+ async promptAgentSelection(role, builtIns, defaultValue, includeNone = false) {
214
+ const choices = builtIns.map((name) => ({
215
+ description: AGENT_DESCRIPTIONS[name] || '',
216
+ name: `${name} — ${AGENT_DESCRIPTIONS[name] || ''}`,
217
+ value: name,
218
+ }));
219
+ if (includeNone) {
220
+ choices.push({ description: 'Skip this phase', name: 'none — Skip this phase', value: '__none__' });
221
+ }
222
+ choices.push({ description: 'Path to custom agent file', name: 'custom — Path to custom agent file', value: '__custom__' });
223
+ const selected = await select({
224
+ choices,
225
+ default: defaultValue,
226
+ message: `${role.toUpperCase()} agent:`,
227
+ });
228
+ if (selected === '__custom__') {
229
+ return this.promptCustomAgentPath();
230
+ }
231
+ return selected;
232
+ }
233
+ /**
234
+ * Prompt for a custom agent file path and validate it exists.
235
+ */
236
+ async promptCustomAgentPath() {
237
+ const customPath = await input({
238
+ message: 'Path to custom agent file (relative to project root):',
239
+ });
240
+ const resolved = resolve(customPath);
241
+ if (!fs.existsSync(resolved)) {
242
+ throw new ValidationError(`Agent file not found: ${customPath}`, { code: 'ERR_VALIDATION', path: customPath }, 'Provide a valid path to an existing .md file relative to the project root.');
243
+ }
244
+ return customPath;
245
+ }
246
+ /**
247
+ * Validate an agent value — must be a built-in name or an existing file path.
248
+ */
249
+ validateAgentValue(value, role) {
250
+ // Check built-in names
251
+ if (ALL_VALID_NAMES.has(value))
252
+ return value;
253
+ // Check file path
254
+ if (value.includes('/') || value.endsWith('.md')) {
255
+ const resolved = resolve(value);
256
+ if (fs.existsSync(resolved))
257
+ return value;
258
+ throw new ValidationError(`Agent file not found: ${value}`, { code: 'ERR_VALIDATION', path: value, role }, 'Provide a valid path to an existing .md file relative to the project root.');
259
+ }
260
+ // Invalid name
261
+ const roleAgents = BUILT_IN_AGENTS[role];
262
+ const validNames = roleAgents ? [...roleAgents] : [...ALL_VALID_NAMES];
263
+ throw new ValidationError(`Unknown agent "${value}". Valid names: ${validNames.join(', ')}`, { code: 'ERR_VALIDATION', role, value }, 'Use a built-in name or provide a path to a custom .md file.');
264
+ }
265
+ }
@@ -308,6 +308,7 @@ export default class StoriesDevelopCommand extends Command {
308
308
  this.logger.info({ storyNumber }, 'Running Claude dev agent');
309
309
  const result = await runAgentWithRetry(this.agentRunner, prompt, {
310
310
  agentType: 'dev',
311
+ cwd,
311
312
  timeout: timeout ?? 2_700_000,
312
313
  }, {
313
314
  backoffMs: retryBackoff,
@@ -29,6 +29,7 @@ export default class StoriesQaCommand extends Command {
29
29
  description: string;
30
30
  }[];
31
31
  static flags: {
32
+ cwd: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
32
33
  'dev-prompt': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
33
34
  interval: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
34
35
  'max-retries': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
@@ -57,6 +57,10 @@ export default class StoriesQaCommand extends Command {
57
57
  },
58
58
  ];
59
59
  static flags = {
60
+ cwd: Flags.string({
61
+ description: 'Working directory path to pass to AI agents. Agents will operate in this directory.',
62
+ helpGroup: 'Agent Customization',
63
+ }),
60
64
  'dev-prompt': Flags.string({
61
65
  description: 'Custom prompt/instructions for dev fix-forward phase',
62
66
  helpGroup: 'Prompt Customization',
@@ -422,6 +426,7 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
422
426
  const qaPrompt = this.buildQaPrompt(storyPath, flags['qa-prompt'], flags.reference);
423
427
  const qaResult = await runAgentWithRetry(this.agentRunner, qaPrompt, {
424
428
  agentType: 'tea',
429
+ cwd: flags.cwd,
425
430
  timeout: agentTimeout,
426
431
  }, {
427
432
  backoffMs: retryBackoff,
@@ -444,6 +449,7 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
444
449
  // eslint-disable-next-line no-await-in-loop
445
450
  const devResult = await runAgentWithRetry(this.agentRunner, devPrompt, {
446
451
  agentType: 'dev',
452
+ cwd: flags.cwd,
447
453
  timeout: agentTimeout,
448
454
  }, {
449
455
  backoffMs: retryBackoff,
@@ -459,6 +465,7 @@ ${result.finalGate === 'PASS' ? '3. [x] Final QA Validation - PASSED' : result.f
459
465
  // eslint-disable-next-line no-await-in-loop
460
466
  const reQaResult = await runAgentWithRetry(this.agentRunner, qaPrompt, {
461
467
  agentType: 'tea',
468
+ cwd: flags.cwd,
462
469
  timeout: agentTimeout,
463
470
  }, {
464
471
  backoffMs: retryBackoff,
@@ -26,17 +26,20 @@ export default class Workflow extends Command {
26
26
  static flags: {
27
27
  'auto-fix': import("@oclif/core/interfaces").BooleanFlag<boolean>;
28
28
  cwd: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
29
+ worktree: import("@oclif/core/interfaces").BooleanFlag<boolean>;
30
+ 'gut-entities': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
31
+ 'worktree-install': import("@oclif/core/interfaces").BooleanFlag<boolean>;
29
32
  'dev-agent': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
30
33
  'sm-agent': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
31
34
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
32
35
  'epic-interval': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
33
- parallel: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
36
+ parallel: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
34
37
  pipeline: import("@oclif/core/interfaces").BooleanFlag<boolean>;
35
38
  'prd-interval': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
36
39
  model: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
37
40
  prefix: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
38
41
  'session-prefix': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
39
- provider: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
42
+ provider: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
40
43
  mcp: import("@oclif/core/interfaces").BooleanFlag<boolean>;
41
44
  'mcp-phases': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
42
45
  'mcp-preset': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -52,7 +55,7 @@ export default class Workflow extends Command {
52
55
  'skip-epics': import("@oclif/core/interfaces").BooleanFlag<boolean>;
53
56
  'skip-stories': import("@oclif/core/interfaces").BooleanFlag<boolean>;
54
57
  'story-interval': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
55
- timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
58
+ timeout: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
56
59
  'review-timeout': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
57
60
  'max-retries': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
58
61
  'retry-backoff': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;