bmad-method 6.6.1-next.9 → 6.7.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 (25) hide show
  1. package/{tools/installer/modules/registry-fallback.yaml → bmad-modules.yaml} +29 -15
  2. package/package.json +1 -1
  3. package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +18 -11
  4. package/src/bmm-skills/1-analysis/bmad-product-brief/customize.toml +13 -8
  5. package/src/bmm-skills/2-plan-workflows/bmad-prd/SKILL.md +54 -57
  6. package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/headless-schemas.md +2 -2
  7. package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/prd-template.md +40 -30
  8. package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/prd-validation-checklist.md +126 -21
  9. package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/validation-report-template.html +193 -58
  10. package/src/bmm-skills/2-plan-workflows/bmad-prd/customize.toml +47 -13
  11. package/src/bmm-skills/2-plan-workflows/bmad-prd/references/headless.md +27 -12
  12. package/src/bmm-skills/2-plan-workflows/bmad-prd/references/validate.md +97 -0
  13. package/src/bmm-skills/module.yaml +2 -2
  14. package/src/core-skills/module.yaml +1 -1
  15. package/tools/installer/core/installer.js +1 -22
  16. package/tools/installer/core/manifest.js +0 -22
  17. package/tools/installer/modules/channel-plan.js +1 -1
  18. package/tools/installer/modules/external-manager.js +9 -27
  19. package/tools/installer/modules/official-modules.js +9 -48
  20. package/tools/installer/ui.js +12 -196
  21. package/src/bmm-skills/2-plan-workflows/bmad-prd/references/facilitation-guide.md +0 -79
  22. package/src/bmm-skills/2-plan-workflows/bmad-prd/references/validation-render.md +0 -58
  23. package/src/bmm-skills/2-plan-workflows/bmad-prd/scripts/render-validation-html.py +0 -290
  24. package/tools/installer/modules/community-manager.js +0 -704
  25. package/tools/installer/modules/registry-client.js +0 -187
