bmad-plus 0.22.0 → 0.23.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 (60) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +46 -26
  3. package/SECURITY.md +1 -1
  4. package/osint-agent-package/agents/osint-investigator.md +3 -3
  5. package/osint-agent-package/skills/bmad-osint-investigate/osint/SKILL.md +51 -51
  6. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/content-extraction.md +7 -7
  7. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/platforms.md +2 -2
  8. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/tools.md +31 -41
  9. package/package.json +24 -5
  10. package/readme-international/README.de.md +46 -26
  11. package/readme-international/README.es.md +47 -27
  12. package/readme-international/README.fr.md +46 -26
  13. package/src/bmad-plus/agents/agent-maker/SKILL.md +39 -14
  14. package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +5 -1
  15. package/src/bmad-plus/data/role-triggers.yaml +34 -0
  16. package/src/bmad-plus/module-help.csv +2 -0
  17. package/src/bmad-plus/module.yaml +26 -4
  18. package/src/bmad-plus/packs/pack-animated/animated-website-agent.md +40 -14
  19. package/src/bmad-plus/packs/pack-animated/templates/animated-website-workflow.md +15 -12
  20. package/src/bmad-plus/packs/pack-backup/backup-agent.md +2 -1
  21. package/src/bmad-plus/packs/pack-backup/templates/backup-workflow.md +2 -0
  22. package/src/bmad-plus/packs/pack-design/SKILL.md +72 -0
  23. package/src/bmad-plus/packs/pack-design/bmad-skill-manifest.yaml +13 -0
  24. package/src/bmad-plus/packs/pack-design/design-engine.md +126 -0
  25. package/src/bmad-plus/packs/pack-design/intake.yaml +105 -0
  26. package/src/bmad-plus/packs/pack-memory/zecher-agent.md +1 -1
  27. package/src/bmad-plus/packs/pack-shield/README.md +1 -1
  28. package/src/bmad-plus/packs/pack-shield/SKILL.md +3 -3
  29. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/pci-dss-agent.md +1 -1
  30. package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +2 -2
  31. package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +7 -0
  32. package/src/bmad-plus/skills/bmad-plus-intake/SKILL.md +87 -0
  33. package/src/bmad-plus/skills/bmad-plus-intake/ref/intake-format.md +112 -0
  34. package/src/bmad-plus/skills/bmad-plus-parallel/SKILL.md +18 -0
  35. package/src/bmad-plus/skills/bmad-plus-swarm/SKILL.md +115 -0
  36. package/src/bmad-plus/skills/bmad-plus-swarm/ref/definition.md +188 -0
  37. package/tools/build/adapters.config.js +3 -1
  38. package/tools/build/generate-adapters.js +26 -2
  39. package/tools/build/generate.js +269 -7
  40. package/tools/build/intake-contract.js +522 -0
  41. package/tools/cli/bmad-plus-cli.js +2 -0
  42. package/tools/cli/commands/install.js +45 -11
  43. package/tools/cli/commands/intake.js +124 -0
  44. package/tools/cli/commands/nexus.js +2 -1
  45. package/tools/cli/commands/review.js +1 -1
  46. package/tools/cli/commands/swarm.js +119 -0
  47. package/tools/cli/commands/uninstall.js +17 -0
  48. package/tools/cli/commands/update.js +30 -1
  49. package/tools/cli/i18n.js +50 -30
  50. package/tools/cli/lib/intake.js +202 -0
  51. package/tools/cli/lib/pack-copy.js +95 -5
  52. package/tools/cli/lib/packs.js +102 -12
  53. package/tools/cli/lib/review.js +2 -1
  54. package/tools/cli/lib/swarm-deal.js +116 -0
  55. package/tools/cli/lib/swarm-defaults.js +6 -0
  56. package/tools/cli/lib/swarm-tally.js +195 -0
  57. package/tools/cli/lib/swarm-validate.js +589 -0
  58. package/tools/cli/lib/swarm.js +712 -0
  59. package/tools/cli/lib/update-dispatch.js +10 -2
  60. package/tools/cli/lib/update-transaction.js +9 -6
