ma-agents 3.3.0 → 3.4.0

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 (80) hide show
  1. package/.opencode/skills/.ma-agents.json +99 -99
  2. package/.roo/skills/.ma-agents.json +99 -99
  3. package/README.md +19 -1
  4. package/bin/cli.js +55 -0
  5. package/lib/agents.js +23 -0
  6. package/lib/bmad-cache/cache-manifest.json +1 -1
  7. package/lib/bmad-customizations/bmm-demerzel.customize.yaml +36 -0
  8. package/lib/bmad-customizations/demerzel.md +32 -0
  9. package/lib/bmad-extension/module-help.csv +13 -0
  10. package/lib/bmad-extension/skills/bmad-ma-agent-ml/.gitkeep +0 -0
  11. package/lib/bmad-extension/skills/bmad-ma-agent-ml/SKILL.md +59 -0
  12. package/lib/bmad-extension/skills/bmad-ma-agent-ml/bmad-skill-manifest.yaml +11 -0
  13. package/lib/bmad-extension/skills/generate-backlog/.gitkeep +0 -0
  14. package/lib/bmad-extension/skills/ml-advise/.gitkeep +0 -0
  15. package/lib/bmad-extension/skills/ml-advise/SKILL.md +76 -0
  16. package/lib/bmad-extension/skills/ml-advise/bmad-skill-manifest.yaml +3 -0
  17. package/lib/bmad-extension/skills/ml-advise/skill.json +7 -0
  18. package/lib/bmad-extension/skills/ml-analysis/.gitkeep +0 -0
  19. package/lib/bmad-extension/skills/ml-analysis/SKILL.md +60 -0
  20. package/lib/bmad-extension/skills/ml-analysis/bmad-skill-manifest.yaml +3 -0
  21. package/lib/bmad-extension/skills/ml-analysis/skill.json +7 -0
  22. package/lib/bmad-extension/skills/ml-architecture/.gitkeep +0 -0
  23. package/lib/bmad-extension/skills/ml-architecture/SKILL.md +55 -0
  24. package/lib/bmad-extension/skills/ml-architecture/bmad-skill-manifest.yaml +3 -0
  25. package/lib/bmad-extension/skills/ml-architecture/skill.json +7 -0
  26. package/lib/bmad-extension/skills/ml-detailed-design/.gitkeep +0 -0
  27. package/lib/bmad-extension/skills/ml-detailed-design/SKILL.md +67 -0
  28. package/lib/bmad-extension/skills/ml-detailed-design/bmad-skill-manifest.yaml +3 -0
  29. package/lib/bmad-extension/skills/ml-detailed-design/skill.json +7 -0
  30. package/lib/bmad-extension/skills/ml-eda/.gitkeep +0 -0
  31. package/lib/bmad-extension/skills/ml-eda/SKILL.md +56 -0
  32. package/lib/bmad-extension/skills/ml-eda/bmad-skill-manifest.yaml +3 -0
  33. package/lib/bmad-extension/skills/ml-eda/scripts/baseline_classifier.py +522 -0
  34. package/lib/bmad-extension/skills/ml-eda/scripts/class_weights_calculator.py +295 -0
  35. package/lib/bmad-extension/skills/ml-eda/scripts/clustering_explorer.py +383 -0
  36. package/lib/bmad-extension/skills/ml-eda/scripts/eda_analyzer.py +654 -0
  37. package/lib/bmad-extension/skills/ml-eda/skill.json +7 -0
  38. package/lib/bmad-extension/skills/ml-experiment/.gitkeep +0 -0
  39. package/lib/bmad-extension/skills/ml-experiment/SKILL.md +74 -0
  40. package/lib/bmad-extension/skills/ml-experiment/assets/advanced_trainer_configs.py +430 -0
  41. package/lib/bmad-extension/skills/ml-experiment/assets/quick_trainer_setup.py +233 -0
  42. package/lib/bmad-extension/skills/ml-experiment/assets/template_datamodule.py +219 -0
  43. package/lib/bmad-extension/skills/ml-experiment/assets/template_gnn_module.py +341 -0
  44. package/lib/bmad-extension/skills/ml-experiment/assets/template_lightning_module.py +158 -0
  45. package/lib/bmad-extension/skills/ml-experiment/bmad-skill-manifest.yaml +3 -0
  46. package/lib/bmad-extension/skills/ml-experiment/skill.json +7 -0
  47. package/lib/bmad-extension/skills/ml-hparam/.gitkeep +0 -0
  48. package/lib/bmad-extension/skills/ml-hparam/SKILL.md +81 -0
  49. package/lib/bmad-extension/skills/ml-hparam/bmad-skill-manifest.yaml +3 -0
  50. package/lib/bmad-extension/skills/ml-hparam/skill.json +7 -0
  51. package/lib/bmad-extension/skills/ml-ideation/.gitkeep +0 -0
  52. package/lib/bmad-extension/skills/ml-ideation/SKILL.md +50 -0
  53. package/lib/bmad-extension/skills/ml-ideation/bmad-skill-manifest.yaml +3 -0
  54. package/lib/bmad-extension/skills/ml-ideation/scripts/validate_ml_prd.py +287 -0
  55. package/lib/bmad-extension/skills/ml-ideation/skill.json +7 -0
  56. package/lib/bmad-extension/skills/ml-infra/.gitkeep +0 -0
  57. package/lib/bmad-extension/skills/ml-infra/SKILL.md +58 -0
  58. package/lib/bmad-extension/skills/ml-infra/bmad-skill-manifest.yaml +3 -0
  59. package/lib/bmad-extension/skills/ml-infra/skill.json +7 -0
  60. package/lib/bmad-extension/skills/ml-retrospective/.gitkeep +0 -0
  61. package/lib/bmad-extension/skills/ml-retrospective/SKILL.md +63 -0
  62. package/lib/bmad-extension/skills/ml-retrospective/bmad-skill-manifest.yaml +3 -0
  63. package/lib/bmad-extension/skills/ml-retrospective/skill.json +7 -0
  64. package/lib/bmad-extension/skills/ml-revision/.gitkeep +0 -0
  65. package/lib/bmad-extension/skills/ml-revision/SKILL.md +82 -0
  66. package/lib/bmad-extension/skills/ml-revision/bmad-skill-manifest.yaml +3 -0
  67. package/lib/bmad-extension/skills/ml-revision/skill.json +7 -0
  68. package/lib/bmad-extension/skills/ml-techspec/.gitkeep +0 -0
  69. package/lib/bmad-extension/skills/ml-techspec/SKILL.md +80 -0
  70. package/lib/bmad-extension/skills/ml-techspec/bmad-skill-manifest.yaml +3 -0
  71. package/lib/bmad-extension/skills/ml-techspec/skill.json +7 -0
  72. package/lib/bmad.js +85 -8
  73. package/lib/skill-authoring.js +1 -1
  74. package/package.json +2 -2
  75. package/test/agent-injection-strategy.test.js +4 -4
  76. package/test/bmad-version-bump.test.js +34 -34
  77. package/test/build-bmad-args.test.js +13 -6
  78. package/test/convert-agents-to-skills.test.js +11 -1
  79. package/test/extension-module-restructure.test.js +31 -7
  80. package/test/migration-validation.test.js +14 -11