@@ -0,0 +1,97 @@
1
+ # Validate
2
+
3
+ The Validate intent playbook. Standalone — this intent critiques an existing PRD without changing it and ends after the user has seen the report; it does not run Finalize. The synthesis pipeline below is also reused for mid-session report requests during Create/Update.
4
+
5
+ ## Orient
6
+
7
+ Source-extract against `.decision-log.md`, any original inputs, and the PRD/addendum themselves. Delegate to subagents per PRD Discipline → "Extract, don't ingest" (in SKILL.md); the parent assembles from extracts.
8
+
9
+ ## Run the Reviewer Gate
10
+
11
+ Run the Reviewer Gate (see SKILL.md) against `prd.md` (and `addendum.md` if present). The rubric walker is the default entry in the gate menu; under Validate intent it additionally runs the synthesis pipeline below. The Finalize discipline pass during Create/Update does NOT render a report — findings stay in-conversation.
12
+
13
+ ## Rubric-walker pipeline
14
+
15
+ The rubric walker is the primary review entry. Spawn it as a subagent with this prompt:
16
+
17
+ > You are validating a PRD against the quality rubric at `{workflow.validation_checklist_template}`. Read the full rubric first, then read `prd.md` (and `addendum.md` if present). Form a judgment per dimension — *strong / adequate / thin / broken* — and write findings only where they add information. Cite specific PRD locations and quote phrases. Severity ranks impact on the PRD's usefulness, not how easy the fix is. Write your review to `{doc_workspace}/review-rubric.md` in the format the rubric specifies. Return ONLY a compact summary (overall verdict, dimension verdicts, finding counts by severity, file path).
18
+
19
+ The Reviewer Gate may also dispatch additional reviewers from `{workflow.finalize_reviewers}` (adversarial-general by default) and any ad-hoc reviewers the parent judges warranted. Each writes its review to `{doc_workspace}/review-{slug}.md` and returns a compact summary. Run in parallel.
20
+
21
+ ## Synthesis pipeline
22
+
23
+ Once every selected reviewer has returned, the parent synthesizes one consolidated report. **Do not skip this step under Validate intent** — it produces the persistent artifact the user opens.
24
+
25
+ ### Inputs
26
+
27
+ - `{doc_workspace}/review-rubric.md` — primary, structured by the seven dimensions
28
+ - Zero or more `{doc_workspace}/review-{slug}.md` files — extra reviewers (adversarial, etc.)
29
+ - `{workflow.validation_report_template}` — the HTML skeleton
30
+
31
+ ### What the synthesis pass does
32
+
33
+ 1. Read every reviewer file in `{doc_workspace}/review-*.md`.
34
+ 2. Fill the HTML skeleton:
35
+ - **Header.** PRD name, path. Grade derived from the rubric verdicts and severity counts: *Excellent* = all dimensions strong/adequate, no high/critical findings · *Good* = ≤1 thin dimension, no critical findings · *Fair* = multiple thin dimensions or any high finding · *Poor* = any broken dimension or any critical finding. Set the matching `grade-excellent | grade-good | grade-fair | grade-poor` class.
36
+ - **Synthesis block.** Lift the rubric's *Overall verdict* paragraph as the lead; if adversarial or ad-hoc reviewers materially shift the picture, add a second paragraph that names what they surfaced.
37
+ - **Dimension summary cards.** One per dimension that was assessed. Colored verdict text. Skip dimensions the rubric marked n/a for this PRD (e.g. downstream usability for a standalone PRD).
38
+ - **Dimension sections.** One `<section class="dimension">` per assessed dimension, in rubric order. `<details open>` for *thin* and *broken*; closed for *strong* and *adequate*. Each contains the dimension judgment (the prose from review-rubric.md) and the findings list.
39
+ - **Reviewer sections.** One `<section class="reviewer-section">` per extra reviewer that ran. The source file path goes in the `<span class="reviewer-source">`. Closed by default. Adversarial findings keep their adversarial voice — do not soften.
40
+ - **Mechanical notes.** Bullet list from the rubric's "Mechanical notes" section. Skip the block if empty.
41
+ - **Footer.** Rubric path, ISO timestamp.
42
+ 3. Write the filled HTML to `{doc_workspace}/validation-report.html`.
43
+ 4. Write the markdown twin to `{doc_workspace}/validation-report.md` (same content, grouped by severity rather than by dimension — see format below; this is the canonical form for downstream re-reading).
44
+ 5. Open the HTML in the default browser:
45
+ ```bash
46
+ python3 -c "import webbrowser, pathlib; webbrowser.open(pathlib.Path('{doc_workspace}/validation-report.html').resolve().as_uri())"
47
+ ```
48
+ Skip the open step in headless mode (see `references/headless.md`).
49
+
50
+ ### Markdown twin format
51
+
52
+ ```markdown
53
+ # Validation Report — {prd_name}
54
+
55
+ - **PRD:** `{prd_path}`
56
+ - **Rubric:** `{rubric_path}`
57
+ - **Run at:** {ISO timestamp}
58
+ - **Grade:** {Excellent | Good | Fair | Poor}
59
+
60
+ ## Overall verdict
61
+ {synthesis paragraphs}
62
+
63
+ ## Dimension verdicts
64
+ - Decision-readiness — {verdict}
65
+ - Substance over theater — {verdict}
66
+ - (etc. for each assessed dimension)
67
+
68
+ ## Findings by severity
69
+
70
+ ### Critical (n)
71
+ **[Dimension or Reviewer]** — Title (§ location)
72
+ {Note}
73
+ Fix: {suggested fix}
74
+
75
+ ### High (n)
76
+ ...
77
+
78
+ ### Medium (n)
79
+ ...
80
+
81
+ ### Low (n)
82
+ ...
83
+
84
+ ## Mechanical notes
85
+ - {bullet}
86
+
87
+ ## Reviewer files
88
+ - `review-rubric.md`
89
+ - `review-adversarial-general.md` (if present)
90
+ - (etc.)
91
+ ```
92
+
93
+ Re-running validation overwrites the consolidated report in place. The individual `review-*.md` files are preserved so the user can drill in.
94
+
95
+ ## Close
96
+
97
+ Surface artifact paths; the rendered HTML/markdown is the persistent artifact. Always offer to roll findings into an Update.
@@ -1,6 +1,6 @@
1
1
  code: bmm
2
- name: "BMad Method Agile-AI Driven-Development"
3
- description: "AI-driven agile development framework"
2
+ name: "BMad Method"
3
+ description: "Full-lifecycle AI agile development: analysis, planning, architecture, implementation"
4
4
  default_selected: true # This module will be selected by default for new installations
5
5
 
6
6
  # Variables from Core Config inserted:
@@ -1,6 +1,6 @@
1
1
  code: core
2
2
  name: "BMad Core Module"
3
- description: "Core configuration and shared resources"
3
+ description: "Shared utilities across modules"
4
4
 
5
5
  header: "BMad Core Configuration"
