bmad-method 6.11.1-next.4 → 6.11.1-next.6

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 (39) hide show
  1. package/package.json +2 -2
  2. package/src/bmm-skills/plan/bmad-generate-project-context/SKILL.md +2 -0
  3. package/src/bmm-skills/plan/bmad-sprint-planning/scripts/__pycache__/sprint_plan.cpython-311.pyc +0 -0
  4. package/src/bmm-skills/plan/bmad-sprint-planning/scripts/tests/__pycache__/test_sprint_plan.cpython-311-pytest-9.1.1.pyc +0 -0
  5. package/src/bmm-skills/ship/bmad-retrospective/scripts/__pycache__/sprint_status.cpython-311.pyc +0 -0
  6. package/src/bmm-skills/ship/bmad-retrospective/scripts/tests/__pycache__/test_git_evidence.cpython-311-pytest-9.1.1.pyc +0 -0
  7. package/src/bmm-skills/ship/bmad-retrospective/scripts/tests/__pycache__/test_sprint_status.cpython-311-pytest-9.1.1.pyc +0 -0
  8. package/src/bmm-skills/v6-shims/bmad-create-architecture/SKILL.md +2 -0
  9. package/src/bmm-skills/v6-shims/bmad-create-prd/SKILL.md +2 -0
  10. package/src/bmm-skills/v6-shims/bmad-create-story/SKILL.md +2 -0
  11. package/src/bmm-skills/v6-shims/bmad-dev-auto/SKILL.md +2 -0
  12. package/src/bmm-skills/v6-shims/bmad-dev-story/SKILL.md +2 -0
  13. package/src/bmm-skills/v6-shims/bmad-document-project/SKILL.md +2 -0
  14. package/src/bmm-skills/v6-shims/bmad-domain-research/SKILL.md +2 -0
  15. package/src/bmm-skills/v6-shims/bmad-edit-prd/SKILL.md +2 -0
  16. package/src/bmm-skills/v6-shims/bmad-market-research/SKILL.md +2 -0
  17. package/src/bmm-skills/v6-shims/bmad-quick-dev/SKILL.md +2 -0
  18. package/src/bmm-skills/v6-shims/bmad-sprint-status/SKILL.md +2 -0
  19. package/src/bmm-skills/v6-shims/bmad-technical-research/SKILL.md +2 -0
  20. package/src/bmm-skills/v6-shims/bmad-validate-prd/SKILL.md +2 -0
  21. package/src/core-skills/bmad-review/SKILL.md +1 -1
  22. package/src/core-skills/v6-shims/bmad-editorial-review/SKILL.md +2 -0
  23. package/src/core-skills/v6-shims/bmad-editorial-review-prose/SKILL.md +2 -0
  24. package/src/core-skills/v6-shims/bmad-editorial-review-structure/SKILL.md +2 -0
  25. package/src/core-skills/v6-shims/bmad-review-adversarial-general/SKILL.md +2 -0
  26. package/src/core-skills/v6-shims/bmad-review-edge-case-hunter/SKILL.md +2 -0
  27. package/src/core-skills/v6-shims/bmad-review-verification-gap/SKILL.md +2 -0
  28. package/src/scripts/__pycache__/config_utils.cpython-311.pyc +0 -0
  29. package/src/scripts/tests/__pycache__/test_config_utils.cpython-311.pyc +0 -0
  30. package/src/scripts/tests/__pycache__/test_resolve_config.cpython-311.pyc +0 -0
  31. package/src/scripts/tests/__pycache__/test_resolve_customization.cpython-311.pyc +0 -0
  32. package/tools/installer/commands/install.js +2 -0
  33. package/tools/installer/core/config.js +3 -0
  34. package/tools/installer/core/installer.js +25 -1
  35. package/tools/installer/core/manifest-generator.js +6 -0
  36. package/tools/installer/core/manifest.js +6 -0
  37. package/tools/installer/core/shim-policy.js +91 -0
  38. package/tools/installer/modules/official-modules.js +38 -6
  39. package/tools/installer/ui.js +46 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method",