package/lib/bmad.js CHANGED
@@ -7,6 +7,23 @@ const chalk = require('chalk');
7
7
  const BMAD_DIR = '_bmad';
8
8
  const CONFIG_DIR = path.join(BMAD_DIR, '_config', 'agents');
9
9
 
10
+ /**
11
+ * Run a shell command, relaying output to stdout/stderr.
12
+ * When MA_AGENTS_LOG_ACTIVE is set, uses pipe mode so the
13
+ * tee hooks in cli.js capture subprocess output in the log file.
14
+ * Otherwise falls back to inherit for direct pass-through.
15
+ */
16
+ function runCommand(command, options = {}) {
17
+ if (process.env.MA_AGENTS_LOG_ACTIVE) {
18
+ const result = execSync(command, { ...options, stdio: 'pipe' });
19
+ if (result && result.length > 0) {
20
+ process.stdout.write(result);
21
+ }
22
+ } else {
23
+ execSync(command, { ...options, stdio: 'inherit' });
24
+ }
25
+ }
26
+
10
27
  function getBmadCommand(args) {
11
28
  try {
12
29
  const wrapperPath = require.resolve('bmad-method/tools/bmad-npx-wrapper.js');
@@ -63,9 +80,13 @@ function buildBmadArgs(ctx) {
63
80
  parts.push('--output-folder', `"${ctx.outputFolder}"`);
64
81
  }
65
82
 
66
- // Extension module — only if directory exists
83
+ // Extension module — only on fresh installs.
84
+ // During updates, the cached copy in _bmad/_config/custom/ is used by
85
+ // bmad-method automatically; passing --custom-content on update causes
86
+ // "Source for module 'custom' is not available" when the cached directory
87
+ // name doesn't match the module code.
67
88
  const extensionPath = path.join(__dirname, 'bmad-extension');
68
- if (fs.existsSync(extensionPath)) {
89
+ if (ctx.action !== 'update' && fs.existsSync(extensionPath)) {
69
90
  parts.push('--custom-content', `"${extensionPath}"`);
70
91
  }
71
92
 
@@ -101,7 +122,7 @@ async function installBmad(modules = ['bmm', 'bmb'], tools = [], projectRoot = p
101
122
 
102
123
  console.log(chalk.gray(` Running: ${command}`));
103
124
  try {
104
- execSync(command, { stdio: 'inherit', cwd: projectRoot, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
125
+ runCommand(command, { cwd: projectRoot, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
105
126
  await deployMethodology(projectRoot, force);
106
127
  return true;
107
128
  } catch (error) {
@@ -136,7 +157,7 @@ async function runMigration(modules, tools, projectRoot, force, { userName, comm
136
157
 
137
158
  console.log(chalk.gray(` Running: ${command}`));
138
159
  try {
139
- execSync(command, { stdio: 'inherit', cwd: projectRoot, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
160
+ runCommand(command, { cwd: projectRoot, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
140
161
  } catch (error) {
141
162
  // Rollback on failure
142
163
  console.error(chalk.red(` BMAD update failed: ${error.message}`));
@@ -183,7 +204,7 @@ async function updateBmad(modules = ['bmm', 'bmb'], tools = [], projectRoot = pr
183
204
 
184
205
  console.log(chalk.gray(` Running: ${command}`));
185
206
  try {
186
- execSync(command, { stdio: 'inherit', cwd: projectRoot, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
207
+ runCommand(command, { cwd: projectRoot, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
187
208
  await deployMethodology(projectRoot, force);
188
209
  return true;
189
210
  } catch (error) {
@@ -206,7 +227,8 @@ async function applyCustomizations(projectRoot = process.cwd(), modules = ['bmm'
206
227
  'bmm-sre': 'sre.md',
207
228
  'bmm-devops': 'devops.md',
208
229
  'bmm-cyber': 'cyber.md',
209
- 'bmm-mil498': 'mil498.md'
230
+ 'bmm-mil498': 'mil498.md',
231
+ 'bmm-demerzel': 'demerzel.md'
210
232
  };
211
233
 
212
234
  // 1. Apply YAML customizations (filtered by selected agents)
@@ -259,7 +281,7 @@ async function applyCustomizations(projectRoot = process.cwd(), modules = ['bmm'
259
281
 
260
282
  console.log(chalk.gray(` Running: ${command}`));
261
283
  try {
262
- execSync(command, { stdio: 'inherit', cwd: projectRoot, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
284
+ runCommand(command, { cwd: projectRoot, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
263
285
  } catch (error) {
264
286
  console.error(chalk.red(` BMAD recompile failed: ${error.message}`));
265
287
  }
@@ -330,6 +352,11 @@ async function applyCustomizations(projectRoot = process.cwd(), modules = ['bmm'
330
352
  if (selectedAgentIds.length === 0 || selectedAgentIds.includes('bmm-mil498')) {
331
353
  await registerMilWorkflows(projectRoot);
332
354
  }
355
+
356
+ // 8. Register ML workflows
357
+ if (selectedAgentIds.length === 0 || selectedAgentIds.includes('bmm-demerzel')) {
358
+ await registerMlWorkflows(projectRoot);
359
+ }
333
360
  }
334
361
 
335
362
  /**
@@ -411,6 +438,55 @@ async function registerMilWorkflows(projectRoot) {
411
438
  }
412
439
  }
413
440
 
441
+ /**
442
+ * Append ML lifecycle workflow entries to the BMAD CSV registries
443
+ * so they appear as available slash commands for Demerzel.
444
+ */
445
+ async function registerMlWorkflows(projectRoot) {
446
+ const mlEntries = [
447
+ { name: 'ML Ideation & PRD', code: 'MLI', command: 'ml-ideation', description: 'Frame ML research problem, define requirements, and produce Research Thesis and PRD' },
448
+ { name: 'ML EDA & Research', code: 'MLE', command: 'ml-eda', description: 'Perform exploratory data analysis, establish baselines, and produce EDA report' },
449
+ { name: 'ML Architecture Design', code: 'MLA', command: 'ml-architecture', description: 'Design model architecture, stack, and experiment tracking strategy' },
450
+ { name: 'ML Detailed Design', code: 'MLD', command: 'ml-detailed-design', description: 'Break down infrastructure and experiment tasks from architecture' },
451
+ { name: 'ML TechSpec (Contract)', code: 'MLS', command: 'ml-techspec', description: 'Lock experiment parameters and performance tiers before training' },
452
+ { name: 'ML Infra & Sync', code: 'MLNF', command: 'ml-infra', description: 'Build ML infrastructure, manage dependencies, and run smoke tests' },
453
+ { name: 'ML Experiment', code: 'MLX', command: 'ml-experiment', description: 'Execute training experiments against locked TechSpec and log metrics' },
454
+ { name: 'ML Results Analysis', code: 'MLAN', command: 'ml-analysis', description: 'Evaluate experiment outcomes against TechSpec success tiers' },
455
+ { name: 'ML HPO (Tuning)', code: 'MLH', command: 'ml-hparam', description: 'Perform automated hyperparameter optimization' },
456
+ { name: 'ML Iterative Revision', code: 'MLR', command: 'ml-revision', description: 'Amend hypothesis and requirements based on experiment findings' },
457
+ { name: 'ML Advise & Search', code: 'MLAD', command: 'ml-advise', description: 'Search past experiments and surface findings or failure warnings' },
458
+ { name: 'ML Retrospective', code: 'MLRT', command: 'ml-retrospective', description: 'Capture session learnings and update project context' }
459
+ ];
460
+
461
+ // Append or update entries in module-help.csv
462
+ const moduleHelpPath = path.join(projectRoot, BMAD_DIR, 'bmm', 'module-help.csv');
463
+ if (fs.existsSync(moduleHelpPath)) {
464
+ let content = await fs.readFile(moduleHelpPath, 'utf-8');
465
+ for (const entry of mlEntries) {
466
+ const row = `bmm,anytime,${entry.name},${entry.code},,skills/${entry.command}/SKILL.md,${entry.command},false,bmm-demerzel,Create Mode,${entry.description},planning_artifacts,document,`;
467
+ // Remove any existing line for this command, then append the current one
468
+ const lines = content.split('\n').filter(line => !line.includes(entry.command));
469
+ content = lines.join('\n').trimEnd() + '\n' + row + '\n';
470
+ }
471
+ await fs.writeFile(moduleHelpPath, content, 'utf-8');
472
+ console.log(chalk.cyan(` + Registered ML workflows in module-help.csv`));
473
+ }
474
+
475
+ // Append or update entries in bmad-help.csv
476
+ const bmadHelpPath = path.join(projectRoot, BMAD_DIR, '_config', 'bmad-help.csv');
477
+ if (fs.existsSync(bmadHelpPath)) {
478
+ let content = await fs.readFile(bmadHelpPath, 'utf-8');
479
+ for (const entry of mlEntries) {
480
+ const row = `bmm,anytime,${entry.name},${entry.code},,skills/${entry.command}/SKILL.md,${entry.command},false,bmm-demerzel,bmad:Machine Learning Lifecycle:agent:demerzel,Demerzel,🧪 ML Scientist,Create Mode,${entry.description},planning_artifacts,document`;
481
+ // Remove any existing line for this command, then append the current one
482
+ const lines = content.split('\n').filter(line => !line.includes(entry.command));
483
+ content = lines.join('\n').trimEnd() + '\n' + row + '\n';
484
+ }
485
+ await fs.writeFile(bmadHelpPath, content, 'utf-8');
486
+ console.log(chalk.cyan(` + Registered ML workflows in bmad-help.csv`));
487
+ }
488
+ }
489
+
414
490
  /**
415
491
  * Deploy the methodology presentation to the target project's _bmad-output/methodology/ directory.
416
492
  * Version-aware: skips if target is same or newer; overwrites only with --force.
@@ -521,7 +597,7 @@ async function prePopulateBmadCache(force = false) {
521
597
 
522
598
  // ── Migration constants ─────────────────────────────────────────────────────
523
599
 
524
- const BMAD_TARGET_VERSION = '6.2.1';
600
+ const BMAD_TARGET_VERSION = '6.2.2';
525
601
  const BMAD_MIGRATION_THRESHOLD = '6.2.0'; // versions below this need migration
526
602
  const BACKUP_DIR_NAME = '.backup-pre-migration';
527
603
 
@@ -1018,4 +1094,5 @@ module.exports = {
1018
1094
  mergeUserCustomizations,
1019
1095
  cleanupLegacyArtifacts,
1020
1096
  parseCustomizeYaml,
1097
+ registerMlWorkflows,
1021
1098
  };
@@ -405,7 +405,7 @@ async function handleSetMandatory(args) {
405
405
 
406
406
  // ─── Story 3.4: BMAD Persona Customization Tooling ───────────────────────────
407
407
 
408
- const CUSTOM_AGENTS = ['bmm-sre', 'bmm-devops', 'bmm-cyber', 'bmm-mil498'];
408
+ const CUSTOM_AGENTS = ['bmm-sre', 'bmm-devops', 'bmm-cyber', 'bmm-mil498', 'bmm-demerzel'];
409
409
  const BUILTIN_AGENTS = ['bmm-pm', 'bmm-architect', 'bmm-dev', 'bmm-qa', 'bmm-sm', 'bmm-tech-writer', 'bmm-ux-designer'];
410
410
 
411
411
  const MANDATORY_CRITICAL_ACTIONS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ma-agents",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "NPX tool to install skills for AI coding agents (Claude Code, Gemini, Copilot, Kilocode, Cline, Cursor, Roo Code)",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -27,7 +27,7 @@
27
27
  "author": "",
28
28
  "license": "MIT",
29
29
  "dependencies": {
30
- "bmad-method": "6.2.1",
30
+ "bmad-method": "6.2.2",
31
31
  "prompts": "^2.4.2",
32
32
  "chalk": "^4.1.2",
33
33
  "fs-extra": "^11.1.1"
@@ -66,15 +66,15 @@ test('3.3: installer.js contains NO agent-name-specific logic', () => {
66
66
  console.log('\nTask 4 — Registry completeness');
67
67
 
68
68
  const EXPECTED_IDE = ['claude-code', 'gemini', 'copilot', 'kilocode', 'cline', 'cursor', 'antigravity', 'opencode'];
69
- const EXPECTED_BMAD = ['bmm-sre', 'bmm-devops', 'bmm-cyber', 'bmm-mil498'];
69
+ const EXPECTED_BMAD = ['bmm-sre', 'bmm-devops', 'bmm-cyber', 'bmm-mil498', 'bmm-demerzel'];
70
70
  const EXPECTED_ALL = [...EXPECTED_IDE, ...EXPECTED_BMAD];
71
71
 
72
- test('4.1: registry has at least 12 agents (8+ IDE + 4 BMAD)', () => {
73
- assert.ok(allAgents.length >= 12, `Expected at least 12 agents, got ${allAgents.length}`);
72
+ test('4.1: registry has at least 13 agents (8+ IDE + 5 BMAD)', () => {
73
+ assert.ok(allAgents.length >= 13, `Expected at least 13 agents, got ${allAgents.length}`);
74
74
  const ideCount = allAgents.filter(a => a.category === 'ide').length;
75
75
  const bmadCount = allAgents.filter(a => a.category === 'bmad').length;
76
76
  assert.ok(ideCount >= 8, `Expected at least 8 IDE agents, got ${ideCount}`);
77
- assert.strictEqual(bmadCount, 4, `Expected 4 BMAD agents, got ${bmadCount}`);
77
+ assert.strictEqual(bmadCount, 5, `Expected 5 BMAD agents, got ${bmadCount}`);
78
78
  });
79
79
 
80
80
  test('4.2: every agent has well-shaped injectionStrategy property', () => {
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Tests for Story 15.1: Bump bmad-method to 6.2.1 and Update Cache
3
+ * Tests for Story 15.1: Bump bmad-method to 6.2.2 and Update Cache
4
4
  *
5
5
  * Task 1: Verify package.json dependency updated
6
6
  * Task 2: Verify build:bmad-cache compatibility
@@ -31,22 +31,22 @@ function test(name, fn) {
31
31
 
32
32
  const projectRoot = path.resolve(__dirname, '..');
33
33
 
34
- console.log('\nStory 15.1: Bump bmad-method to 6.2.1 and Update Cache\n');
34
+ console.log('\nStory 15.1: Bump bmad-method to 6.2.2 and Update Cache\n');
35
35
 
36
36
  // ---- Task 1: package.json dependency ----
37
37
 
38
38
  console.log('Task 1: package.json dependency');
39
39
 
40
- test('package.json pins bmad-method at 6.2.1', () => {
40
+ test('package.json pins bmad-method at 6.2.2', () => {
41
41
  const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
42
- assert.strictEqual(pkg.dependencies['bmad-method'], '6.2.1');
42
+ assert.strictEqual(pkg.dependencies['bmad-method'], '6.2.2');
43
43
  });
44
44
 
45
- test('installed bmad-method is version 6.2.1', () => {
45
+ test('installed bmad-method is version 6.2.2', () => {
46
46
  const bmadPkg = JSON.parse(
47
47
  fs.readFileSync(path.join(projectRoot, 'node_modules', 'bmad-method', 'package.json'), 'utf8')
48
48
  );
49
- assert.strictEqual(bmadPkg.version, '6.2.1');
49
+ assert.strictEqual(bmadPkg.version, '6.2.2');
50
50
  });
51
51
 
52
52
  test('require.resolve finds bmad-npx-wrapper.js', () => {
@@ -59,13 +59,13 @@ test('require.resolve finds bmad-npx-wrapper.js', () => {
59
59
 
60
60
  console.log('\nTask 2: build:bmad-cache compatibility');
61
61
 
62
- test('external-official-modules.yaml exists in 6.2.1', () => {
62
+ test('external-official-modules.yaml exists in 6.2.2', () => {
63
63
  const bmadDir = path.dirname(require.resolve('bmad-method/package.json'));
64
64
  const yamlPath = path.join(bmadDir, 'tools', 'cli', 'external-official-modules.yaml');
65
65
  assert.ok(fs.existsSync(yamlPath), `YAML not found at ${yamlPath}`);
66
66
  });
67
67
 
68
- test('YAML parser extracts 5 modules from 6.2.1', () => {
68
+ test('YAML parser extracts 5 modules from 6.2.2', () => {
69
69
  // Import the parser from build-bmad-cache.js by reading and evaluating it
70
70
  const bmadDir = path.dirname(require.resolve('bmad-method/package.json'));
71
71
  const yamlPath = path.join(bmadDir, 'tools', 'cli', 'external-official-modules.yaml');
@@ -133,10 +133,10 @@ test('cache-manifest.json exists and lists 5 modules', () => {
133
133
  assert.deepStrictEqual(moduleCodes.sort(), ['bmb', 'cis', 'gds', 'tea', 'wds']);
134
134
  });
135
135
 
136
- test('cache-manifest.json records bmadMethodVersion as 6.2.1', () => {
136
+ test('cache-manifest.json records bmadMethodVersion as 6.2.2', () => {
137
137
  const manifestPath = path.join(projectRoot, 'lib', 'bmad-cache', 'cache-manifest.json');
138
138
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
139
- assert.strictEqual(manifest.bmadMethodVersion, '6.2.1');
139
+ assert.strictEqual(manifest.bmadMethodVersion, '6.2.2');
140
140
  });
141
141
 
142
142
  test('each cached module has a commit SHA in manifest', () => {
@@ -171,7 +171,7 @@ console.log('\nTask 3: bmad.js CLI compatibility');
171
171
 
172
172
  const bmad = require('../lib/bmad');
173
173
 
174
- test('getBmadCommand resolves bmad-npx-wrapper.js from 6.2.1', () => {
174
+ test('getBmadCommand resolves bmad-npx-wrapper.js from 6.2.2', () => {
175
175
  const result = bmad.buildBmadArgs({
176
176
  projectRoot: '/test',
177
177
  modules: ['bmm'],
@@ -192,82 +192,82 @@ test('buildBmadArgs generates valid --action update flag', () => {
192
192
  assert.ok(result.includes('--action update'), `Expected --action update in: ${result}`);
193
193
  });
194
194
 
195
- test('6.2.1 CLI accepts --directory flag', () => {
195
+ test('6.2.2 CLI accepts --directory flag', () => {
196
196
  const bmadDir = path.dirname(require.resolve('bmad-method/package.json'));
197
197
  const installCmd = path.join(bmadDir, 'tools', 'cli', 'commands', 'install.js');
198
198
  const content = fs.readFileSync(installCmd, 'utf8');
199
- assert.ok(content.includes('--directory'), '6.2.1 install.js missing --directory flag');
199
+ assert.ok(content.includes('--directory'), '6.2.2 install.js missing --directory flag');
200
200
  });
201
201
 
202
- test('6.2.1 CLI accepts --modules flag', () => {
202
+ test('6.2.2 CLI accepts --modules flag', () => {
203
203
  const bmadDir = path.dirname(require.resolve('bmad-method/package.json'));
204
204
  const installCmd = path.join(bmadDir, 'tools', 'cli', 'commands', 'install.js');
205
205
  const content = fs.readFileSync(installCmd, 'utf8');
206
- assert.ok(content.includes('--modules'), '6.2.1 install.js missing --modules flag');
206
+ assert.ok(content.includes('--modules'), '6.2.2 install.js missing --modules flag');
207
207
  });
208
208
 
209
- test('6.2.1 CLI accepts --tools flag', () => {
209
+ test('6.2.2 CLI accepts --tools flag', () => {
210
210
  const bmadDir = path.dirname(require.resolve('bmad-method/package.json'));
211
211
  const installCmd = path.join(bmadDir, 'tools', 'cli', 'commands', 'install.js');
212
212
  const content = fs.readFileSync(installCmd, 'utf8');
213
- assert.ok(content.includes('--tools'), '6.2.1 install.js missing --tools flag');
213
+ assert.ok(content.includes('--tools'), '6.2.2 install.js missing --tools flag');
214
214
  });
215
215
 
216
- test('6.2.1 CLI accepts --custom-content flag', () => {
216
+ test('6.2.2 CLI accepts --custom-content flag', () => {
217
217
  const bmadDir = path.dirname(require.resolve('bmad-method/package.json'));
218
218
  const installCmd = path.join(bmadDir, 'tools', 'cli', 'commands', 'install.js');
219
219
  const content = fs.readFileSync(installCmd, 'utf8');
220
- assert.ok(content.includes('--custom-content'), '6.2.1 install.js missing --custom-content flag');
220
+ assert.ok(content.includes('--custom-content'), '6.2.2 install.js missing --custom-content flag');
221
221
  });
222
222
 
223
- test('6.2.1 CLI accepts --action flag', () => {
223
+ test('6.2.2 CLI accepts --action flag', () => {
224
224
  const bmadDir = path.dirname(require.resolve('bmad-method/package.json'));
225
225
  const installCmd = path.join(bmadDir, 'tools', 'cli', 'commands', 'install.js');
226
226
  const content = fs.readFileSync(installCmd, 'utf8');
227
- assert.ok(content.includes('--action'), '6.2.1 install.js missing --action flag');
227
+ assert.ok(content.includes('--action'), '6.2.2 install.js missing --action flag');
228
228
  });
229
229
 
230
- test('6.2.1 CLI accepts --yes flag', () => {
230
+ test('6.2.2 CLI accepts --yes flag', () => {
231
231
  const bmadDir = path.dirname(require.resolve('bmad-method/package.json'));
232
232
  const installCmd = path.join(bmadDir, 'tools', 'cli', 'commands', 'install.js');
233
233
  const content = fs.readFileSync(installCmd, 'utf8');
234
- assert.ok(content.includes('--yes'), '6.2.1 install.js missing --yes flag');
234
+ assert.ok(content.includes('--yes'), '6.2.2 install.js missing --yes flag');
235
235
  });
236
236
 
237
- test('6.2.1 CLI accepts --user-name flag', () => {
237
+ test('6.2.2 CLI accepts --user-name flag', () => {
238
238
  const bmadDir = path.dirname(require.resolve('bmad-method/package.json'));
239
239
  const installCmd = path.join(bmadDir, 'tools', 'cli', 'commands', 'install.js');
240
240
  const content = fs.readFileSync(installCmd, 'utf8');
241
- assert.ok(content.includes('--user-name'), '6.2.1 install.js missing --user-name flag');
241
+ assert.ok(content.includes('--user-name'), '6.2.2 install.js missing --user-name flag');
242
242
  });
243
243
 
244
- test('6.2.1 CLI accepts --communication-language flag', () => {
244
+ test('6.2.2 CLI accepts --communication-language flag', () => {
245
245
  const bmadDir = path.dirname(require.resolve('bmad-method/package.json'));
246
246
  const installCmd = path.join(bmadDir, 'tools', 'cli', 'commands', 'install.js');
247
247
  const content = fs.readFileSync(installCmd, 'utf8');
248
- assert.ok(content.includes('--communication-language'), '6.2.1 install.js missing --communication-language flag');
248
+ assert.ok(content.includes('--communication-language'), '6.2.2 install.js missing --communication-language flag');
249
249
  });
250
250
 
251
- test('6.2.1 CLI accepts --document-output-language flag', () => {
251
+ test('6.2.2 CLI accepts --document-output-language flag', () => {
252
252
  const bmadDir = path.dirname(require.resolve('bmad-method/package.json'));
253
253
  const installCmd = path.join(bmadDir, 'tools', 'cli', 'commands', 'install.js');
254
254
  const content = fs.readFileSync(installCmd, 'utf8');
255
- assert.ok(content.includes('--document-output-language'), '6.2.1 install.js missing --document-output-language flag');
255
+ assert.ok(content.includes('--document-output-language'), '6.2.2 install.js missing --document-output-language flag');
256
256
  });
257
257
 
258
- test('6.2.1 CLI accepts --output-folder flag', () => {
258
+ test('6.2.2 CLI accepts --output-folder flag', () => {
259
259
  const bmadDir = path.dirname(require.resolve('bmad-method/package.json'));
260
260
  const installCmd = path.join(bmadDir, 'tools', 'cli', 'commands', 'install.js');
261
261
  const content = fs.readFileSync(installCmd, 'utf8');
262
- assert.ok(content.includes('--output-folder'), '6.2.1 install.js missing --output-folder flag');
262
+ assert.ok(content.includes('--output-folder'), '6.2.2 install.js missing --output-folder flag');
263
263
  });
264
264
 
265
- test('module.yaml custom-content format is accepted by 6.2.1', () => {
265
+ test('module.yaml custom-content format is accepted by 6.2.2', () => {
266
266
  const bmadDir = path.dirname(require.resolve('bmad-method/package.json'));
267
267
  const handlerPath = path.join(bmadDir, 'tools', 'cli', 'installers', 'lib', 'custom', 'handler.js');
268
268
  const content = fs.readFileSync(handlerPath, 'utf8');
269
- // 6.2.1 still looks for module.yaml files
270
- assert.ok(content.includes('module.yaml'), '6.2.1 handler.js no longer references module.yaml');
269
+ // 6.2.2 still looks for module.yaml files
270
+ assert.ok(content.includes('module.yaml'), '6.2.2 handler.js no longer references module.yaml');
271
271
  });
272
272
 
273
273
  // ---- Task 4: prePopulateBmadCache compatibility ----
@@ -218,7 +218,7 @@ test('buildBmadArgs quotes path with spaces in --directory', () => {
218
218
  assert.ok(result.includes('--directory "D:\\My Projects\\test project"'), `Expected quoted path in: ${result}`);
219
219
  });
220
220
 
221
- test('buildBmadArgs includes --custom-content when extension exists', () => {
221
+ test('buildBmadArgs includes --custom-content on fresh install when extension exists', () => {
222
222
  const fs = require('fs');
223
223
  const extensionPath = path.join(__dirname, '..', 'lib', 'bmad-extension');
224
224
  // This test requires lib/bmad-extension/ to exist (it does in this repo)
@@ -234,6 +234,16 @@ test('buildBmadArgs includes --custom-content when extension exists', () => {
234
234
  assert.ok(result.includes(`"${extensionPath}"`), `Expected quoted extension path in: ${result}`);
235
235
  });
236
236
 
237
+ test('buildBmadArgs omits --custom-content on update to avoid stale cache conflict', () => {
238
+ const result = bmad.buildBmadArgs({
239
+ projectRoot: '/test',
240
+ modules: ['bmm'],
241
+ tools: [],
242
+ action: 'update',
243
+ });
244
+ assert.ok(!result.includes('--custom-content'), `Expected NO --custom-content on update, got: ${result}`);
245
+ });
246
+
237
247
  test('buildBmadArgs includes all params in correct combined command', () => {
238
248
  const result = bmad.buildBmadArgs({
239
249
  projectRoot: '/test/project',
@@ -268,14 +278,11 @@ test('buildBmadArgs starts with node and bmad-npx-wrapper path', () => {
268
278
  assert.ok(result.includes('bmad-npx-wrapper.js'), `Expected bmad-npx-wrapper.js in: ${result}`);
269
279
  });
270
280
 
271
- // ---- M4: Negative test for --custom-content omission ----
281
+ // ---- M4: --custom-content conditional tests ----
272
282
 
273
283
  console.log('\nCode review fixes: edge cases');
274
284
 
275
- test('buildBmadArgs omits --custom-content when extension dir missing (simulated via __dirname)', () => {
276
- // We can't remove the real dir, but we verify the conditional logic by checking
277
- // that the function correctly includes it when present (tested above).
278
- // This test validates the conditional branch exists by checking the output format.
285
+ test('buildBmadArgs includes exactly one --custom-content on fresh install', () => {
279
286
  const result = bmad.buildBmadArgs({
280
287
  projectRoot: '/test',
281
288
  modules: ['bmm'],
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Tests for Story 15-3: Convert 4 Custom Agents to BMAD 6.2.0 Skill Folders
3
+ * Tests for Story 15-3: Convert 5 Custom Agents to BMAD 6.2.0 Skill Folders
4
4
  *
5
5
  * Verifies:
6
6
  * - Each agent has SKILL.md and bmad-skill-manifest.yaml
@@ -74,6 +74,16 @@ const AGENTS = [
74
74
  oldPaths: ['bmm/workflows/mil498/', '_bmad/bmm/workflows/mil498/'],
75
75
  chatTopic: 'MIL-STD-498',
76
76
  menuItemCount: 10 // MH, CH, GS, GD, GP, GO, SS, GT, SD, DA
77
+ },
78
+ {
79
+ folder: 'bmad-ma-agent-ml',
80
+ name: 'bmad-ma-agent-ml',
81
+ displayName: 'Demerzel',
82
+ title: 'ML Scientist',
83
+ skillNames: ['ml-ideation', 'ml-eda', 'ml-architecture', 'ml-detailed-design', 'ml-techspec', 'ml-infra', 'ml-experiment', 'ml-analysis', 'ml-hparam', 'ml-revision', 'ml-advise', 'ml-retrospective'],
84
+ oldPaths: ['bmm/workflows/ml/', '_bmad/bmm/workflows/ml/'],
85
+ chatTopic: 'Machine Learning',
86
+ menuItemCount: 15 // MH, CH, MLI, MLE, MLA, MLD, MLS, MLNF, MLX, MLAN, MLH, MLR, MLAD, MLRT, DA
77
87
  }
78
88
  ];
79
89
 
@@ -139,6 +139,7 @@ const expectedAgentSkills = [
139
139
  'bmad-ma-agent-devops',
140
140
  'bmad-ma-agent-cyber',
141
141
  'bmad-ma-agent-mil498',
142
+ 'bmad-ma-agent-ml',
142
143
  ];
143
144
 
144
145
  const expectedMil498Skills = [
@@ -166,6 +167,13 @@ const expectedCyberSkills = [
166
167
  const expectedExtensionSkills = [
167
168
  'create-bug-story', 'add-sprint', 'modify-sprint',
168
169
  'add-to-sprint', 'project-context-expansion', 'sprint-status-view',
170
+ 'cleanup-done', 'generate-backlog', 'prioritize-backlog', 'remove-from-sprint',
171
+ ];
172
+
173
+ const expectedMlSkills = [
174
+ 'ml-ideation', 'ml-eda', 'ml-architecture', 'ml-detailed-design',
175
+ 'ml-techspec', 'ml-infra', 'ml-experiment', 'ml-analysis',
176
+ 'ml-hparam', 'ml-revision', 'ml-advise', 'ml-retrospective',
169
177
  ];
170
178
 
171
179
  const allExpectedSkills = [
@@ -175,16 +183,17 @@ const allExpectedSkills = [
175
183
  ...expectedDevopsSkills,
176
184
  ...expectedCyberSkills,
177
185
  ...expectedExtensionSkills,
186
+ ...expectedMlSkills,
178
187
  ];
179
188
 
180
- test('skills/ has exactly 36 subdirectories', () => {
189
+ test('skills/ has exactly 53 subdirectories', () => {
181
190
  const entries = fs.readdirSync(skillsDir).filter(e =>
182
191
  fs.statSync(path.join(skillsDir, e)).isDirectory()
183
192
  );
184
- assert.strictEqual(entries.length, 36, `Expected 36 skill dirs, found ${entries.length}`);
193
+ assert.strictEqual(entries.length, 53, `Expected 53 skill dirs, found ${entries.length}`);
185
194
  });
186
195
 
187
- test('all 4 agent skill directories exist', () => {
196
+ test('all 5 agent skill directories exist', () => {
188
197
  for (const skill of expectedAgentSkills) {
189
198
  const p = path.join(skillsDir, skill);
190
199
  assert.ok(fs.existsSync(p), `Missing agent skill dir: ${skill}`);
@@ -219,7 +228,14 @@ test('all 7 Cyber skill directories exist', () => {
219
228
  }
220
229
  });
221
230
 
222
- test('all 6 extension workflow skill directories exist', () => {
231
+ test('all 12 ML skill directories exist', () => {
232
+ for (const skill of expectedMlSkills) {
233
+ const p = path.join(skillsDir, skill);
234
+ assert.ok(fs.existsSync(p), `Missing ML skill dir: ${skill}`);
235
+ }
236
+ });
237
+
238
+ test('all 10 extension workflow skill directories exist', () => {
223
239
  for (const skill of expectedExtensionSkills) {
224
240
  const p = path.join(skillsDir, skill);
225
241
  assert.ok(fs.existsSync(p), `Missing extension skill dir: ${skill}`);
@@ -259,8 +275,8 @@ test('module-help.csv has correct header columns', () => {
259
275
  }
260
276
  });
261
277
 
262
- test('module-help.csv has 36 skill entries (excluding header)', () => {
263
- assert.strictEqual(csvData.rows.length, 36, `Expected 36 rows, found ${csvData.rows.length}`);
278
+ test('module-help.csv has 53 skill entries (excluding header)', () => {
279
+ assert.strictEqual(csvData.rows.length, 53, `Expected 53 rows, found ${csvData.rows.length}`);
264
280
  });
265
281
 
266
282
  test('all CSV entries use module code ma-skills', () => {
@@ -310,7 +326,7 @@ test('CSV has entries for all 7 Cyber skills', () => {
310
326
  }
311
327
  });
312
328
 
313
- test('CSV has entries for all 6 extension workflow skills', () => {
329
+ test('CSV has entries for all 10 extension workflow skills', () => {
314
330
  const codeIdx = csvData.headers.indexOf('code');
315
331
  const codes = csvData.rows.map(r => r[codeIdx]);
316
332
  for (const skill of expectedExtensionSkills) {
@@ -318,6 +334,14 @@ test('CSV has entries for all 6 extension workflow skills', () => {
318
334
  }
319
335
  });
320
336
 
337
+ test('CSV has entries for all 12 ML skills', () => {
338
+ const codeIdx = csvData.headers.indexOf('code');
339
+ const codes = csvData.rows.map(r => r[codeIdx]);
340
+ for (const skill of expectedMlSkills) {
341
+ assert.ok(codes.includes(skill), `Missing CSV entry for ML skill: ${skill}`);
342
+ }
343
+ });
344
+
321
345
  // ═══════════════════════════════════════════════════════════════════════════════
322
346
  // Task 4: Directory state after Story 15.6 migration
323
347
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -348,11 +348,11 @@ test('5.2: module.yaml has extends-module: bmm', () => {
348
348
  assert.ok(content.includes('extends-module: bmm'), 'module.yaml must declare extends-module: bmm');
349
349
  });
350
350
 
351
- test('5.3: skills/ contains exactly 36 skill folders', () => {
351
+ test('5.3: skills/ contains exactly 53 skill folders', () => {
352
352
  const folders = fs.readdirSync(SKILLS_DIR).filter(f =>
353
353
  fs.statSync(path.join(SKILLS_DIR, f)).isDirectory());
354
- assert.strictEqual(folders.length, 36,
355
- `Expected 36 skill folders, found ${folders.length}: ${folders.join(', ')}`);
354
+ assert.strictEqual(folders.length, 53,
355
+ `Expected 53 skill folders, found ${folders.length}: ${folders.join(', ')}`);
356
356
  });
357
357
 
358
358
  test('5.4: every skill folder has SKILL.md', () => {
@@ -373,12 +373,12 @@ test('5.5: every skill folder has bmad-skill-manifest.yaml', () => {
373
373
  }
374
374
  });
375
375
 
376
- test('5.6: module-help.csv exists and lists all 36 skills', () => {
376
+ test('5.6: module-help.csv exists and lists all 53 skills', () => {
377
377
  const csv = fs.readFileSync(path.join(EXT_DIR, 'module-help.csv'), 'utf-8');
378
378
  const lines = csv.split('\n').filter(l => l.trim() && !l.startsWith('module,'));
379
- // Should have a line for each of the 36 skills (header excluded)
380
- assert.strictEqual(lines.length, 36,
381
- `Expected 36 data rows in module-help.csv, found ${lines.length}`);
379
+ // Should have a line for each of the 53 skills (header excluded)
380
+ assert.strictEqual(lines.length, 53,
381
+ `Expected 53 data rows in module-help.csv, found ${lines.length}`);
382
382
  });
383
383
 
384
384
  test('5.7: module-help.csv references module code ma-skills', () => {
@@ -390,7 +390,7 @@ test('5.7: module-help.csv references module code ma-skills', () => {
390
390
  }
391
391
  });
392
392
 
393
- test('5.8: skill folder breakdown: 4 agents + 7 mil498 + 7 sre + 5 devops + 7 cyber + 6 extension', () => {
393
+ test('5.8: skill folder breakdown: 5 agents + 7 mil498 + 7 sre + 5 devops + 7 cyber + 12 ml + 10 extension', () => {
394
394
  const folders = fs.readdirSync(SKILLS_DIR).filter(f =>
395
395
  fs.statSync(path.join(SKILLS_DIR, f)).isDirectory());
396
396
 
@@ -399,16 +399,19 @@ test('5.8: skill folder breakdown: 4 agents + 7 mil498 + 7 sre + 5 devops + 7 cy
399
399
  const sre = folders.filter(f => f.startsWith('sre-'));
400
400
  const devops = folders.filter(f => f.startsWith('devops-'));
401
401
  const cyber = folders.filter(f => f.startsWith('cyber-'));
402
+ const ml = folders.filter(f => f.startsWith('ml-'));
402
403
  const ext = folders.filter(f =>
403
404
  !f.startsWith('bmad-ma-agent-') && !f.startsWith('mil498-') &&
404
- !f.startsWith('sre-') && !f.startsWith('devops-') && !f.startsWith('cyber-'));
405
+ !f.startsWith('sre-') && !f.startsWith('devops-') && !f.startsWith('cyber-') &&
406
+ !f.startsWith('ml-'));
405
407
 
406
- assert.strictEqual(agents.length, 4, `Expected 4 agent folders, found ${agents.length}`);
408
+ assert.strictEqual(agents.length, 5, `Expected 5 agent folders, found ${agents.length}`);
407
409
  assert.strictEqual(mil498.length, 7, `Expected 7 mil498 folders, found ${mil498.length}`);
408
410
  assert.strictEqual(sre.length, 7, `Expected 7 sre folders, found ${sre.length}`);
409
411
  assert.strictEqual(devops.length, 5, `Expected 5 devops folders, found ${devops.length}`);
410
412
  assert.strictEqual(cyber.length, 7, `Expected 7 cyber folders, found ${cyber.length}`);
411
- assert.strictEqual(ext.length, 6, `Expected 6 extension folders, found ${ext.length}`);
413
+ assert.strictEqual(ml.length, 12, `Expected 12 ml folders, found ${ml.length}`);
414
+ assert.strictEqual(ext.length, 10, `Expected 10 extension folders, found ${ext.length}`);
412
415
  });
413
416
 
414
417
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━