6
6
  subheader: "Configure the core settings for your BMad installation.\nThese settings will be used across all installed bmad skills, workflows, and agents."
@@ -640,13 +640,7 @@ class Installer {
640
640
  const moduleInfo = sourcePath ? await officialModules.getModuleInfo(sourcePath, moduleName, '') : null;
641
641
  const displayName = moduleInfo?.name || moduleName;
642
642
 
643
- const externalResolution = officialModules.externalModuleManager.getResolution(moduleName);
644
- let communityResolution = null;
645
- if (!externalResolution) {
646
- const { CommunityModuleManager } = require('../modules/community-manager');
647
- communityResolution = new CommunityModuleManager().getResolution(moduleName);
648
- }
649
- const resolution = externalResolution || communityResolution;
643
+ const resolution = officialModules.externalModuleManager.getResolution(moduleName);
650
644
  const cachedResolution = CustomModuleManager._resolutionCache.get(moduleName);
651
645
  const versionInfo = await resolveModuleVersion(moduleName, {
652
646
  moduleSourcePath: sourcePath,
@@ -1178,21 +1172,6 @@ class Installer {
1178
1172
  }
1179
1173
  }
1180
1174
 
1181
- // Add installed community modules to available modules
1182
- const { CommunityModuleManager } = require('../modules/community-manager');
1183
- const communityMgr = new CommunityModuleManager();
1184
- const communityModules = await communityMgr.listAll();
1185
- for (const communityModule of communityModules) {
1186
- if (installedModules.includes(communityModule.code) && !availableModules.some((m) => m.id === communityModule.code)) {
1187
- availableModules.push({
1188
- id: communityModule.code,
1189
- name: communityModule.displayName,
1190
- isExternal: true,
1191
- fromCommunity: true,
1192
- });
1193
- }
1194
- }
1195
-
1196
1175
  // Add installed custom modules to available modules
1197
1176
  const { CustomModuleManager } = require('../modules/custom-module-manager');
1198
1177
  const customMgr = new CustomModuleManager();
@@ -310,28 +310,6 @@ class Manifest {
310
310
  };
311
311
  }
312
312
 
313
- // Check if this is a community module
314
- const { CommunityModuleManager } = require('../modules/community-manager');
315
- const communityMgr = new CommunityModuleManager();
316
- const communityInfo = await communityMgr.getModuleByCode(moduleName);
317
- if (communityInfo) {
318
- const communityResolution = communityMgr.getResolution(moduleName);
319
- const versionInfo = await resolveModuleVersion(moduleName, {
320
- moduleSourcePath,
321
- fallbackVersion: communityInfo.version,
322
- });
323
- return {
324
- version: communityResolution?.version || versionInfo.version || communityInfo.version,
325
- source: 'community',
326
- npmPackage: communityInfo.npmPackage || null,
327
- repoUrl: communityInfo.url || null,
328
- channel: communityResolution?.channel || null,
329
- sha: communityResolution?.sha || null,
330
- registryApprovedTag: communityResolution?.registryApprovedTag || null,
331
- registryApprovedSha: communityResolution?.registryApprovedSha || null,
332
- };
333
- }
334
-
335
313
  // Check if this is a custom module (from user-provided URL or local path)
336
314
  const { CustomModuleManager } = require('../modules/custom-module-manager');
337
315
  const customMgr = new CustomModuleManager();
@@ -7,7 +7,7 @@
7
7
  * We build the plan from:
8
8
  * 1. CLI flags (--channel / --all-* / --next=CODE / --pin CODE=TAG)
9
9
  * 2. Interactive answers (the "all stable?" gate + per-module picker)
10
- * 3. Registry defaults (default_channel from registry-fallback.yaml / official.yaml)
10
+ * 3. Registry defaults (default_channel from bmad-modules.yaml)
11
11
  * 4. Hardcoded fallback 'stable'
12
12
  *
13
13
  * Precedence: --pin > --next=CODE > --channel (global) > registry default > 'stable'.
@@ -4,9 +4,9 @@ const path = require('node:path');
4
4
  const { execSync } = require('node:child_process');
5
5
  const yaml = require('yaml');
6
6
  const prompts = require('../prompts');
7
- const { RegistryClient } = require('./registry-client');
8
7
  const { resolveChannel, tagExists, parseGitHubRepo } = require('./channel-resolver');
9
8
  const { decideChannelForModule } = require('./channel-plan');
9
+ const { getProjectRoot } = require('../project-root');
10
10
 
11
11
  const VALID_CHANNELS = new Set(['stable', 'next', 'pinned']);
12
12
 
@@ -46,15 +46,12 @@ async function writeChannelMarker(markerPath, data) {
46
46
  }
47
47
  }