4
- "version": "6.11.1-next.4",
4
+ "version": "6.11.1-next.6",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
@@ -44,7 +44,7 @@
44
44
  "rebundle": "node tools/installer/bundlers/bundle-web.js rebundle",
45
45
  "test": "npm run test:refs && npm run test:install && npm run test:urls && npm run test:site-url && npm run test:channels && npm run test:renderer && npm run test:retrospective && npm run test:sprint-planning && npm run test:skills && npm run lint && npm run lint:md && npm run format:check",
46
46
  "test:channels": "node test/test-installer-channels.js",
47
- "test:install": "node test/test-installation-components.js",
47
+ "test:install": "node test/test-installation-components.js && node test/test-shim-policy.js",
48
48
  "test:refs": "node test/test-file-refs-csv.js",
49
49
  "test:renderer": "uv run --python 3.11 python -m unittest src/scripts/tests/test_config_utils.py src/scripts/tests/test_resolve_config.py src/scripts/tests/test_resolve_customization.py && node test/test-build-auto-renderer.js",
50
50
  "test:retrospective": "uv run --python 3.11 src/bmm-skills/ship/bmad-retrospective/scripts/tests/test_git_evidence.py && uv run --python 3.11 src/bmm-skills/ship/bmad-retrospective/scripts/tests/test_sprint_status.py",
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-generate-project-context
3
3
  description: 'Deprecated — forwards to bmad-project-context. Use when the user says "generate project context" or "create project context"'
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  # DEPRECATED — forwards to bmad-project-context
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-create-architecture
3
3
  description: 'Deprecated — forwards to bmad-architecture (create intent)'
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  # DEPRECATED — forwards to bmad-architecture (create intent)
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-create-prd
3
3
  description: 'Deprecated — forwards to bmad-prd (create intent)'
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  # DEPRECATED — forwards to bmad-prd (create intent)
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-create-story
3
3
  description: "Deprecated: `bmad-build` is now the official implementation method. Only use this when explicitly invoked by name"
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  # Create Story Workflow
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-dev-auto
3
3
  description: "Deprecated: forwards to bmad-build-auto. Do not use unless invoked by name"
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  # Deprecated Build Auto Alias
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-dev-story
3
3
  description: "Deprecated: `bmad-build` is now the official implementation method. Only use this when explicitly invoked by name"
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  # Dev Story Workflow
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-document-project
3
3
  description: 'Deprecated — forwards to bmad-project-context. Use when the user says "document this project" or "generate project docs"'
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  # DEPRECATED — forwards to bmad-project-context
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-domain-research
3
3
  description: 'Deprecated — forwards to bmad-deep-recon (domain type)'
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  # DEPRECATED — forwards to bmad-deep-recon (domain type)
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-edit-prd
3
3
  description: 'Deprecated — forwards to bmad-prd (update intent)'
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  # DEPRECATED — forwards to bmad-prd (update intent)
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-market-research
3
3
  description: 'Deprecated — forwards to bmad-deep-recon (market type)'
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  # DEPRECATED — forwards to bmad-deep-recon (market type)
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-quick-dev
3
3
  description: "Deprecated: forwards to bmad-build. Do not use unless invoked by name"
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  # Deprecated Build Alias
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-sprint-status
3
3
  description: 'Deprecated — forwards to bmad-sprint-planning (status view)'
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  # DEPRECATED — forwards to bmad-sprint-planning (status view)
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-technical-research
3
3
  description: 'Deprecated — forwards to bmad-deep-recon (technical type)'
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  # DEPRECATED — forwards to bmad-deep-recon (technical type)
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-validate-prd
3
3
  description: 'Deprecated — forwards to bmad-prd (validate intent)'
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  # DEPRECATED — forwards to bmad-prd (validate intent)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: bmad-review
3
- description: 'Review any diff, document, spec, or other artifact with one or more installed review methods (lenses)shipped ones cover adversarial critique, edge cases, verification gaps, document structure, and prose. Runs whichever fit the content, or exactly the ones asked for. Use when the user says "review this", "critical review", "editorial review", "hunt edge cases", "review the structure", or "review the prose"'
3
+ description: 'Runs one or more installed review lenses — adversarial critique, edge cases, verification gaps, structure, prose — and reports triaged findings. Use when, and only when, the user asks you to review a diff, a pull request, or an artifact code or documents, one or many — and actually says "review"; an explicit skill:bmad-review directive from another skill counts as that ask. A request to act on feedback from an earlier review is a change, not a review. Never invoke this uninvited, including on edits you just made.'
4
4
  ---
