bmad-plus 0.16.0 → 0.17.1

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 (66) hide show
  1. package/CHANGELOG.md +114 -29
  2. package/README.md +45 -27
  3. package/SECURITY.md +9 -1
  4. package/osint-agent-package/README.md +13 -0
  5. package/osint-agent-package/SETUP_KEYS.md +13 -0
  6. package/package.json +32 -8
  7. package/readme-international/README.de.md +134 -537
  8. package/readme-international/README.es.md +136 -556
  9. package/readme-international/README.fr.md +135 -553
  10. package/src/bmad-plus/agents/agent-shadow/SKILL.md +6 -0
  11. package/src/bmad-plus/packs/pack-memory/README.md +1 -1
  12. package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +1 -1
  13. package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +1 -1
  14. package/src/bmad-plus/packs/pack-shield/README.md +2 -0
  15. package/src/bmad-plus/packs/pack-shield/SKILL.md +2 -0
  16. package/src/bmad-plus/packs/pack-shield/shared/audit-report-template.md +6 -0
  17. package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +4 -0
  18. package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +18 -4
  19. package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +95 -19
  20. package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +90 -10
  21. package/src/bmad-plus/skills/bmad-plus-uat/templates/README.md +63 -0
  22. package/src/bmad-plus/skills/bmad-plus-uat/templates/example-uat-spec.json +95 -0
  23. package/src/bmad-plus/skills/bmad-plus-uat/templates/uat-release-gate.mjs +127 -0
  24. package/src/bmad-plus/skills/bmad-plus-uat/templates/uat-spec-present.mjs +77 -0
  25. package/tools/build/adapters.config.js +1 -1
  26. package/tools/build/generate-adapters.js +186 -60
  27. package/tools/build/generate.js +49 -13
  28. package/tools/cli/bmad-plus-cli.js +3 -6
  29. package/tools/cli/commands/autoconfig.js +66 -24
  30. package/tools/cli/commands/install.js +620 -356
  31. package/tools/cli/commands/memory.js +40 -13
  32. package/tools/cli/commands/scan.js +94 -30
  33. package/tools/cli/commands/uat.js +21 -5
  34. package/tools/cli/commands/uninstall.js +65 -16
  35. package/tools/cli/commands/update-check.js +8 -3
  36. package/tools/cli/commands/update-policy.js +14 -5
  37. package/tools/cli/commands/update.js +89 -20
  38. package/tools/cli/i18n.js +128 -66
  39. package/tools/cli/lib/ide-config.js +3 -1
  40. package/tools/cli/lib/install-manifest.js +13 -6
  41. package/tools/cli/lib/installation-health.js +33 -1
  42. package/tools/cli/lib/installed-adapters.js +29 -6
  43. package/tools/cli/lib/memory-init.js +19 -4
  44. package/tools/cli/lib/packs.js +17 -28
  45. package/tools/cli/lib/path-hash.js +5 -1
  46. package/tools/cli/lib/python-provision.js +51 -12
  47. package/tools/cli/lib/stack-detect.js +51 -25
  48. package/tools/cli/lib/uat.js +18 -0
  49. package/tools/cli/lib/update-check.js +132 -34
  50. package/tools/cli/lib/update-dispatch.js +2 -2
  51. package/tools/cli/lib/update-policy.js +57 -19
  52. package/tools/cli/lib/update-transaction.js +439 -85
  53. package/tools/build/README.md +0 -78
  54. package/tools/build/check-counts.js +0 -626
  55. package/tools/build/check-install-contract.js +0 -648
  56. package/tools/build/generated-adapters/.codex/AGENTS.md +0 -134
  57. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +0 -139
  58. package/tools/build/generated-adapters/.opencode/AGENTS.md +0 -134
  59. package/tools/build/generated-adapters/AGENTS.md +0 -132
  60. package/tools/build/generated-adapters/CLAUDE.md +0 -135
  61. package/tools/build/generated-adapters/CONVENTIONS.md +0 -134
  62. package/tools/build/generated-adapters/GEMINI.md +0 -139
  63. package/tools/build/generated-adapters/README.md +0 -79
  64. package/tools/maintain/upstream-candidate.js +0 -456
  65. package/tools/release/publication-content.js +0 -903
  66. package/tools/release/supply-chain.js +0 -282