48
48
 
49
- const MARKETPLACE_OWNER = 'bmad-code-org';
50
- const MARKETPLACE_REPO = 'bmad-plugins-marketplace';
51
- const MARKETPLACE_REF = 'main';
52
- const FALLBACK_CONFIG_PATH = path.join(__dirname, 'registry-fallback.yaml');
49
+ const REGISTRY_CONFIG_PATH = path.join(getProjectRoot(), 'bmad-modules.yaml');
53
50
 
54
51
  /**
55
- * Manages official modules from the remote BMad marketplace registry.
56
- * Fetches registry/official.yaml from GitHub; falls back to the bundled
57
- * external-official-modules.yaml when the network is unavailable.
52
+ * Manages official modules from the bundled registry file. The remote
53
+ * marketplace fetch has been retired; this repo is the single source of truth
54
+ * for which official modules exist and how they are displayed.
58
55
  *
59
56
  * @class ExternalModuleManager
60
57
  */
@@ -65,9 +62,7 @@ class ExternalModuleManager {
65
62
  // ExternalModuleManager) sees resolutions made during install.
66
63
  static _resolutions = new Map();
67
64
 
68
- constructor() {
69
- this._client = new RegistryClient();
70
- }
65
+ constructor() {}
71
66
 
72
67
  /**
73
68
  * Get the most recent channel resolution for a module (if any).
@@ -79,8 +74,7 @@ class ExternalModuleManager {
79
74
  }
80
75
 
81
76
  /**
82
- * Load the official modules registry from GitHub, falling back to the
83
- * bundled YAML file if the fetch fails.
77
+ * Load the official modules registry from the bundled YAML file.
84
78
  * @returns {Object} Parsed YAML content with modules array
85
79
  */
86
80
  async loadExternalModulesConfig() {
@@ -88,23 +82,10 @@ class ExternalModuleManager {
88
82
  return this.cachedModules;
89
83
  }
90
84
 
91
- // Try remote registry first
92
- try {
93
- const config = await this._client.fetchGitHubYaml(MARKETPLACE_OWNER, MARKETPLACE_REPO, 'registry/official.yaml', MARKETPLACE_REF);
94
- if (config?.modules?.length) {
95
- this.cachedModules = config;
96
- return config;
97
- }
98
- } catch {
99
- // Fall through to local fallback
100
- }
101
-
102
- // Fallback to bundled file
103
85
  try {
104
- const content = await fs.readFile(FALLBACK_CONFIG_PATH, 'utf8');
86
+ const content = await fs.readFile(REGISTRY_CONFIG_PATH, 'utf8');
105
87
  const config = yaml.parse(content);
106
88
  this.cachedModules = config;
107
- await prompts.log.warn('Could not reach BMad registry; using bundled module list.');
108
89
  return config;
109
90
  } catch (error) {
110
91
  await prompts.log.warn(`Failed to load modules config: ${error.message}`);
@@ -130,6 +111,7 @@ class ExternalModuleManager {
130
111
  defaultSelected: mod.default_selected === true || mod.defaultSelected === true,
131
112
  type: mod.type || 'bmad-org',
132
113
  npmPackage: mod.npm_package || mod.npmPackage || null,
114
+ pluginName: mod.plugin_name || mod.pluginName || null,
133
115
  defaultChannel: normalizeChannelName(mod.default_channel || mod.defaultChannel) || 'stable',
134
116
  builtIn: mod.built_in === true,
135
117
  isExternal: mod.built_in !== true,
@@ -231,14 +231,6 @@ class OfficialModules {
231
231
  return externalSource;
232
232
  }
233
233
 
234
- // Check community modules (pass channelOptions for --next/--pin overrides)
235
- const { CommunityModuleManager } = require('./community-manager');
236
- const communityMgr = new CommunityModuleManager();
237
- const communitySource = await communityMgr.findModuleSource(moduleCode, options);
238
- if (communitySource) {
239
- return communitySource;
240
- }
241
-
242
234
  // Check custom modules (from user-provided URLs, already cloned to cache)
243
235
  const { CustomModuleManager } = require('./custom-module-manager');
244
236
  const customMgr = new CustomModuleManager();
@@ -269,21 +261,6 @@ class OfficialModules {
269
261
  return this.installFromResolution(resolved, bmadDir, fileTrackingCallback, options);
270
262
  }
271
263
 
272
- // Community modules whose cloned repo ships marketplace.json get the same
273
- // skill-level install treatment as custom-source installs. If the in-process
274
- // cache wasn't populated (e.g. caller skipped the pre-clone phase), fall
275
- // back to resolving directly from `~/.bmad/cache/community-modules/<name>/`
276
- // so we don't silently regress to the legacy half-install path.
277
- const { CommunityModuleManager } = require('./community-manager');
278
- const communityMgr = new CommunityModuleManager();
279
- let communityResolved = communityMgr.getPluginResolution(moduleName);
280
- if (!communityResolved) {
281
- communityResolved = await communityMgr.resolveFromCache(moduleName);
282
- }
283
- if (communityResolved) {
284
- return this.installFromResolution(communityResolved, bmadDir, fileTrackingCallback, options);
285
- }
286
-
287
264
  const sourcePath = await this.findModuleSource(moduleName, {
288
265
  silent: options.silent,
289
266
  channelOptions: options.channelOptions,
@@ -310,14 +287,9 @@ class OfficialModules {
310
287
  const manifestObj = new Manifest();
311
288
  const versionInfo = await manifestObj.getModuleVersionInfo(moduleName, bmadDir, sourcePath);
312
289
 
313
- // Pick up channel resolution recorded by whichever manager did the clone.
314
- const externalResolution = this.externalModuleManager.getResolution(moduleName);
315
- let communityResolution = null;
316
- if (!externalResolution) {
317
- const { CommunityModuleManager } = require('./community-manager');
318
- communityResolution = new CommunityModuleManager().getResolution(moduleName);
319
- }
320
- const resolution = externalResolution || communityResolution;
290
+ // Pick up channel resolution recorded by the external manager (the only
291
+ // manager that does pre-clone resolution now that community is retired).
292
+ const resolution = this.externalModuleManager.getResolution(moduleName);
321
293
 
322
294
  await manifestObj.addModule(bmadDir, moduleName, {
323
295
  version: resolution?.version || versionInfo.version,
@@ -326,8 +298,6 @@ class OfficialModules {
326
298
  repoUrl: versionInfo.repoUrl,
327
299
  channel: resolution?.channel,
328
300
  sha: resolution?.sha,
329
- registryApprovedTag: communityResolution?.registryApprovedTag,
330
- registryApprovedSha: communityResolution?.registryApprovedSha,
331
301
  });
332
302
 
333
303
  return { success: true, module: moduleName, path: targetPath, versionInfo };
@@ -375,27 +345,19 @@ class OfficialModules {
375
345
  await this.createModuleDirectories(resolved.code, bmadDir, options);
376
346
  }
377
347
 
378
- // Update manifest. For community installs we honor the channel resolved by
379
- // CommunityModuleManager (stable/next/pinned) and propagate the registry's
380
- // approved tag/sha. For custom-source installs we derive channel from the
348
+ // Update manifest. For custom-source installs we derive channel from the
381
349
  // cloneRef (present → pinned, absent → next; local paths have no channel).
382
350
  const { Manifest } = require('../core/manifest');
383
351
  const manifestObj = new Manifest();
384
352
 
385
353
  const hasGitClone = !!resolved.repoUrl;
386
- const isCommunity = resolved.communitySource === true;
387
354
  const manifestEntry = {
388
- version: resolved.communityVersion || resolved.cloneRef || (hasGitClone ? 'main' : resolved.version || null),
389
- source: isCommunity ? 'community' : 'custom',
355
+ version: resolved.cloneRef || (hasGitClone ? 'main' : resolved.version || null),
356
+ source: 'custom',
390
357
  npmPackage: null,
391
358
  repoUrl: resolved.repoUrl || null,
392
359
  };
393
- if (isCommunity) {
394
- if (resolved.communityChannel) manifestEntry.channel = resolved.communityChannel;
395
- if (resolved.cloneSha) manifestEntry.sha = resolved.cloneSha;
396
- if (resolved.registryApprovedTag) manifestEntry.registryApprovedTag = resolved.registryApprovedTag;
397
- if (resolved.registryApprovedSha) manifestEntry.registryApprovedSha = resolved.registryApprovedSha;
398
- } else if (hasGitClone) {
360
+ if (hasGitClone) {
399
361
  manifestEntry.channel = resolved.cloneRef ? 'pinned' : 'next';
400
362
  if (resolved.cloneSha) manifestEntry.sha = resolved.cloneSha;
401
363
  if (resolved.rawInput) manifestEntry.rawSource = resolved.rawInput;
@@ -408,11 +370,10 @@ class OfficialModules {
408
370
  module: resolved.code,
409
371
  path: targetPath,
410
372
  // Mirror the manifestEntry.version precedence above so downstream summary
411
- // lines show the same string we just wrote to disk (community installs
412
- // use the registry-approved tag via `communityVersion`; custom git-backed
373
+ // lines show the same string we just wrote to disk (custom git-backed
413
374
  // installs show the cloned ref or 'main').
414
375
  versionInfo: {
415
- version: resolved.communityVersion || resolved.cloneRef || (hasGitClone ? 'main' : resolved.version || ''),
376
+ version: resolved.cloneRef || (hasGitClone ? 'main' : resolved.version || ''),
416
377
  },
417
378
  };
418
379
  }
@@ -818,32 +818,18 @@ class UI {
818
818
  // Phase 1: Official modules
819
819
  const officialSelected = await this._selectOfficialModules(installedModuleIds, installedModuleVersions, channelOptions);
820
820
 
821
- // Determine which installed modules are NOT official (community or custom).
822
- // These must be preserved even if the user declines to browse community/custom.
823
- const officialCodes = new Set(officialSelected);
821
+ // Identify installed modules that aren't official (previously installed
822
+ // community modules or custom-source modules). Preserve them on update;
823
+ // they can be managed via --custom-source, uninstall, or a dedicated installer.
824
824
  const externalManager = new ExternalModuleManager();
825
825
  const registryModules = await externalManager.listAvailable();
826
826
  const officialRegistryCodes = new Set(['core', 'bmm', ...registryModules.map((m) => m.code)]);
827
827
  const installedNonOfficial = [...installedModuleIds].filter((id) => !officialRegistryCodes.has(id));
828
828
 
829
- // Phase 2: Community modules (category drill-down)
830
- // Returns { codes, didBrowse } so we know if the user entered the flow
831
- const communityResult = await this._browseCommunityModules(installedModuleIds);
832
-
833
- // Phase 3: Custom URL modules
829
+ // Phase 2: Custom URL modules
834
830
  const customSelected = await this._addCustomUrlModules(installedModuleIds);
835
831
 
836
- // Merge all selections
837
- const allSelected = new Set([...officialSelected, ...communityResult.codes, ...customSelected]);
838
-
839
- // Auto-include installed non-official modules that the user didn't get
840
- // a chance to manage (they declined to browse). If they did browse,
841
- // trust their selections - they could have deselected intentionally.
842
- if (!communityResult.didBrowse) {
843
- for (const code of installedNonOfficial) {
844
- allSelected.add(code);
845
- }
846
- }
832
+ const allSelected = new Set([...officialSelected, ...customSelected, ...installedNonOfficial]);
847
833
 
848
834
  return [...allSelected];
849
835
  }
@@ -954,166 +940,6 @@ class UI {
954
940
  return result;
955
941
  }
956
942
 
957
- /**
958
- * Browse and select community modules using category drill-down.
959
- * Featured/promoted modules appear at the top.
960
- * @param {Set} installedModuleIds - Currently installed module IDs
961
- * @returns {Object} { codes: string[], didBrowse: boolean }
962
- */
963
- async _browseCommunityModules(installedModuleIds = new Set()) {
964
- const browseCommunity = await prompts.confirm({
965
- message: 'Would you like to browse community modules?',
966
- default: false,
967
- });
968
- if (!browseCommunity) return { codes: [], didBrowse: false };
969
-
970
- const { CommunityModuleManager } = require('./modules/community-manager');
971
- const communityMgr = new CommunityModuleManager();
972
-
973
- const s = await prompts.spinner();
974
- s.start('Loading community module catalog...');
975
-
976
- let categories, featured, allCommunity;
977
- try {
978
- [categories, featured, allCommunity] = await Promise.all([
979
- communityMgr.getCategoryList(),
980
- communityMgr.listFeatured(),
981
- communityMgr.listAll(),
982
- ]);
983
- s.stop(`Community catalog loaded (${allCommunity.length} modules)`);
984
- } catch (error) {
985
- s.error('Failed to load community catalog');
986
- await prompts.log.warn(` ${error.message}`);
987
- return { codes: [], didBrowse: false };
988
- }
989
-
990
- if (allCommunity.length === 0) {
991
- await prompts.log.info('No community modules are currently available.');
992
- return { codes: [], didBrowse: false };
993
- }
994
-
995
- const selectedCodes = new Set();
996
- let browsing = true;
997
-
998
- while (browsing) {
999
- const categoryChoices = [];
1000
-
1001
- // Featured section at top
1002
- if (featured.length > 0) {
1003
- categoryChoices.push({
1004
- value: '__featured__',
1005
- label: `\u2605 Featured (${featured.length} module${featured.length === 1 ? '' : 's'})`,
1006
- });
1007
- }
1008
-
1009
- // Categories with module counts
1010
- for (const cat of categories) {
1011
- categoryChoices.push({
1012
- value: cat.slug,
1013
- label: `${cat.name} (${cat.moduleCount} module${cat.moduleCount === 1 ? '' : 's'})`,
1014
- });
1015
- }
1016
-
1017
- // Special actions at bottom
1018
- categoryChoices.push(
1019
- { value: '__all__', label: '\u25CE View all community modules' },
1020
- { value: '__search__', label: '\u25CE Search by keyword' },
1021
- { value: '__done__', label: '\u2713 Done browsing' },
1022
- );
1023
-
1024
- const selectedCount = selectedCodes.size;
1025
- const categoryChoice = await prompts.select({
1026
- message: `Browse community modules${selectedCount > 0 ? ` (${selectedCount} selected)` : ''}:`,
1027
- choices: categoryChoices,
1028
- });
1029
-
1030
- if (categoryChoice === '__done__') {
1031
- browsing = false;
1032
- continue;
1033
- }
1034
-
1035
- let modulesToShow;
1036
- switch (categoryChoice) {
1037
- case '__featured__': {
1038
- modulesToShow = featured;
1039
-
1040
- break;
1041
- }
1042
- case '__all__': {
1043
- modulesToShow = allCommunity;
1044
-
1045
- break;
1046
- }
1047
- case '__search__': {
1048
- const query = await prompts.text({
1049
- message: 'Search community modules:',
1050
- placeholder: 'e.g., design, testing, game',
1051
- });
1052
- if (!query || query.trim() === '') continue;
1053
- modulesToShow = await communityMgr.searchByKeyword(query.trim());
1054
- if (modulesToShow.length === 0) {
1055
- await prompts.log.warn('No matching modules found.');
1056
- continue;
1057
- }
1058
-
1059
- break;
1060
- }
1061
- default: {
1062
- modulesToShow = await communityMgr.listByCategory(categoryChoice);
1063
- }
1064
- }
1065
-
1066
- // Build options for autocompleteMultiselect
1067
- const trustBadge = (tier) => {
1068
- if (tier === 'bmad-certified') return '\u2713';
1069
- if (tier === 'community-reviewed') return '\u25CB';
1070
- return '\u26A0';
1071
- };
1072
-
1073
- const options = modulesToShow.map((mod) => {
1074
- const versionStr = mod.version ? ` (v${mod.version})` : '';
1075
- const badge = trustBadge(mod.trustTier);
1076
- return {
1077
- label: `${mod.displayName}${versionStr} [${badge}]`,
1078
- value: mod.code,
1079
- hint: mod.description,
1080
- };
1081
- });
1082
-
1083
- // Pre-check modules that are already selected or installed
1084
- const initialValues = modulesToShow.filter((m) => selectedCodes.has(m.code) || installedModuleIds.has(m.code)).map((m) => m.code);
1085
-
1086
- const selected = await prompts.autocompleteMultiselect({
1087
- message: 'Select community modules:',
1088
- options,
1089
- initialValues: initialValues.length > 0 ? initialValues : undefined,
1090
- required: false,
1091
- maxItems: Math.min(options.length, 10),
1092
- });
1093
-
1094
- // Update accumulated selections: sync with what user selected in this view
1095
- const shownCodes = new Set(modulesToShow.map((m) => m.code));
1096
- for (const code of shownCodes) {
1097
- if (selected && selected.includes(code)) {
1098
- selectedCodes.add(code);
1099
- } else {
1100
- selectedCodes.delete(code);
1101
- }
1102
- }
1103
- }
1104
-
1105
- if (selectedCodes.size > 0) {
1106
- const moduleLines = [];
1107
- for (const code of selectedCodes) {
1108
- const mod = await communityMgr.getModuleByCode(code);
1109
- moduleLines.push(` \u2022 ${mod?.displayName || code}`);
1110
- }
1111
- await prompts.log.message('Selected community modules:\n' + moduleLines.join('\n'));
1112
- }
1113
-
1114
- return { codes: [...selectedCodes], didBrowse: true };
1115
- }
1116
-
1117
943
  /**
1118
944
  * Prompt user to install modules from custom sources (Git URLs or local paths).
1119
945
  * @param {Set} installedModuleIds - Currently installed module IDs
@@ -1121,7 +947,7 @@ class UI {
1121
947
  */
1122
948
  async _addCustomUrlModules(installedModuleIds = new Set()) {
1123
949
  const addCustom = await prompts.confirm({
1124
- message: 'Would you like to install from a custom source (Git URL or local path)?',
950
+ message: 'Do you want to install custom or community modules (Git URL or local path)?',
1125
951
  default: false,
1126
952
  });
1127
953
  if (!addCustom) return [];
@@ -1885,19 +1711,14 @@ class UI {
1885
1711
  const haveFlagIntent = channelOptions.global || channelOptions.nextSet.size > 0 || channelOptions.pins.size > 0;
1886
1712
  if (haveFlagIntent) return;
1887
1713
 
1888
- // Figure out which selected modules actually get a channel (externals +
1889
- // community modules). Bundled core/bmm and custom modules skip the picker.
1714
+ // Figure out which selected modules actually get a channel (externals only).
1715
+ // Bundled core/bmm and custom modules skip the picker.
1890
1716
  const externalManager = new ExternalModuleManager();
1891
1717
  const externals = await externalManager.listAvailable();
1892
1718
  const externalByCode = new Map(externals.map((m) => [m.code, m]));
1893
1719
 
1894
- const { CommunityModuleManager } = require('./modules/community-manager');
1895
- const communityMgr = new CommunityModuleManager();
1896
- const community = await communityMgr.listAll();
1897
- const communityByCode = new Map(community.map((m) => [m.code, m]));
1898
-
1899
1720
  const channelSelectable = selectedModules.filter((code) => {
1900
- const info = externalByCode.get(code) || communityByCode.get(code);
1721
+ const info = externalByCode.get(code);
1901
1722
  return info && !info.builtIn;
1902
1723
  });
1903
1724
  if (channelSelectable.length === 0) return;
@@ -1912,7 +1733,7 @@ class UI {
1912
1733
  const { fetchStableTags, parseGitHubRepo } = require('./modules/channel-resolver');
1913
1734
 
1914
1735
  for (const code of channelSelectable) {
1915
- const info = externalByCode.get(code) || communityByCode.get(code);
1736
+ const info = externalByCode.get(code);
1916
1737
  const repoUrl = info.url;
1917
1738
 
1918
1739
  // Try to pre-resolve the top stable tag so we can surface it in the picker.
@@ -1987,11 +1808,6 @@ class UI {
1987
1808
  const externals = await externalManager.listAvailable();
1988
1809
  const externalByCode = new Map(externals.map((m) => [m.code, m]));
1989
1810
 
1990
- const { CommunityModuleManager } = require('./modules/community-manager');
1991
- const communityMgr = new CommunityModuleManager();
1992
- const community = await communityMgr.listAll();
1993
- const communityByCode = new Map(community.map((m) => [m.code, m]));
1994
-
1995
1811
  const { fetchStableTags, classifyUpgrade, releaseNotesUrl } = require('./modules/channel-resolver');
1996
1812
  const { parseGitHubRepo } = require('./modules/channel-resolver');
1997
1813
 
@@ -2003,7 +1819,7 @@ class UI {
2003
1819
  const existingWithChannel = selectedModules.filter((code) => {
2004
1820
  const prev = existingByName.get(code);
2005
1821
  if (!prev) return false;
2006
- const info = externalByCode.get(code) || communityByCode.get(code);
1822
+ const info = externalByCode.get(code);
2007
1823
  return info && !info.builtIn;
2008
1824
  });
2009
1825
  if (existingWithChannel.length > 0) {
@@ -2018,7 +1834,7 @@ class UI {
2018
1834
  const prev = existingByName.get(code);
2019
1835
  if (!prev) continue;
2020
1836
 
2021
- const info = externalByCode.get(code) || communityByCode.get(code);
1837
+ const info = externalByCode.get(code);
2022
1838
  if (!info) continue;
2023
1839
  // Bundled modules (core/bmm) ship with the installer binary itself —
2024
1840
  // their version is stapled to the CLI version, not a git tag. Skip