@@ -26,6 +26,7 @@ const fs = require('node:fs');
26
26
  const path = require('node:path');
27
27
  const { isDeepStrictEqual } = require('node:util');
28
28
  const yaml = require('js-yaml');
29
+ const { validateIntake } = require('./intake-contract');
29
30
 
30
31
  const REPO_ROOT = path.join(__dirname, '..', '..');
31
32
  const DEFAULT_REGISTRY_PATH = path.join(REPO_ROOT, 'registry.yaml');
@@ -39,6 +40,10 @@ const OWNED_COUNT_RE =
39
40
  /(?<![\w.-])\d+\+?[\s-]+(?:[A-Za-zÀ-ÿ-]+\s+){0,6}(?:agents?|skills?|workflows?|packs?|frameworks?|languages?|reference files)\b/gi;
40
41
  const STANDARD_PREFIX_RE =
41
42
  /\b(?:ISO(?:\/IEC)?|IEC|SOC|NIST(?: SP)?|PCI(?: DSS)?|WCAG|Section)\s*$/i;
43
+ const SKILL_ID_RE = /^[a-z][a-z0-9-]*$/;
44
+ const PACKAGE_NAME_RE = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
45
+ const EXACT_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
46
+ const PIPELINE_GATES = ['require_approval', 'notify_only', 'auto'];
42
47
 