5
5
 
6
6
  # BMad Review
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-editorial-review
3
3
  description: 'Deprecated — forwards to bmad-review'
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  Merged into `bmad-review`. Invoke the `bmad-review` skill on the same content with the `structure` and `prose` lenses — both, structure first, so prose runs on top of the structure findings — unless the caller asked for a structure-only or prose-only review, in which case pass only that lens. Pass through any `also_consider` areas, and forward this skill's resolved `[workflow]` fields as pre-resolved values — but only those that resolved to something, since an empty value here means no legacy override exists and bmad-review's own default should stand: `reader_type`, `style_guide`, `review_guidance`, `output_preferences`, `persistent_facts`, `activation_steps_prepend`, `activation_steps_append`, `on_complete`, and `review_output_path` as the report path. Present the findings in the legacy shape: the two-pass findings table `| Pass | Original Text | Revised Text | Changes |` with the purpose/audience read above it and, when the structure pass ran, the reduction summary below it — and no other lens's output.
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-editorial-review-prose
3
3
  description: 'Deprecated — forwards to bmad-review'
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  Merged into `bmad-review`. Invoke the `bmad-review` skill on the same content with only the `prose` lens, passing through the same inputs and any `also_consider` areas. Present the findings in the legacy shape: a three-column markdown table `| Original Text | Revised Text | Changes |` — no Pass column, no preamble above the table. If no issues are found, output exactly: `No editorial issues identified`.
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-editorial-review-structure
3
3
  description: 'Deprecated — forwards to bmad-review'
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  Merged into `bmad-review`. Invoke the `bmad-review` skill on the same content with only the `structure` lens, passing through the same inputs and any `also_consider` areas. Present the findings in the legacy report shape: a `## Document Summary` block (purpose, audience, reader type, structure model, current length), a `## Recommendations` list of numbered `[CUT/MERGE/MOVE/CONDENSE/QUESTION/PRESERVE]` entries each with rationale and word impact, and a closing `## Summary` (total recommendations, estimated reduction) — not the findings table. If no structural issues are found, output exactly: `No substantive changes recommended`.
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-review-adversarial-general
3
3
  description: 'Deprecated — forwards to bmad-review'
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  Merged into `bmad-review`. Invoke the `bmad-review` skill on the same content with only the `adversarial` lens, passing through any `also_consider` areas. Present the findings as a Markdown list — descriptions only, no severity, priority, or ranking; no JSON block.
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-review-edge-case-hunter
3
3
  description: 'Deprecated — forwards to bmad-review'
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  Merged into `bmad-review`. Invoke the `bmad-review` skill on the same content with only the `edge-case-hunter` lens, passing through any `also_consider` areas. Output ONLY the raw findings JSON array in the legacy shape: the four standard fields (plus `kind`/`confidence` on deletion findings), no `lens` field, no markdown wrapping, no extra text. `[]` is valid when nothing is found.
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: bmad-review-verification-gap
3
3
  description: 'Deprecated — forwards to bmad-review'
4
+ metadata:
5
+ lifecycle: shim
4
6
  ---
5
7
 
6
8
  Merged into `bmad-review`. Invoke the `bmad-review` skill on the same content with only the `verification-gap` lens. Present the markdown rendering only (no JSON block), listing any `gap_shape: "other"` findings under an `## Other findings` heading. When there are no findings at all, output exactly this single line: `No verification gaps found.`