@@ -43,7 +43,12 @@
43
43
  const fs = require('node:fs');
44
44
  const path = require('node:path');
45
45
  const { loadRegistry, buildDerived, DEFAULT_REGISTRY_PATH } = require('./generate');
46
- const { PROJECT_INSTRUCTIONS, MEMORY_INSTRUCTIONS, UPDATE_INSTRUCTIONS, TOOL_METADATA } = require('./adapters.config');
46
+ const {
47
+ PROJECT_INSTRUCTIONS,
48
+ MEMORY_INSTRUCTIONS,
49
+ UPDATE_INSTRUCTIONS,
50
+ TOOL_METADATA,
51
+ } = require('./adapters.config');
47
52
 
48
53
  const DEFAULT_OUT_DIR = path.join(__dirname, 'generated-adapters');
49
54
  const REPO_ROOT = path.join(__dirname, '..', '..');
@@ -108,7 +113,13 @@ function validateTargets(registry) {
108
113
  throw new Error('registry.yaml: targets.adapters must be a non-empty list');
109
114
  }
110
115
  t.adapters.forEach((a, i) => {
111
- if (!a || typeof a.tool !== 'string' || a.tool === '' || typeof a.file !== 'string' || a.file === '') {
116
+ if (
117
+ !a ||
118
+ typeof a.tool !== 'string' ||
119
+ a.tool === '' ||
120
+ typeof a.file !== 'string' ||
121
+ a.file === ''
122
+ ) {
112
123
  throw new Error(`registry.yaml: targets.adapters[${i}] must have string "tool" and "file"`);
113
124
  }
114
125
  validateRelativeFile(a.file);
@@ -118,13 +129,19 @@ function validateTargets(registry) {
118
129
 
119
130
  /** Convert the canonical derivation into the adapter presentation shape. */
120
131
  function factsFromDerived(derived, packs = derived.packOrder) {
121
- const selected = packs.map(id => {
132
+ const selected = packs.map((id) => {
122
133
  const pack = derived.packs[id];
123
134
  return {
124
- id, name: pack.name, displayName: pack.displayName, desc: pack.desc,
125
- required: pack.required, agentCount: pack.installerAgentCount,
126
- categoryAgentCount: pack.categoryAgentCount, subAgentCount: pack.subAgentCount,
127
- workflowCount: pack.workflowCount, frameworkCount: pack.frameworkCount,
135
+ id,
136
+ name: pack.name,
137
+ displayName: pack.displayName,
138
+ desc: pack.desc,
139
+ required: pack.required,
140
+ agentCount: pack.installerAgentCount,
141
+ categoryAgentCount: pack.categoryAgentCount,
142
+ subAgentCount: pack.subAgentCount,
143
+ workflowCount: pack.workflowCount,
144
+ frameworkCount: pack.frameworkCount,
128
145
  personas: pack.personas,
129
146
  };
130
147
  });
@@ -133,9 +150,10 @@ function factsFromDerived(derived, packs = derived.packOrder) {
133
150
  spine: derived.targets.spine,
134
151
  modelsSupported: derived.targets.models_supported,
135
152
  packCount: selected.length,
136
- installerAgentCount: packs.length === derived.packOrder.length
137
- ? derived.installerAgents
138
- : selected.reduce((total, pack) => total + pack.agentCount, 0),
153
+ installerAgentCount:
154
+ packs.length === derived.packOrder.length
155
+ ? derived.installerAgents
156
+ : selected.reduce((total, pack) => total + pack.agentCount, 0),
139
157
  packs: selected,
140
158
  };
141
159
  }
@@ -153,8 +171,15 @@ function renderRoster(facts) {
153
171
  const identity = persona.alias
154
172
  ? '(' + persona.alias + ') — ' + persona.role
155
173
  : '(' + persona.role + ')';
156
- lines.push('- **' + persona.name + '** ' + identity + ' — ' + persona.description +
157
- (pack.required ? '' : ' (' + pack.name + ')'));
174
+ lines.push(
175
+ '- **' +
176
+ persona.name +
177
+ '** ' +
178
+ identity +
179
+ ' — ' +
180
+ persona.description +
181
+ (pack.required ? '' : ' (' + pack.name + ')')
182
+ );
158
183
  }
159
184
  }
160
185
  return lines;
@@ -346,40 +371,66 @@ function generateAllFiles(registry) {
346
371
  /** Installable tools and paths come from the registry; only detection is local. */
347
372
  function buildIDEConfigs(derived) {
348
373
  validateTargets({ targets: derived.targets });
349
- return Object.fromEntries(derived.targets.adapters.map(({ tool, file }) => [
350
- tool, { name: TOOL_METADATA[tool]?.name || tool, configFile: file,
351
- detect: [...(TOOL_METADATA[tool]?.detect || [])] },
352
- ]));
374
+ return Object.fromEntries(
375
+ derived.targets.adapters.map(({ tool, file }) => [
376
+ tool,
377
+ {
378
+ name: TOOL_METADATA[tool]?.name || tool,
379
+ configFile: file,
380
+ detect: [...(TOOL_METADATA[tool]?.detect || [])],
381
+ },
382
+ ])
383
+ );
353
384
  }
354
385
 
355
386
  function selectedPacks(derived, packs) {
356
- if (packs !== undefined && !Array.isArray(packs)) throw new Error('packs must be an array of pack IDs');
387
+ if (packs !== undefined && !Array.isArray(packs))
388
+ throw new Error('packs must be an array of pack IDs');
357
389
  const selected = new Set(packs === undefined ? derived.packOrder : packs);
358
390
  for (const id of selected) {
359
391
  if (!Object.hasOwn(derived.packs, id)) throw new Error('Unknown pack: ' + id);
360
392
  }
361
393
  for (const id of derived.packOrder) if (derived.packs[id].required) selected.add(id);
362
- return derived.packOrder.filter(id => selected.has(id));
394
+ return derived.packOrder.filter((id) => selected.has(id));
363
395
  }
364
396
 
365
397
  function renderUserSections(derived, { packs, userName = 'Developer', language = 'English' } = {}) {
366
398
  const ids = selectedPacks(derived, packs);
367
399
  const facts = factsFromDerived(derived, ids);
368
400
  const lines = [
369
- '## Project Context', '',
370
- 'This project uses ' + facts.product.displayName + ', an augmented AI-driven development framework.',
371
- 'Based on ' + facts.product.derivedFrom + ' with multi-role agents, autopilot mode, and parallel execution.',
372
- '', ...renderRoster(facts), '',
373
- '## Skills', '',
401
+ '## Project Context',
402
+ '',
403
+ 'This project uses ' +
404
+ facts.product.displayName +
405
+ ', an augmented AI-driven development framework.',
406
+ 'Based on ' +
407
+ facts.product.derivedFrom +
408
+ ' with multi-role agents, autopilot mode, and parallel execution.',
409
+ '',
410
+ ...renderRoster(facts),
411
+ '',
412
+ '## Skills',
413
+ '',
374
414
  '- Load skills from `.agents/skills/`.',
375
415
  '- Each agent has a SKILL.md with capabilities, activation protocol, and role-switching rules.',
376
416
  '- Auto-activation triggers: `.agents/data/role-triggers.yaml`.',
377
- '', '## Communication', '',
417
+ '',
418
+ '## Communication',
419
+ '',
378
420
  '- User name: ' + String(userName).replace(/[\r\n]/g, ' '),
379
- '- Default language: ' + String(language).replace(/[\r\n]/g, ' ') + ' for user-facing content, English for code and technical docs.',
421
+ '- Default language: ' +
422
+ String(language).replace(/[\r\n]/g, ' ') +
423
+ ' for user-facing content, English for code and technical docs.',
380
424
  ];
381
425
  if (ids.includes('memory')) lines.push('', ...MEMORY_INSTRUCTIONS);
382
- lines.push('', ...UPDATE_INSTRUCTIONS, '', ...renderFactsSection(facts), '', ...renderKeyCommands());
426
+ lines.push(
427
+ '',
428
+ ...UPDATE_INSTRUCTIONS,
429
+ '',
430
+ ...renderFactsSection(facts),
431
+ '',
432
+ ...renderKeyCommands()
433
+ );
383
434
  return lines;
384
435
  }
385
436
 
@@ -395,36 +446,61 @@ function userMarkerLines() {
395
446
  function renderUserConfig(derived, options = {}) {
396
447
  validateTargets({ targets: derived.targets });
397
448
  return [
398
- ...userMarkerLines(), '',
399
- '# ' + derived.product.displayName + ' — Agent Spine', '',
400
- 'This file is the common project instruction spine for the installed ' + derived.product.displayName + ' tools.', '',
401
- ...renderUserSections(derived, options), '',
449
+ ...userMarkerLines(),
450
+ '',
451
+ '# ' + derived.product.displayName + ' — Agent Spine',
452
+ '',
453
+ 'This file is the common project instruction spine for the installed ' +
454
+ derived.product.displayName +
455
+ ' tools.',
456
+ '',
457
+ ...renderUserSections(derived, options),
458
+ '',
402
459
  ].join('\n');
403
460
  }
404
461
 
405
462
  /** Pure render of the common spine and every selected registry adapter. */
406
463
  function generateUserFiles(derived, options = {}) {
407
464
  validateTargets({ targets: derived.targets });
408
- const available = derived.targets.adapters.map(adapter => adapter.tool);
465
+ const available = derived.targets.adapters.map((adapter) => adapter.tool);
409
466
  const requested = options.tools === undefined ? available : options.tools;
410
467
  if (!Array.isArray(requested)) throw new Error('tools must be an array of tool IDs');
411
468
  for (const tool of requested) {
412
469
  if (!available.includes(tool)) throw new Error('Unknown tool: ' + tool);
413
470
  }
414
- const targets = { ...derived.targets,
415
- adapters: derived.targets.adapters.filter(adapter => requested.includes(adapter.tool)) };
416
- const files = [{ file: targets.spine, tools: ['spine'], spine: true,
417
- content: renderUserConfig(derived, options) }];
471
+ const targets = {
472
+ ...derived.targets,
473
+ adapters: derived.targets.adapters.filter((adapter) => requested.includes(adapter.tool)),
474
+ };
475
+ const files = [
476
+ {
477
+ file: targets.spine,
478
+ tools: ['spine'],
479
+ spine: true,
480
+ content: renderUserConfig(derived, options),
481
+ },
482
+ ];
418
483
  for (const { file, tools } of groupAdaptersByFile({ targets })) {
419
484
  if (file === targets.spine) continue;
420
485
  const reference = path.posix.relative(path.posix.dirname(file), targets.spine);
421
486
  const lines = [];
422
- if (file.endsWith('.mdc')) lines.push('---',
423
- 'description: "' + derived.product.displayName + ' agents adapter"', 'alwaysApply: true', '---', '');
424
- lines.push(...userMarkerLines(), '',
425
- '# ' + derived.product.displayName + ' — Adapter for ' + tools.join(' + '), '',
487
+ if (file.endsWith('.mdc'))
488
+ lines.push(
489
+ '---',
490
+ 'description: "' + derived.product.displayName + ' agents adapter"',
491
+ 'alwaysApply: true',
492
+ '---',
493
+ ''
494
+ );
495
+ lines.push(
496
+ ...userMarkerLines(),
497
+ '',
498
+ '# ' + derived.product.displayName + ' — Adapter for ' + tools.join(' + '),
499
+ '',
426
500
  'Read the project spine first: [' + targets.spine + '](' + reference + ').',
427
- 'Paths in the shared project instructions below are relative to the project root.', '');
501
+ 'Paths in the shared project instructions below are relative to the project root.',
502
+ ''
503
+ );
428
504
  for (const tool of tools) {
429
505
  lines.push('## Tool notes — ' + tool, '');
430
506
  for (const note of TOOL_NOTES[tool] || buildDefaultNotes(tool, file)) lines.push('- ' + note);
@@ -438,8 +514,14 @@ function generateUserFiles(derived, options = {}) {
438
514
 
439
515
  /** Refuse portable-path escapes even when generated input has been modified. */
440
516
  function validateRelativeFile(file) {
441
- if (typeof file !== 'string' || !file || /[\\:]/.test(file) || file.includes('\0') ||
442
- path.posix.isAbsolute(file) || file.split('/').some(part => !part || part === '.' || part === '..')) {
517
+ if (
518
+ typeof file !== 'string' ||
519
+ !file ||
520
+ /[\\:]/.test(file) ||
521
+ file.includes('\0') ||
522
+ path.posix.isAbsolute(file) ||
523
+ file.split('/').some((part) => !part || part === '.' || part === '..')
524
+ ) {
443
525
  throw new Error('Adapter file must be a relative project path: ' + file);
444
526
  }
445
527
  }
@@ -450,20 +532,30 @@ function safeTarget(outDir, file) {
450
532
  const root = path.resolve(outDir);
451
533
  const target = path.resolve(root, file);
452
534
  const relative = path.relative(root, target);
453
- if (!relative || relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative)) {
535
+ if (
536
+ !relative ||
537
+ relative === '..' ||
538
+ relative.startsWith('..' + path.sep) ||
539
+ path.isAbsolute(relative)
540
+ ) {
454
541
  throw new Error('Adapter path escapes the target project: ' + file);
455
542
  }
456
543
  let current = path.parse(target).root;
457
544
  for (const part of target.slice(current.length).split(path.sep)) {
458
545
  current = path.join(current, part);
459
546
  let stat;
460
- try { stat = fs.lstatSync(current); } catch (error) {
547
+ try {
548
+ stat = fs.lstatSync(current);
549
+ } catch (error) {
461
550
  if (error.code === 'ENOENT') break;
462
551
  throw error;
463
552
  }
464
- if (stat.isSymbolicLink()) throw new Error('Adapter path contains a symlink or junction: ' + current);
465
- if (current === target && !stat.isFile()) throw new Error('Adapter target is not a regular file: ' + current);
466
- if (current !== target && !stat.isDirectory()) throw new Error('Adapter parent is not a directory: ' + current);
553
+ if (stat.isSymbolicLink())
554
+ throw new Error('Adapter path contains a symlink or junction: ' + current);
555
+ if (current === target && !stat.isFile())
556
+ throw new Error('Adapter target is not a regular file: ' + current);
557
+ if (current !== target && !stat.isDirectory())
558
+ throw new Error('Adapter parent is not a directory: ' + current);
467
559
  }
468
560
  return target;
469
561
  }
@@ -480,14 +572,24 @@ function writeFiles(outDir, files) {
480
572
  });
481
573
  }
482
574
 
483
- function writeUserAdapters({ outDir, derived = require('../cli/lib/packs').DERIVED,
484
- packs, userName, language, tools, force = false } = {}) {
575
+ function writeUserAdapters({
576
+ outDir,
577
+ derived = require('../cli/lib/packs').DERIVED,
578
+ packs,
579
+ userName,
580
+ language,
581
+ tools,
582
+ force = false,
583
+ } = {}) {
485
584
  if (!outDir) throw new Error('A target project directory is required');
486
585
  const files = generateUserFiles(derived, { packs, userName, language, tools });
487
586
  const blockers = listAdoptionBlockers(outDir, files, USER_CONFIG_MARKER);
488
587
  if (blockers.length && !force) {
489
- throw new Error('REFUSING to overwrite files not owned by this user configuration: ' +
490
- blockers.join(', ') + '. Review them and use --force to replace them explicitly.');
588
+ throw new Error(
589
+ 'REFUSING to overwrite files not owned by this user configuration: ' +
590
+ blockers.join(', ') +
591
+ '. Review them and use --force to replace them explicitly.'
592
+ );
491
593
  }
492
594
  return writeFiles(outDir, files);
493
595
  }
@@ -546,13 +648,17 @@ function parseOutDir(args) {
546
648
  const idx = args.indexOf('--out-dir');
547
649
  const targetIdx = args.indexOf('--target');
548
650
  if (targetIdx !== -1) {
549
- if (args.includes('--adopt') || idx !== -1) throw new Error('--target, --adopt and --out-dir are mutually exclusive');
651
+ if (args.includes('--adopt') || idx !== -1)
652
+ throw new Error('--target, --adopt and --out-dir are mutually exclusive');
550
653
  const value = args[targetIdx + 1];
551
654
  if (!value || value.startsWith('--')) throw new Error('--target requires a directory path');
552
655
  return path.resolve(value);
553
656
  }
554
657
  if (args.includes('--adopt')) {
555
- if (idx !== -1) throw new Error('--adopt and --out-dir are mutually exclusive (--adopt targets the repo root)');
658
+ if (idx !== -1)
659
+ throw new Error(
660
+ '--adopt and --out-dir are mutually exclusive (--adopt targets the repo root)'
661
+ );
556
662
  return REPO_ROOT;
557
663
  }
558
664
  if (idx === -1) return DEFAULT_OUT_DIR;
@@ -581,9 +687,18 @@ function main(argv) {
581
687
  };
582
688
  const packs = option('--packs');
583
689
  const tools = option('--tools');
584
- const options = { packs: !packs || packs === 'all' ? undefined : packs.split(',').map(value => value.trim()),
585
- tools: !tools || tools === 'all' ? undefined : tools === 'none' ? [] : tools.split(',').map(value => value.trim()),
586
- userName: option('--user-name'), language: option('--language') };
690
+ const options = {
691
+ packs:
692
+ !packs || packs === 'all' ? undefined : packs.split(',').map((value) => value.trim()),
693
+ tools:
694
+ !tools || tools === 'all'
695
+ ? undefined
696
+ : tools === 'none'
697
+ ? []
698
+ : tools.split(',').map((value) => value.trim()),
699
+ userName: option('--user-name'),
700
+ language: option('--language'),
701
+ };
587
702
  const derived = require('../cli/lib/packs').DERIVED;
588
703
  const files = generateUserFiles(derived, options);
589
704
  if (args.includes('--print')) {
@@ -593,12 +708,21 @@ function main(argv) {
593
708
  if (args.includes('--check')) {
594
709
  const mismatches = files.filter(({ file, content }) => {
595
710
  const target = safeTarget(outDir, file);
596
- return !fs.existsSync(target) || normalizeEol(fs.readFileSync(target, 'utf8')) !== normalizeEol(content);
711
+ return (
712
+ !fs.existsSync(target) ||
713
+ normalizeEol(fs.readFileSync(target, 'utf8')) !== normalizeEol(content)
714
+ );
597
715
  });
598
- if (mismatches.length) console.error('User adapter drift: ' + mismatches.map(entry => entry.file).join(', '));
716
+ if (mismatches.length)
717
+ console.error('User adapter drift: ' + mismatches.map((entry) => entry.file).join(', '));
599
718
  return mismatches.length ? 1 : 0;
600
719
  }
601
- const written = writeUserAdapters({ outDir, derived, ...options, force: args.includes('--force') });
720
+ const written = writeUserAdapters({
721
+ outDir,
722
+ derived,
723
+ ...options,
724
+ force: args.includes('--force'),
725
+ });
602
726
  for (const target of written) console.log('Generated ' + target);
603
727
  return 0;
604
728
  } catch (error) {
@@ -658,7 +782,9 @@ module.exports = {
658
782
  GENERATED_MARKER,
659
783
  USER_CONFIG_MARKER,
660
784
  buildIDEConfigs,
661
- get IDE_CONFIGS() { return buildIDEConfigs(require('../cli/lib/packs').DERIVED); },
785
+ get IDE_CONFIGS() {
786
+ return buildIDEConfigs(require('../cli/lib/packs').DERIVED);
787
+ },
662
788
  renderUserConfig,
663
789
  generateUserFiles,
664
790
  writeUserAdapters,
@@ -58,8 +58,8 @@ function assertResourcePath(file, where) {
58
58
  }
59
59
 
60
60
  /** Installed entry points, derived from the same layout used by pack-copy.js. */
61
- function buildRequiredResources(pack) {
62
- const where = `registry.yaml: pack "${pack.pack_dir}"`;
61
+ function buildRequiredResources(pack, id = pack.pack_dir) {
62
+ const where = `registry.yaml: pack "${id}"`;
63
63
  for (const file of [
64
64
  ...pack.agents,
65
65
  ...(pack.skills || []),
@@ -135,12 +135,15 @@ function validateRegistry(registry) {
135
135
  if (pack.skills !== undefined && !Array.isArray(pack.skills)) {
136
136
  throw new Error(`${where}: "skills" must be a list`);
137
137
  }
138
- if (typeof pack.pack_dir !== 'string' || pack.pack_dir === '') {
139
- throw new Error(`${where}: missing "pack_dir"`);
140
- }
141
138
  if (!INSTALL_LAYOUTS.includes(pack.install_layout)) {
142
139
  throw new Error(`${where}: "install_layout" must be one of ${INSTALL_LAYOUTS.join('|')}`);
143
140
  }
141
+ if (
142
+ (pack.install_layout === 'packaged' || pack.pack_dir !== undefined) &&
143
+ (typeof pack.pack_dir !== 'string' || pack.pack_dir === '')
144
+ ) {
145
+ throw new Error(`${where}: missing "pack_dir"`);
146
+ }
144
147
  if (pack.install_layout === 'packaged' && !Array.isArray((pack.doctor || {}).pack_agents)) {
145
148
  throw new Error(`${where}: packaged layout requires "doctor.pack_agents" list`);
146
149
  }
@@ -157,7 +160,7 @@ function validateRegistry(registry) {
157
160
  ) {
158
161
  throw new Error(`${where}: runtime must declare known runtimes`);
159
162
  }
160
- buildRequiredResources(pack);
163
+ buildRequiredResources(pack, id);
161
164
  if (pack.personas !== undefined) {
162
165
  if (!Array.isArray(pack.personas)) throw new Error(`${where}: personas must be a list`);
163
166
  const roster = new Set([...pack.agents, ...(pack.sub_agents || [])]);
@@ -253,13 +256,16 @@ function buildPacks(registry) {
253
256
  const entry = {
254
257
  name: p.cli.name,
255
258
  icon: p.cli.icon,
256
- agents: [...p.agents],
259
+ // Only loose packs install agent directories; packaged agents live in packDir.
260
+ agents: p.install_layout === 'loose' ? [...p.agents] : [],
257
261
  skills: [...(p.skills || [])],
258
262
  };
259
263
  if ('data' in p) entry.data = [...p.data];
260
264
  if (p.external_package) entry.externalPackage = p.external_package;
261
- entry.packDir = p.pack_dir;
262
- entry.packSrcDir = p.pack_src_dir || 'packs';
265
+ if (p.pack_dir) {
266
+ entry.packDir = p.pack_dir;
267
+ entry.packSrcDir = p.pack_src_dir || 'packs';
268
+ }
263
269
  if (p.required === true) entry.required = true;
264
270
  entry.desc = derived.packs[id].desc;
265
271
  packs[id] = entry;
@@ -313,6 +319,7 @@ function buildDerived(registry, { sourceRoot = path.join(REPO_ROOT, 'src', 'bmad
313
319
  order: pack.order,
314
320
  name: pack.cli.name,
315
321
  displayName: pack.display_name || pack.cli.name,
322
+ iconEmoji: pack.icon_emoji || '',
316
323
  required: pack.required === true,
317
324
  installerAgentCount,
318
325
  categoryAgentCount,
@@ -324,12 +331,14 @@ function buildDerived(registry, { sourceRoot = path.join(REPO_ROOT, 'src', 'bmad
324
331
  frameworkCount: (pack.compliance_tags || []).length,
325
332
  categoryCount: categories.length,
326
333
  skillCount: (pack.skills || []).length,
327
- referenceFiles: countMarkdownFiles(
328
- path.join(sourceRoot, pack.pack_src_dir || 'packs', pack.pack_dir, 'references')
329
- ),
334
+ referenceFiles: pack.pack_dir
335
+ ? countMarkdownFiles(
336
+ path.join(sourceRoot, pack.pack_src_dir || 'packs', pack.pack_dir, 'references')
337
+ )
338
+ : 0,
330
339
  categoryAgentCounts,
331
340
  runtimes: [...pack.runtime],
332
- requiredResources: buildRequiredResources(pack),
341
+ requiredResources: buildRequiredResources(pack, id),
333
342
  personas: (pack.personas || []).map(({ id, name, role, description, alias }) => ({
334
343
  id,
335
344
  name,
@@ -528,6 +537,31 @@ function describeDiff(label, generated, current, mismatches) {
528
537
  }
529
538
  }
530
539
 
540
+ /**
541
+ * Every source path the installer copies from (see pack-copy.js listPackFiles)
542
+ * must exist; the copier tolerates a missing path, so the build must not.
543
+ */
544
+ function checkSourcePaths(packs, packageRoot = REPO_ROOT) {
545
+ const sourceRoot = path.join(packageRoot, 'src', 'bmad-plus');
546
+ const missing = [];
547
+ for (const [id, pack] of Object.entries(packs)) {
548
+ const declared = [
549
+ ...pack.agents.map((name) => path.join(sourceRoot, 'agents', name)),
550
+ ...pack.skills.map((name) => path.join(sourceRoot, 'skills', name)),
551
+ ...(pack.data || []).map((name) => path.join(sourceRoot, 'data', name)),
552
+ ...(pack.externalPackage ? [path.join(packageRoot, pack.externalPackage, 'skills')] : []),
553
+ ...(pack.packDir ? [path.join(sourceRoot, pack.packSrcDir, pack.packDir)] : []),
554
+ ];
555
+ for (const source of declared) {
556
+ if (!fs.existsSync(source)) {
557
+ const relative = path.relative(packageRoot, source).split(path.sep).join('/');
558
+ missing.push(`PACKS.${id}: declared source path does not exist: ${relative}`);
559
+ }
560
+ }
561
+ }
562
+ return missing;
563
+ }
564
+
531
565
  /**
532
566
  * `--check` mode: prove registry.yaml reproduces the live packs.js exactly,
533
567
  * and that product.version matches package.json.
@@ -546,6 +580,7 @@ function check({
546
580
  const mismatches = [];
547
581
 
548
582
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
583
+ mismatches.push(...checkSourcePaths(generated.PACKS));
549
584
  const { checkPythonDelivery } = require('./pack-delivery');
550
585
  mismatches.push(...checkPythonDelivery(registry, { packageRoot: REPO_ROOT, packageJson: pkg }));
551
586
  try {
@@ -642,6 +677,7 @@ module.exports = {
642
677
  generateModuleSource,
643
678
  generatePacksModuleSource,
644
679
  check,
680
+ checkSourcePaths,
645
681
  main,
646
682
  };
647
683
 
@@ -29,10 +29,11 @@ const doctor = require('./commands/doctor');
29
29
  const scan = require('./commands/scan');
30
30
  const memory = require('./commands/memory');
31
31
 
32
+ // `--lang` is declared per command: a program-level copy would swallow it.
32
33
  program
34
+ .name('bmad-plus')
33
35
  .version(packageJson.version)
34
- .description('BMAD+ — Augmented AI-Driven Development Framework')
35
- .option('-l, --lang <code>', 'Language code: en, fr, es, de, pt-br, ru, zh, he, ja, it');
36
+ .description('BMAD+ — Augmented AI-Driven Development Framework');
36
37
 
37
38
  // Install command
38
39
  const installCmd = program
@@ -131,7 +132,3 @@ program.parseAsync(process.argv).catch((error) => {
131
132
  console.error(`BMAD+: ${error.message}`);
132
133
  process.exitCode = 1;
133
134
  });
134
-
135
- if (process.argv.slice(2).length === 0) {
136
- program.outputHelp();
137
- }