43
48
  function hasOwnedCount(value) {
44
49
  return [...value.matchAll(OWNED_COUNT_RE)].some(
@@ -60,13 +65,16 @@ function assertResourcePath(file, where) {
60
65
  /** Installed entry points, derived from the same layout used by pack-copy.js. */
61
66
  function buildRequiredResources(pack, id = pack.pack_dir) {
62
67
  const where = `registry.yaml: pack "${id}"`;
68
+ const externalSkills = (pack.external_module || {}).skills || [];
63
69
  for (const file of [
64
70
  ...pack.agents,
65
71
  ...(pack.skills || []),
72
+ ...externalSkills,
66
73
  ...(pack.data || []),
67
74
  ...((pack.doctor || {}).pack_agents || []),
68
75
  ...((pack.doctor || {}).required_resources || []),
69
76
  ...(pack.python_entry ? [pack.python_entry] : []),
77
+ ...(pack.intake ? [pack.intake] : []),
70
78
  ]) {
71
79
  assertResourcePath(file, where);
72
80
  }
@@ -79,6 +87,7 @@ function buildRequiredResources(pack, id = pack.pack_dir) {
79
87
  );
80
88
  }
81
89
  resources.push(...(pack.skills || []).map((id) => `.agents/skills/${id}/SKILL.md`));
90
+ resources.push(...externalSkills.map((id) => `.agents/skills/${id}/SKILL.md`));
82
91
  resources.push(...(pack.data || []).map((file) => `.agents/data/${file}`));
83
92
  resources.push(
84
93
  ...((pack.doctor || {}).required_resources || []).map(
@@ -86,10 +95,137 @@ function buildRequiredResources(pack, id = pack.pack_dir) {
86
95
  )
87
96
  );
88
97
  if (pack.python_entry) resources.push(`.agents/skills/${pack.pack_dir}/${pack.python_entry}`);
98
+ if (pack.intake) resources.push(`.agents/skills/${pack.pack_dir}/${pack.intake}`);
89
99
  for (const file of resources) assertResourcePath(file, where);
90
100
  return [...new Set(resources)];
91
101
  }
92
102
 
103
+ function assertSkillIds(list, where) {
104
+ if (
105
+ !Array.isArray(list) ||
106
+ list.length === 0 ||
107
+ list.some((id) => typeof id !== 'string' || !SKILL_ID_RE.test(id)) ||
108
+ new Set(list).size !== list.length
109
+ ) {
110
+ throw new Error(`${where}: must be a non-empty list of distinct skill ids`);
111
+ }
112
+ }
113
+
114
+ /** Structural contract of `external_module` (§5.2); the package itself is checked by --check. */
115
+ function validateExternalModule(pack, where) {
116
+ const module = pack.external_module;
117
+ if (module === undefined) return;
118
+ if (
119
+ !module ||
120
+ typeof module !== 'object' ||
121
+ Object.keys(module).some((key) => !['package', 'version', 'skills'].includes(key))
122
+ ) {
123
+ throw new Error(`${where}: external_module takes exactly package, version and skills`);
124
+ }
125
+ if (typeof module.package !== 'string' || !PACKAGE_NAME_RE.test(module.package)) {
126
+ throw new Error(`${where}: external_module.package must be an npm package name`);
127
+ }
128
+ if (typeof module.version !== 'string' || !EXACT_VERSION_RE.test(module.version)) {
129
+ throw new Error(`${where}: external_module.version must be an exact version`);
130
+ }
131
+ assertSkillIds(module.skills, `${where}: external_module.skills`);
132
+ for (const id of module.skills) {
133
+ if ((pack.skills || []).includes(id)) {
134
+ throw new Error(`${where}: skill ${id} is both shipped in-tree and taken from the module`);
135
+ }
136
+ }
137
+ }
138
+
139
+ /** Structural contract of `pipelines` (§5.3): every step names one real thing of the pack. */
140
+ function validatePipelines(pack, where) {
141
+ if (pack.pipelines === undefined) return;
142
+ if (!Array.isArray(pack.pipelines) || pack.pipelines.length === 0) {
143
+ throw new Error(`${where}: pipelines must be a non-empty list`);
144
+ }
145
+ const workflows = new Set([
146
+ ...(pack.workflows || []),
147
+ ...(pack.categories || []).flatMap((category) => category.workflows || []),
148
+ ]);
149
+ const agents = new Set([...pack.agents, ...(pack.sub_agents || [])]);
150
+ const ids = new Set();
151
+ for (const pipeline of pack.pipelines) {
152
+ if (
153
+ !pipeline ||
154
+ typeof pipeline !== 'object' ||
155
+ Object.keys(pipeline).some((key) => !['id', 'title', 'steps'].includes(key))
156
+ ) {
157
+ throw new Error(`${where}: each pipeline takes exactly id, title and steps`);
158
+ }
159
+ if (typeof pipeline.id !== 'string' || !SKILL_ID_RE.test(pipeline.id) || ids.has(pipeline.id)) {
160
+ throw new Error(`${where}: pipeline ids must be distinct lowercase slugs`);
161
+ }
162
+ ids.add(pipeline.id);
163
+ const label = `${where}: pipeline ${pipeline.id}`;
164
+ if (typeof pipeline.title !== 'string' || !pipeline.title.trim()) {
165
+ throw new Error(`${label}: needs a title`);
166
+ }
167
+ if (!Array.isArray(pipeline.steps) || pipeline.steps.length === 0) {
168
+ throw new Error(`${label}: needs at least one step`);
169
+ }
170
+ pipeline.steps.forEach((step, index) => {
171
+ const at = `${label}: step ${index + 1}`;
172
+ if (
173
+ !step ||
174
+ typeof step !== 'object' ||
175
+ Object.keys(step).some((key) => !['workflow', 'agent', 'gate'].includes(key))
176
+ ) {
177
+ throw new Error(`${at}: takes workflow or agent, and gate`);
178
+ }
179
+ if ((step.workflow === undefined) === (step.agent === undefined)) {
180
+ throw new Error(`${at}: names exactly one of workflow or agent`);
181
+ }
182
+ if (step.workflow !== undefined && !workflows.has(step.workflow)) {
183
+ throw new Error(`${at}: workflow ${step.workflow} is not declared by the pack`);
184
+ }
185
+ if (step.agent !== undefined && !agents.has(step.agent)) {
186
+ throw new Error(`${at}: agent ${step.agent} is not declared by the pack`);
187
+ }
188
+ if (!PIPELINE_GATES.includes(step.gate)) {
189
+ throw new Error(`${at}: gate must be one of ${PIPELINE_GATES.join('|')}`);
190
+ }
191
+ });
192
+ }
193
+ }
194
+
195
+ /** `intake` names a file inside the pack directory (§5.4); its content is checked by --check. */
196
+ function validateIntakeField(pack, where) {
197
+ if (pack.intake === undefined) return;
198
+ if (typeof pack.intake !== 'string' || !pack.intake) {
199
+ throw new Error(`${where}: intake must be a file path relative to pack_dir`);
200
+ }
201
+ if (!pack.pack_dir) throw new Error(`${where}: intake needs a pack_dir to live in`);
202
+ }
203
+
204
+ /** Every `uses_skills` id (§5.1) is a skill some pack ships in-tree or through its module. */
205
+ function validateUsesSkills(registry) {
206
+ const known = new Set();
207
+ for (const pack of Object.values(registry.packs)) {
208
+ for (const id of pack.skills || []) known.add(id);
209
+ for (const id of (pack.external_module || {}).skills || []) known.add(id);
210
+ }
211
+ const resolve = (list, where) => {
212
+ if (list === undefined) return;
213
+ assertSkillIds(list, where);
214
+ for (const id of list) {
215
+ if (!known.has(id)) throw new Error(`${where}: skill ${id} is shipped by no pack`);
216
+ }
217
+ };
218
+ for (const [id, pack] of Object.entries(registry.packs)) {
219
+ resolve(pack.uses_skills, `registry.yaml: pack "${id}": uses_skills`);
220
+ for (const persona of pack.personas || []) {
221
+ resolve(
222
+ persona.uses_skills,
223
+ `registry.yaml: pack "${id}": persona ${persona.id}: uses_skills`
224
+ );
225
+ }
226
+ }
227
+ }
228
+
93
229
  /**
94
230
  * Validate the minimal contract the generator relies on.
95
231
  * Throws with a precise message on the first violation.
@@ -160,6 +296,15 @@ function validateRegistry(registry) {
160
296
  ) {
161
297
  throw new Error(`${where}: runtime must declare known runtimes`);
162
298
  }
299
+ validateExternalModule(pack, where);
300
+ validatePipelines(pack, where);
301
+ validateIntakeField(pack, where);
302
+ if (
303
+ pack.cohabitation_warning !== undefined &&
304
+ (typeof pack.cohabitation_warning !== 'string' || !pack.cohabitation_warning.trim())
305
+ ) {
306
+ throw new Error(`${where}: cohabitation_warning must be a non-empty sentence`);
307
+ }
163
308
  buildRequiredResources(pack, id);
164
309
  if (pack.personas !== undefined) {
165
310
  if (!Array.isArray(pack.personas)) throw new Error(`${where}: personas must be a list`);
@@ -183,6 +328,7 @@ function validateRegistry(registry) {
183
328
  }
184
329
  }
185
330
  }
331
+ validateUsesSkills(registry);
186
332
  const integration = registry.targets?.integration;
187
333
  if (
188
334
  integration?.execution !== 'host-managed' ||
@@ -242,6 +388,21 @@ function buildPackOrder(registry) {
242
388
  return sortedPackEntries(registry).map(([id]) => id);
243
389
  }
244
390
 
391
+ function externalModuleFacts({ package: name, version, skills }) {
392
+ return { package: name, version, skills: [...skills] };
393
+ }
394
+
395
+ function pipelineFacts(pipelines) {
396
+ return pipelines.map(({ id, title, steps }) => ({
397
+ id,
398
+ title,
399
+ steps: steps.map((step) => ({
400
+ ...(step.workflow !== undefined ? { workflow: step.workflow } : { agent: step.agent }),
401
+ gate: step.gate,
402
+ })),
403
+ }));
404
+ }
405
+
245
406
  /**
246
407
  * Generate the PACKS object with the exact shape of tools/cli/lib/packs.js.
247
408
  * Key-presence rules mirror the hand-written module:
@@ -262,10 +423,14 @@ function buildPacks(registry) {
262
423
  };
263
424
  if ('data' in p) entry.data = [...p.data];
264
425
  if (p.external_package) entry.externalPackage = p.external_package;
426
+ if (p.external_module) entry.externalModule = externalModuleFacts(p.external_module);
265
427
  if (p.pack_dir) {
266
428
  entry.packDir = p.pack_dir;
267
429
  entry.packSrcDir = p.pack_src_dir || 'packs';
268
430
  }
431
+ if (p.uses_skills) entry.usesSkills = [...p.uses_skills];
432
+ if (p.pipelines) entry.pipelines = pipelineFacts(p.pipelines);
433
+ if (p.intake) entry.intake = p.intake;
269
434
  if (p.required === true) entry.required = true;
270
435
  entry.desc = derived.packs[id].desc;
271
436
  packs[id] = entry;
@@ -352,13 +517,23 @@ function buildDerived(registry, { sourceRoot = path.join(REPO_ROOT, 'src', 'bmad
352
517
  categoryAgentCounts,
353
518
  runtimes: [...pack.runtime],
354
519
  requiredResources: buildRequiredResources(pack, id),
355
- personas: (pack.personas || []).map(({ id, name, role, description, alias }) => ({
356
- id,
357
- name,
358
- role,
359
- description,
360
- ...(alias ? { alias } : {}),
361
- })),
520
+ personas: (pack.personas || []).map(
521
+ ({ id, name, role, description, alias, uses_skills }) => ({
522
+ id,
523
+ name,
524
+ role,
525
+ description,
526
+ ...(alias ? { alias } : {}),
527
+ ...(uses_skills ? { usesSkills: [...uses_skills] } : {}),
528
+ })
529
+ ),
530
+ ...(pack.uses_skills ? { usesSkills: [...pack.uses_skills] } : {}),
531
+ ...(pack.external_module
532
+ ? { externalModule: externalModuleFacts(pack.external_module) }
533
+ : {}),
534
+ ...(pack.pipelines ? { pipelines: pipelineFacts(pack.pipelines) } : {}),
535
+ ...(pack.intake ? { intake: pack.intake } : {}),
536
+ ...(pack.cohabitation_warning ? { cohabitationWarning: pack.cohabitation_warning } : {}),
362
537
  };
363
538
  facts.summary = renderSummary(pack, facts);
364
539
  facts.desc = renderSummary(pack, facts, 'description');
@@ -576,6 +751,88 @@ function checkSourcePaths(packs, packageRoot = REPO_ROOT) {
576
751
  return missing;
577
752
  }
578
753
 
754
+ function resolvePackageManifest(name, packageRoot) {
755
+ return require.resolve(name + '/package.json', { paths: [packageRoot] });
756
+ }
757
+
758
+ /**
759
+ * Every `external_module` is an exact pinned dependency, resolvable from the
760
+ * repository root at the declared version, and ships each listed skill.
761
+ */
762
+ function checkExternalModules(
763
+ registry,
764
+ { packageRoot = REPO_ROOT, packageJson, resolve = resolvePackageManifest } = {}
765
+ ) {
766
+ const problems = [];
767
+ const dependencies = (packageJson && packageJson.dependencies) || {};
768
+ const declared = new Set((registry.data_handling || {}).runtime_dependencies || []);
769
+ for (const [id, pack] of Object.entries(registry.packs)) {
770
+ const module = pack.external_module;
771
+ if (!module) continue;
772
+ const where = `PACKS.${id}: external_module ${module.package}`;
773
+ if (dependencies[module.package] !== module.version) {
774
+ problems.push(
775
+ `${where}: package.json must pin dependencies["${module.package}"] to exactly "${module.version}" (found ${JSON.stringify(dependencies[module.package])})`
776
+ );
777
+ }
778
+ if (!declared.has(module.package)) {
779
+ problems.push(`${where}: missing from data_handling.runtime_dependencies`);
780
+ }
781
+ let manifestPath;
782
+ try {
783
+ manifestPath = resolve(module.package, packageRoot);
784
+ } catch (error) {
785
+ problems.push(`${where}: cannot be resolved from the repository root (${error.message})`);
786
+ continue;
787
+ }
788
+ let manifest;
789
+ try {
790
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
791
+ } catch (error) {
792
+ problems.push(`${where}: unreadable package.json (${error.message})`);
793
+ continue;
794
+ }
795
+ if (!manifest || manifest.name !== module.package || manifest.version !== module.version) {
796
+ problems.push(
797
+ `${where}: installed ${manifest && manifest.name}@${manifest && manifest.version}, declared ${module.version}`
798
+ );
799
+ }
800
+ for (const skill of module.skills) {
801
+ const file = path.join(path.dirname(manifestPath), 'skills', skill, 'SKILL.md');
802
+ if (!fs.existsSync(file)) problems.push(`${where}: skills/${skill}/SKILL.md is missing`);
803
+ }
804
+ }
805
+ return problems;
806
+ }
807
+
808
+ /** Every declared intake file exists in its pack directory and passes the contract. */
809
+ function checkIntakes(registry, { packageRoot = REPO_ROOT } = {}) {
810
+ const problems = [];
811
+ for (const [id, pack] of Object.entries(registry.packs)) {
812
+ if (!pack.intake) continue;
813
+ const relative = `src/bmad-plus/${pack.pack_src_dir || 'packs'}/${pack.pack_dir}/${pack.intake}`;
814
+ const file = path.join(packageRoot, ...relative.split('/'));
815
+ const where = `PACKS.${id}: intake ${relative}`;
816
+ if (!fs.existsSync(file)) {
817
+ problems.push(`${where}: file does not exist`);
818
+ continue;
819
+ }
820
+ let parsed;
821
+ try {
822
+ parsed = yaml.load(fs.readFileSync(file, 'utf8'), { schema: yaml.JSON_SCHEMA });
823
+ } catch (error) {
824
+ problems.push(`${where}: ${error.message}`);
825
+ continue;
826
+ }
827
+ const result = validateIntake(parsed);
828
+ for (const problem of result.problems) problems.push(`${where}: ${problem}`);
829
+ if (result.ok && parsed.pack !== id) {
830
+ problems.push(`${where}: declares pack "${parsed.pack}" instead of "${id}"`);
831
+ }
832
+ }
833
+ return problems;
834
+ }
835
+
579
836
  /**
580
837
  * `--check` mode: prove registry.yaml reproduces the live packs.js exactly,
581
838
  * and that product.version matches package.json.
@@ -595,6 +852,8 @@ function check({
595
852
 
596
853
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
597
854
  mismatches.push(...checkSourcePaths(generated.PACKS));
855
+ mismatches.push(...checkExternalModules(registry, { packageJson: pkg }));
856
+ mismatches.push(...checkIntakes(registry));
598
857
  const { checkPythonDelivery } = require('./pack-delivery');
599
858
  mismatches.push(...checkPythonDelivery(registry, { packageRoot: REPO_ROOT, packageJson: pkg }));
600
859
  try {
@@ -680,6 +939,7 @@ module.exports = {
680
939
  DEFAULT_PACKS_MODULE_PATH,
681
940
  DEFAULT_MODULE_PATH,
682
941
  MODULE_TEMPLATE_PATH,
942
+ PACKAGE_NAME_RE,
683
943
  loadRegistry,
684
944
  validateRegistry,
685
945
  buildPacks,
@@ -692,6 +952,8 @@ module.exports = {
692
952
  generatePacksModuleSource,
693
953
  check,
694
954
  checkSourcePaths,
955
+ checkExternalModules,
956
+ checkIntakes,
695
957
  main,
696
958
  };
697
959