@@ -34,6 +34,8 @@ module.exports = {
34
34
  ['--document-output-language <lang>', 'Language for document output (default: English)'],
35
35
  ['--output-folder <path>', 'Output folder path relative to project root (default: _bmad-output)'],
36
36
  ['--custom-source <sources>', 'Comma-separated Git URLs or local paths to install custom modules from'],
37
+ ['--shims', 'Install deprecated compatibility shim skills when the selected modules provide them'],
38
+ ['--no-shims', 'Do not install deprecated compatibility shim skills'],
37
39
  ['-y, --yes', 'Accept all defaults and skip prompts where possible'],
38
40
  [
39
41
  '--channel <channel>',
@@ -15,6 +15,7 @@ class Config {
15
15
  quickUpdate,
16
16
  channelOptions,
17
17
  setOverrides,
18
+ installShims,
18
19
  }) {
19
20
  this.directory = directory;
20
21
  this.modules = Object.freeze([...modules]);
@@ -32,6 +33,7 @@ class Config {
32
33
  // Intentionally NOT integrated with the prompt/template/schema flow; see
33
34
  // `tools/installer/set-overrides.js` for the rationale and tradeoffs.
34
35
  this.setOverrides = setOverrides || {};
36
+ this.installShims = typeof installShims === 'boolean' ? installShims : undefined;
35
37
  Object.freeze(this);
36
38
  }
37
39
 
@@ -58,6 +60,7 @@ class Config {
58
60
  quickUpdate: userInput._quickUpdate || false,
59
61
  channelOptions: userInput.channelOptions || null,
60
62
  setOverrides: userInput.setOverrides || {},
63
+ installShims: userInput.installShims,
61
64
  });
62
65
  }
63
66
 
@@ -13,6 +13,7 @@ const { InstallPaths } = require('./install-paths');
13
13
  const { ExternalModuleManager } = require('../modules/external-manager');
14
14
  const { resolveModuleVersion } = require('../modules/version-resolver');
15
15
  const { MODULE_HELP_CSV_HEADER } = require('../modules/module-help-schema');
16
+ const { inferShimPreference, readInstalledSkillIds } = require('./shim-policy');
16
17
 
17
18
  const { ExistingInstall } = require('./existing-install');
18
19
  const { warnPreNativeSkillsLegacy } = require('./legacy-warnings');
