@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.
- package/assets/agents/dev-barry.md +69 -0
- package/assets/agents/dev.md +323 -0
- package/assets/agents/qa.md +92 -0
- package/assets/agents/sm-bob.md +65 -0
- package/assets/agents/sm.md +296 -0
- package/assets/config/default-config.yaml +6 -0
- package/assets/templates/epic-tmpl.yaml +277 -0
- package/assets/templates/prd-tmpl.yaml +261 -0
- package/assets/templates/qa-gate-tmpl.yaml +103 -0
- package/assets/templates/story-tmpl.yaml +138 -0
- package/dist/commands/eject.d.ts +76 -0
- package/dist/commands/eject.js +232 -0
- package/dist/commands/init.d.ts +47 -0
- package/dist/commands/init.js +265 -0
- package/dist/commands/stories/develop.js +1 -0
- package/dist/commands/stories/qa.d.ts +1 -0
- package/dist/commands/stories/qa.js +7 -0
- package/dist/commands/workflow.d.ts +6 -3
- package/dist/commands/workflow.js +106 -26
- package/dist/models/bmad-config-schema.d.ts +51 -0
- package/dist/models/bmad-config-schema.js +53 -0
- package/dist/services/agents/gemini-agent-runner.js +7 -2
- package/dist/services/agents/opencode-agent-runner.js +7 -2
- package/dist/services/file-system/asset-resolver.d.ts +117 -0
- package/dist/services/file-system/asset-resolver.js +234 -0
- package/dist/services/file-system/file-manager.d.ts +13 -0
- package/dist/services/file-system/file-manager.js +32 -0
- package/dist/services/file-system/path-resolver.d.ts +22 -1
- package/dist/services/file-system/path-resolver.js +36 -9
- package/dist/services/orchestration/dependency-graph-executor.js +1 -0
- package/dist/services/orchestration/workflow-orchestrator.d.ts +11 -0
- package/dist/services/orchestration/workflow-orchestrator.js +79 -10
- package/dist/utils/config-merge.d.ts +60 -0
- package/dist/utils/config-merge.js +52 -0
- package/package.json +4 -2
|
@@ -0,0 +1,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>;
|