@@ -42,6 +43,21 @@ class Installer {
42
43
  const paths = await InstallPaths.create(config);
43
44
  const officialModules = await OfficialModules.build(config, paths);
44
45
  const existingInstall = await ExistingInstall.detect(paths.bmadDir);
46
+ const availableShims = await officialModules.discoverShims(config.modules, {
47
+ channelOptions: config.channelOptions,
48
+ });
49
+ const previousManifest = existingInstall.installed ? await this.manifest.read(paths.bmadDir) : null;
50
+ const installedSkillIds = existingInstall.installed ? await readInstalledSkillIds(paths.bmadDir) : new Set();
51
+ const shimPolicy = {
52
+ available: availableShims.length > 0,
53
+ install: inferShimPreference({
54
+ requested: config.installShims,
55
+ persisted: previousManifest?.installShims,
56
+ availableShims,
57
+ installedSkillIds,
58
+ existing: existingInstall.installed,
59
+ }),
60
+ };
45
61
 
46
62
  try {
47
63
  await warnPreNativeSkillsLegacy({
@@ -92,6 +108,7 @@ class Installer {
92
108
  addResult,
93
109
  officialModules,
94
110
  previousSkillManifestRows,
111
+ shimPolicy,
95
112
  );
96
113
 
97
114
  await this._setupIdes(config, allModules, paths, addResult, previousSkillIds);
@@ -225,9 +242,11 @@ class Installer {
225
242
  addResult,
226
243
  officialModules,
227
244
  previousSkillManifestRows = [],
245
+ shimPolicy = null,
228
246
  ) {
229
247
  const isQuickUpdate = config.isQuickUpdate();
230
248
  const moduleConfigs = officialModules.moduleConfigs;
249
+ const resolvedShimPolicy = shimPolicy || { available: false, install: false };
231
250
 
232
251
  const dirResults = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
233
252
 
@@ -251,6 +270,7 @@ class Installer {
251
270
  await this._installOfficialModules(config, paths, officialModuleIds, addResult, isQuickUpdate, officialModules, {
252
271
  message,
253
272
  installedModuleNames,
273
+ shimPolicy: resolvedShimPolicy,
254
274
  });
255
275
 
256
276
  return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
@@ -325,6 +345,8 @@ class Installer {
325
345
  ides: config.ides || [],
326
346
  preservedModules: modulesForCsvPreserve,
327
347
  moduleConfigs,
348
+ installShims: resolvedShimPolicy.install,
349
+ shimsAvailable: resolvedShimPolicy.available,
328
350
  });
329
351
  await this._appendPreservedSkillManifestRows(paths.bmadDir, previousSkillManifestRows, preservedModules);
330
352
 
@@ -727,7 +749,7 @@ class Installer {
727
749
  * @param {Object} ctx - Shared context: { message, installedModuleNames }
728
750
  */
729
751
  async _installOfficialModules(config, paths, officialModuleIds, addResult, isQuickUpdate, officialModules, ctx) {
730
- const { message, installedModuleNames } = ctx;
752
+ const { message, installedModuleNames, shimPolicy } = ctx;
731
753
  const { CustomModuleManager } = require('../modules/custom-module-manager');
732
754
 
733
755
  for (const moduleName of officialModuleIds) {
@@ -749,6 +771,7 @@ class Installer {
749
771
  installer: this,
750
772
  silent: true,
751
773
  channelOptions: config.channelOptions,
774
+ installShims: shimPolicy.install,
752
775
  },
753
776
  );
754
777
 
@@ -1500,6 +1523,7 @@ class Installer {
1500
1523
  // (`applySetOverrides`) runs at the end of quick-update too. The
1501
1524
  // installer.install path applies them after writeCentralConfig.
1502
1525
  setOverrides: config.setOverrides || {},
1526
+ installShims: config.installShims,
1503
1527
  actionType: 'install',
1504
1528
  _quickUpdate: true,
1505
1529
  _preserveModules: skippedModules,
@@ -58,6 +58,8 @@ class ManifestGenerator {
58
58
  this.bmadDir = bmadDir;
59
59
  this.bmadFolderName = path.basename(bmadDir); // Get the actual folder name (e.g., '_bmad' or 'bmad')
60
60
  this.allInstalledFiles = installedFiles;
61
+ this.installShims = options.installShims === true;
62
+ this.shimsAvailable = options.shimsAvailable === true;
61
63
 
62
64
  if (!Object.prototype.hasOwnProperty.call(options, 'ides')) {
63
65
  throw new Error('ManifestGenerator requires `options.ides` to be provided – installer should supply the selected IDEs array.');
@@ -378,6 +380,10 @@ class ManifestGenerator {
378
380
  ides: this.selectedIdes,
379
381
  };
380
382
 
383
+ if (this.shimsAvailable) {
384
+ manifest.installation.installShims = this.installShims;
385
+ }
386
+
381
387
  // Clean the manifest to remove any non-serializable values
382
388
  const cleanManifest = structuredClone(manifest);
383
389
 
@@ -61,6 +61,10 @@ class Manifest {
61
61
  ides: data.ides || [],
62
62
  };
63
63
 
64
+ if (typeof data.installShims === 'boolean') {
65
+ manifestData.installation.installShims = data.installShims;
66
+ }
67
+
64
68
  // Write YAML manifest
65
69
  // Clean the manifest data to remove any non-serializable values
66
70
  const cleanManifestData = structuredClone(manifestData);
@@ -106,6 +110,7 @@ class Manifest {
106
110
  version: manifestData.installation?.version,
107
111
  installDate: manifestData.installation?.installDate,
108
112
  lastUpdated: manifestData.installation?.lastUpdated,
113
+ installShims: manifestData.installation?.installShims,
109
114
  modules: moduleNames, // Simple array of module names for backward compatibility
110
115
  modulesDetailed: hasDetailedModules ? modules : null, // New detailed format
111
116
  ides: manifestData.ides || [],
@@ -153,6 +158,7 @@ class Manifest {
153
158
  version: manifest.installation?.version,
154
159
  installDate: manifest.installation?.installDate,
155
160
  lastUpdated: manifest.installation?.lastUpdated,
161
+ installShims: manifest.installation?.installShims,
156
162
  modules: moduleNames,
157
163
  modulesDetailed: hasDetailedModules ? modules : null,
158
164
  ides: manifest.ides || [],
@@ -0,0 +1,91 @@
1
+ const path = require('node:path');
2
+ const fs = require('../fs-native');
3
+ const yaml = require('yaml');
4
+ const csv = require('csv-parse/sync');
5
+
6
+ function parseSkillMetadata(content) {
7
+ const normalized = content.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
8
+ const match = normalized.match(/^---\n([\s\S]*?)\n---/);
9
+ if (!match) return null;
10
+
11
+ try {
12
+ const frontmatter = yaml.parse(match[1]);
13
+ return frontmatter && typeof frontmatter === 'object' ? frontmatter : null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function isShimSkill(metadata) {
20
+ return metadata?.metadata?.lifecycle === 'shim';
21
+ }
22
+
23
+ async function discoverShims(modulePath) {
24
+ const shims = [];
25
+
26
+ const walk = async (dir) => {
27
+ let entries;
28
+ try {
29
+ entries = await fs.readdir(dir, { withFileTypes: true });
30
+ } catch {
31
+ return;
32
+ }
33
+
34
+ const skillFile = path.join(dir, 'SKILL.md');
35
+ if (await fs.pathExists(skillFile)) {
36
+ const metadata = parseSkillMetadata(await fs.readFile(skillFile, 'utf8'));
37
+ if (isShimSkill(metadata)) {
38
+ shims.push({
39
+ id: metadata.name || path.basename(dir),
40
+ directory: dir,
41
+ relativeDirectory: path.relative(modulePath, dir),
42
+ });
43
+ }
44
+ return;
45
+ }
46
+
47
+ for (const entry of entries) {
48
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
49
+ await walk(path.join(dir, entry.name));
50
+ }
51
+ };
52
+
53
+ await walk(modulePath);
54
+ return shims;
55
+ }
56
+
57
+ async function readInstalledSkillIds(bmadDir) {
58
+ const ids = new Set();
59
+ const manifestPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
60
+ if (!(await fs.pathExists(manifestPath))) return ids;
61
+
62
+ try {
63
+ const content = await fs.readFile(manifestPath, 'utf8');
64
+ const records = csv.parse(content, { columns: true, skip_empty_lines: true });
65
+ for (const record of records) {
66
+ if (record.canonicalId) ids.add(record.canonicalId);
67
+ }
68
+ } catch {
69
+ // A missing or unreadable legacy manifest means there is no reliable
70
+ // evidence that compatibility shims were installed.
71
+ }
72
+
73
+ return ids;
74
+ }
75
+
76
+ function inferShimPreference({ requested, persisted, availableShims = [], installedSkillIds = new Set(), existing = false }) {
77
+ if (availableShims.length === 0) return false;
78
+ if (typeof requested === 'boolean') return requested;
79
+ if (typeof persisted === 'boolean') return persisted;
80
+ if (!existing) return false;
81
+
82
+ return availableShims.some((shim) => installedSkillIds.has(shim.id));
83
+ }
84
+
85
+ module.exports = {
86
+ discoverShims,
87
+ inferShimPreference,
88
+ isShimSkill,
89
+ parseSkillMetadata,
90
+ readInstalledSkillIds,
91
+ };
@@ -5,6 +5,7 @@ const prompts = require('../prompts');
5
5
  const { getProjectRoot, getSourcePath, getModulePath } = require('../project-root');
6
6
  const { CLIUtils } = require('../cli-utils');
7
7
  const { ExternalModuleManager } = require('./external-manager');
8
+ const { discoverShims } = require('../core/shim-policy');
8
9
 
9
10
  class OfficialModules {
10
11
  constructor(options = {}) {
@@ -131,6 +132,23 @@ class OfficialModules {
131
132
  return { modules };
132
133
  }
133
134
 
135
+ async discoverShims(moduleNames = [], options = {}) {
136
+ const shims = [];
137
+
138
+ for (const moduleName of moduleNames) {
139
+ const sourcePath = await this.findModuleSource(moduleName, {
140
+ silent: true,
141
+ channelOptions: options.channelOptions,
142
+ });
143
+ if (!sourcePath) continue;
144
+
145
+ const moduleShims = await discoverShims(sourcePath);
146
+ for (const shim of moduleShims) shims.push({ ...shim, module: moduleName });
147
+ }
148
+
149
+ return shims;
150
+ }
151
+
134
152
  /**
135
153
  * Get module information from a module path
136
154
  * @param {string} modulePath - Path to the module directory
@@ -296,9 +314,13 @@ class OfficialModules {
296
314
  `Aborting to avoid a partial install with no skills.`,
297
315
  );
298
316
  }
299
- await this._copyResolvedSkills(pluginResolution, targetPath, fileTrackingCallback, options.moduleConfig);
317
+ await this._copyResolvedSkills(pluginResolution, targetPath, fileTrackingCallback, options.moduleConfig, {
318
+ installShims: options.installShims,
319
+ });
300
320
  } else {
301
- await this.copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback, options.moduleConfig);
321
+ await this.copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback, options.moduleConfig, {
322
+ installShims: options.installShims,
323
+ });
302
324
  }
303
325
 
304
326
  if (!options.skipModuleInstaller) {
@@ -336,7 +358,7 @@ class OfficialModules {
336
358
  * @param {Function} fileTrackingCallback - Optional callback to track installed files
337
359
  * @param {Object} moduleConfig - Module configuration passed to copy filtering
338
360
  */
339
- async _copyResolvedSkills(resolved, targetPath, fileTrackingCallback = null, moduleConfig = {}) {
361
+ async _copyResolvedSkills(resolved, targetPath, fileTrackingCallback = null, moduleConfig = {}, installOptions = {}) {
340
362
  await fs.ensureDir(targetPath);
341
363
 
342
364
  // Copy each skill directory, flattened by leaf name. Leaf names must be
@@ -354,7 +376,7 @@ class OfficialModules {
354
376
  }
355
377
  seenLeaves.set(skillDirName, skillPath);
356
378
  const skillTarget = path.join(targetPath, skillDirName);
357
- await this.copyModuleWithFiltering(skillPath, skillTarget, fileTrackingCallback, moduleConfig);
379
+ await this.copyModuleWithFiltering(skillPath, skillTarget, fileTrackingCallback, moduleConfig, installOptions);
358
380
  }
359
381
 
360
382
  // Place module-help.csv at the module root.
@@ -385,7 +407,9 @@ class OfficialModules {
385
407
  await fs.remove(targetPath);
386
408
  }
387
409
 
388
- await this._copyResolvedSkills(resolved, targetPath, fileTrackingCallback, options.moduleConfig);
410
+ await this._copyResolvedSkills(resolved, targetPath, fileTrackingCallback, options.moduleConfig, {
411
+ installShims: options.installShims,
412
+ });
389
413
 
390
414
  // Create directories declared in module.yaml (strategies 1-4 may have these)
391
415
  if (!options.skipModuleInstaller) {
@@ -522,11 +546,19 @@ class OfficialModules {
522
546
  * @param {Function} fileTrackingCallback - Optional callback to track installed files
523
547
  * @param {Object} moduleConfig - Module configuration with conditional flags
524
548
  */
525
- async copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback = null, moduleConfig = {}) {
549
+ async copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback = null, moduleConfig = {}, installOptions = {}) {
526
550
  // Get all files in source
527
551
  const sourceFiles = await this.getFileList(sourcePath);
552
+ const shimDirectories =
553
+ installOptions.installShims === false
554
+ ? (await discoverShims(sourcePath)).map((shim) => shim.relativeDirectory.split(path.sep).join('/'))
555
+ : [];
528
556
 
529
557
  for (const file of sourceFiles) {
558
+ const normalizedFile = file.split(path.sep).join('/');
559
+ if (shimDirectories.some((shimDir) => shimDir === '' || normalizedFile === shimDir || normalizedFile.startsWith(`${shimDir}/`))) {
560
+ continue;
561
+ }
530
562
  // Skip sub-modules directory - these are IDE-specific and handled separately
531
563
  if (file.startsWith('sub-modules/')) {
532
564
  continue;
@@ -17,6 +17,7 @@ const {
17
17
  const channelResolver = require('./modules/channel-resolver');
18
18
  const prompts = require('./prompts');
19
19
  const { parseSetEntries } = require('./set-overrides');
20
+ const { inferShimPreference, readInstalledSkillIds } = require('./core/shim-policy');
20
21
 
21
22
  const manifest = new Manifest();
22
23
 
@@ -110,6 +111,33 @@ async function getModuleVersion(moduleCode, { repoUrl = null, registryDefault =
110
111
  * UI utilities for the installer
111
112
  */
112
113
  class UI {
114
+ async _selectShimPreference({ selectedModules, bmadDir, existing, options, channelOptions }) {
115
+ const { OfficialModules } = require('./modules/official-modules');
116
+ const officialModules = new OfficialModules({ channelOptions });
117
+ const availableShims = await officialModules.discoverShims(selectedModules, { channelOptions });
118
+
119
+ // The prompt is capability-driven. Once the last shim leaves the incoming
120
+ // release this becomes an ordinary empty set, regardless of old state.
121
+ if (availableShims.length === 0) return;
122
+
123
+ const previousManifest = existing ? await manifest.read(bmadDir) : null;
124
+ const installedSkillIds = existing ? await readInstalledSkillIds(bmadDir) : new Set();
125
+ const currentValue = inferShimPreference({
126
+ requested: options.shims,
127
+ persisted: previousManifest?.installShims,
128
+ availableShims,
129
+ installedSkillIds,
130
+ existing,
131
+ });
132
+
133
+ if (typeof options.shims === 'boolean' || options.yes) return currentValue;
134
+
135
+ return prompts.confirm({
136
+ message: `Install ${availableShims.length} deprecated compatibility shim skill(s)?`,
137
+ default: currentValue,
138
+ });
139
+ }
140
+
113
141
  /**
114
142
  * Warn once for each selected module the registry marks deprecated.
115
143
  *
@@ -302,7 +330,7 @@ class UI {
302
330
  throw new Error('No valid actions available for this installation');
303
331
  }
304
332
  const hasQuickUpdate = choices.some((c) => c.value === 'quick-update');
305
- const needsFullUpdate = !!options.customSource;
333
+ const needsFullUpdate = !!options.customSource || typeof options.shims === 'boolean';
306
334
  actionType = hasQuickUpdate && !needsFullUpdate ? 'quick-update' : (choices.find((c) => c.value === 'update') || choices[0]).value;
307
335
  await prompts.log.info(`Non-interactive mode (--yes): defaulting to ${actionType}`);
308
336
  } else {
@@ -322,6 +350,7 @@ class UI {
322
350
  actionType: 'quick-update',
323
351
  directory: confirmedDirectory,
324
352
  skipPrompts: options.yes || false,
353
+ installShims: options.shims,
325
354
  };
326
355
  }
327
356
 
@@ -406,6 +435,13 @@ class UI {
406
435
  ...options,
407
436
  channelOptions,
408
437
  });
438
+ const installShims = await this._selectShimPreference({
439
+ selectedModules,
440
+ bmadDir,
441
+ existing: true,
442
+ options,
443
+ channelOptions,
444
+ });
409
445
 
410
446
  // Warn about --pin/--next flags that refer to modules the user didn't
411
447
  // select, or that target bundled modules (core/bmm) where channel
@@ -432,6 +468,7 @@ class UI {
432
468
  skipPrompts: options.yes || false,
433
469
  channelOptions,
434
470
  _preserveModules: preservedModules,
471
+ installShims,
435
472
  };
436
473
  }
437
474
  }
@@ -487,6 +524,13 @@ class UI {
487
524
  ...options,
488
525
  channelOptions,
489
526
  });
527
+ const installShims = await this._selectShimPreference({
528
+ selectedModules,
529
+ bmadDir,
530
+ existing: false,
531
+ options,
532
+ channelOptions,
533
+ });
490
534
 
491
535
  // Warn about --pin/--next flags that refer to modules the user didn't
492
536
  // select, or that target bundled modules (core/bmm) where channel
@@ -512,6 +556,7 @@ class UI {
512
556
  setOverrides,
513
557
  skipPrompts: options.yes || false,
514
558
  channelOptions,
559
+ installShims,
515
560
  };
516
561
  }
517
562