From 6c1f9164d0e435be849344238eb3c29baae41ca5 Mon Sep 17 00:00:00 2001 From: bmad Date: Sun, 26 Apr 2026 05:58:09 -0300 Subject: [PATCH 1/8] feat(installer): register automator external module --- .../installer/modules/registry-fallback.yaml | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tools/installer/modules/registry-fallback.yaml b/tools/installer/modules/registry-fallback.yaml index 52bc4b4fc..f9ad2e564 100644 --- a/tools/installer/modules/registry-fallback.yaml +++ b/tools/installer/modules/registry-fallback.yaml @@ -50,3 +50,25 @@ modules: type: bmad-org npmPackage: bmad-method-test-architecture-enterprise default_channel: stable + + bmad-automator: + url: https://github.com/bmad-code-org/bmad-automator + source-root: payload/.claude/skills + code: bma + name: "BMad Automator (Experimental)" + description: "Experimental pure-skill story automation. Runs only from Claude Code; supports Claude Code and Codex worker sessions; requires tmux on macOS." + defaultSelected: false + type: experimental + npmPackage: bmad-story-automator + default_channel: stable + install-targets: + - claude-code + worker-targets: + - claude-code + - codex + requirements: + - Claude Code entrypoint + - Claude Code or Codex worker sessions + - tmux + - macOS + install-note: "Experimental: BMad Automator only works from Claude Code. It currently supports Claude Code and Codex worker sessions, and requires tmux on macOS." From 3372e95b627477e851f66b717f52e113b9457f9b Mon Sep 17 00:00:00 2001 From: bmad Date: Sun, 26 Apr 2026 06:02:24 -0300 Subject: [PATCH 2/8] feat(installer): install automator skill module --- docs/how-to/install-bmad.md | 4 +- docs/reference/modules.md | 18 ++++ test/test-installation-components.js | 96 +++++++++++++++++++++ tools/installer/README.md | 10 ++- tools/installer/core/manifest-generator.js | 13 +++ tools/installer/ide/_config-driven.js | 31 +++++++ tools/installer/modules/external-manager.js | 33 ++++++- tools/installer/ui.js | 39 +++++++++ 8 files changed, 239 insertions(+), 5 deletions(-) diff --git a/docs/how-to/install-bmad.md b/docs/how-to/install-bmad.md index 224704a47..f4115f483 100644 --- a/docs/how-to/install-bmad.md +++ b/docs/how-to/install-bmad.md @@ -31,7 +31,7 @@ npx bmad-method install The interactive flow asks you five things: 1. Installation directory (defaults to the current working directory) -2. Which modules to install (checkboxes for core, bmm, bmb, cis, gds, tea) +2. Which modules to install (checkboxes for core, bmm, bmb, cis, gds, tea, bma) 3. **"Ready to install (all stable)?"** — Yes accepts the latest released tag for every external module 4. Which AI tools/IDEs to integrate with (claude-code, cursor, and others) 5. Per-module config (name, language, output folder) @@ -53,7 +53,7 @@ Two independent axes control what ends up on disk. ### Axis 1: external module channels -Every external module — bmb, cis, gds, tea, and any community module — installs on one of three channels: +Every external module — bmb, cis, gds, tea, bma, and any community module — installs on one of three channels: | Channel | What gets installed | Who picks this | | ------------------ | ---------------------------------------------------------------------------- | --------------------------------------- | diff --git a/docs/reference/modules.md b/docs/reference/modules.md index 6bdc64190..c26d02816 100644 --- a/docs/reference/modules.md +++ b/docs/reference/modules.md @@ -71,6 +71,24 @@ Enterprise-grade test strategy, automation guidance, and release gate decisions - NFR assessment, CI setup, and framework scaffolding - P0-P3 prioritization with optional Playwright Utils and MCP integrations +## BMad Automator (Experimental) + +Automates the BMad story build loop with a pure skill bundle sourced from the separate Automator repository. + +- **Code:** `bma` +- **npm:** [`bmad-story-automator`](https://www.npmjs.com/package/bmad-story-automator) +- **GitHub:** [bmad-code-org/bmad-automator](https://github.com/bmad-code-org/bmad-automator) + +:::caution[Experimental Claude Code-only entrypoint] +BMad Automator only runs from Claude Code. It currently supports Claude Code and Codex worker sessions, and requires tmux on macOS. +::: + +**Provides:** + +- Story build-cycle automation across story creation, development, QA automation, review, and retrospective +- Resumable tmux orchestration state +- Claude Code entry skill plus Claude Code/Codex worker-session coordination + ## Community Modules Community modules and a module marketplace are coming. Check the [BMad GitHub organization](https://github.com/bmad-code-org) for updates. diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 0a5ebed5b..f79850609 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -18,6 +18,7 @@ const { Installer } = require('../tools/installer/core/installer'); const { ManifestGenerator } = require('../tools/installer/core/manifest-generator'); const { OfficialModules } = require('../tools/installer/modules/official-modules'); const { IdeManager } = require('../tools/installer/ide/manager'); +const { ExternalModuleManager } = require('../tools/installer/modules/external-manager'); const { clearCache, loadPlatformCodes } = require('../tools/installer/ide/platform-codes'); // ANSI colors @@ -85,6 +86,41 @@ async function createTestBmadFixture() { return fixtureDir; } +async function createAutomatorBmadFixture() { + const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-fixture-')); + const fixtureDir = path.join(fixtureRoot, '_bmad'); + await fs.ensureDir(path.join(fixtureDir, '_config')); + + await fs.writeFile( + path.join(fixtureDir, '_config', 'skill-manifest.csv'), + [ + 'canonicalId,name,description,module,path', + '"bmad-master","bmad-master","Minimal core skill","core","_bmad/core/bmad-master/SKILL.md"', + '"bmad-story-automator","bmad-story-automator","Automator skill","bma","_bmad/bma/bmad-story-automator/SKILL.md"', + '"bmad-story-automator-review","bmad-story-automator-review","Automator review skill","bma","_bmad/bma/bmad-story-automator-review/SKILL.md"', + '', + ].join('\n'), + ); + + const coreSkillDir = path.join(fixtureDir, 'core', 'bmad-master'); + await fs.ensureDir(coreSkillDir); + await fs.writeFile( + path.join(coreSkillDir, 'SKILL.md'), + ['---', 'name: bmad-master', 'description: Minimal core skill', '---', '', 'Core skill body.'].join('\n'), + ); + + for (const skillName of ['bmad-story-automator', 'bmad-story-automator-review']) { + const skillDir = path.join(fixtureDir, 'bma', skillName); + await fs.ensureDir(skillDir); + await fs.writeFile( + path.join(skillDir, 'SKILL.md'), + ['---', `name: ${skillName}`, 'description: Automator skill', '---', '', 'Automator body.'].join('\n'), + ); + } + + return fixtureDir; +} + async function createSkillCollisionFixture() { const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-skill-collision-')); const fixtureDir = path.join(fixtureRoot, '_bmad'); @@ -3520,6 +3556,66 @@ async function runTests() { console.log(''); + // ============================================================ + // Test Suite 45: Automator External Skill-Only Module + // ============================================================ + console.log(`${colors.yellow}Test Suite 45: Automator External Skill-Only Module${colors.reset}\n`); + + let tempProjectDir42; + let installedBmadDir42; + try { + const externalManager42 = new ExternalModuleManager(); + const automatorInfo42 = await externalManager42.getModuleByCode('bma'); + assert(automatorInfo42 !== null, 'BMad Automator is registered as an external module'); + assert(automatorInfo42.type === 'experimental', 'BMad Automator is marked experimental'); + assert(automatorInfo42.sourceRoot === 'payload/.claude/skills', 'BMad Automator uses source-root for pure skill payload'); + assert( + automatorInfo42.installTargets.length === 1 && automatorInfo42.installTargets.includes('claude-code'), + 'BMad Automator is limited to Claude Code skill installation', + ); + + tempProjectDir42 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-target-')); + installedBmadDir42 = await createAutomatorBmadFixture(); + + const ideManager42 = new IdeManager(); + await ideManager42.ensureInitialized(); + + const codexResult42 = await ideManager42.setup('codex', tempProjectDir42, installedBmadDir42, { + silent: true, + selectedModules: ['core', 'bma'], + }); + assert(codexResult42.success === true, 'Codex setup succeeds with automator module selected'); + assert( + await fs.pathExists(path.join(tempProjectDir42, '.agents', 'skills', 'bmad-master', 'SKILL.md')), + 'Codex setup still installs supported core skills', + ); + assert( + !(await fs.pathExists(path.join(tempProjectDir42, '.agents', 'skills', 'bmad-story-automator', 'SKILL.md'))), + 'Codex setup skips Claude Code-only automator skill', + ); + + const claudeResult42 = await ideManager42.setup('claude-code', tempProjectDir42, installedBmadDir42, { + silent: true, + selectedModules: ['core', 'bma'], + }); + assert(claudeResult42.success === true, 'Claude Code setup succeeds with automator module selected'); + assert( + await fs.pathExists(path.join(tempProjectDir42, '.claude', 'skills', 'bmad-story-automator', 'SKILL.md')), + 'Claude Code setup installs automator skill', + ); + assert( + await fs.pathExists(path.join(tempProjectDir42, '.claude', 'skills', 'bmad-story-automator-review', 'SKILL.md')), + 'Claude Code setup installs automator review skill', + ); + } catch (error) { + assert(false, 'Automator external skill-only module test succeeds', error.message); + } finally { + if (tempProjectDir42) await fs.remove(tempProjectDir42).catch(() => {}); + if (installedBmadDir42) await fs.remove(path.dirname(installedBmadDir42)).catch(() => {}); + } + + console.log(''); + // ============================================================ // Summary // ============================================================ diff --git a/tools/installer/README.md b/tools/installer/README.md index 9e943d689..951011c1d 100644 --- a/tools/installer/README.md +++ b/tools/installer/README.md @@ -2,9 +2,15 @@ ## Installing external repo BMad official modules -For external official modules to be discoverable during install, ensure an entry for the external repo is added to external-official-modules.yaml. +For external official modules to be discoverable during install, ensure an entry for the external repo is added to the marketplace `registry/official.yaml` source of truth. Add the same entry to `modules/registry-fallback.yaml` only when BMAD-METHOD needs a bundled fallback or a staged registry supplement. -For community modules - this will be handled in a different way. This file is only for registration of modules under the bmad-code-org. +For community modules - this is handled through the marketplace community registry. + +Use `module-definition` for conventional module repos with `module.yaml`. +Use `source-root` for pure skill bundles that should be copied directly into `_bmad//`. +This keeps the external repo as the source of truth and avoids vendoring generated skill payloads into BMAD-METHOD. + +Experimental modules can set `type: experimental` and `install-targets` to limit which IDE integrations receive their skills. ## Post-Install Notes diff --git a/tools/installer/core/manifest-generator.js b/tools/installer/core/manifest-generator.js index f7b5d0084..87c669c98 100644 --- a/tools/installer/core/manifest-generator.js +++ b/tools/installer/core/manifest-generator.js @@ -246,6 +246,9 @@ class ManifestGenerator { for (const moduleName of this.updatedModules) { const moduleYamlPath = await resolveInstalledModuleYaml(moduleName); if (!moduleYamlPath) { + if (await this._isSkillOnlyModule(moduleName)) { + continue; + } // External modules live in ~/.bmad/cache/external-modules, not src/modules. // Warn rather than silently skip so missing agent rosters don't vanish // from config.toml without notice. @@ -441,6 +444,9 @@ class ManifestGenerator { for (const moduleName of this.updatedModules) { const moduleYamlPath = await resolveInstalledModuleYaml(moduleName); if (!moduleYamlPath) { + if (await this._isSkillOnlyModule(moduleName)) { + continue; + } console.warn( `[warn] writeCentralConfig: could not locate module.yaml for '${moduleName}'. ` + `Answers from this module will default to team scope — user-scoped keys may mis-file into config.toml.`, @@ -799,6 +805,13 @@ class ManifestGenerator { return false; } + + async _isSkillOnlyModule(moduleName) { + const modulePath = path.join(this.bmadDir, moduleName); + if (!(await fs.pathExists(modulePath))) return false; + if (await fs.pathExists(path.join(modulePath, 'module.yaml'))) return false; + return this._hasSkillMdRecursive(modulePath); + } } /** diff --git a/tools/installer/ide/_config-driven.js b/tools/installer/ide/_config-driven.js index bf6fffbc5..9fbf3d3ba 100644 --- a/tools/installer/ide/_config-driven.js +++ b/tools/installer/ide/_config-driven.js @@ -145,6 +145,8 @@ class ConfigDrivenIdeSetup { this.platformConfig = platformConfig; this.installerConfig = platformConfig.installer || null; this.bmadFolderName = BMAD_FOLDER_NAME; + this.externalModuleManager = null; + this.moduleTargetCache = new Map(); // Set configDir from target_dir so detect() works this.configDir = this.installerConfig?.target_dir || null; @@ -248,9 +250,11 @@ class ConfigDrivenIdeSetup { await fs.ensureDir(targetPath); this.skillWriteTracker = new Set(); + this.skippedUnsupported = 0; const results = { skills: 0 }; results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config); + results.skippedUnsupported = this.skippedUnsupported || 0; results.skillDirectories = this.skillWriteTracker.size; if (config.commands_target_dir) { @@ -259,6 +263,7 @@ class ConfigDrivenIdeSetup { await this.printSummary(results, target_dir, options); this.skillWriteTracker = null; + this.skippedUnsupported = 0; return { success: true, results }; } @@ -433,6 +438,11 @@ class ConfigDrivenIdeSetup { const canonicalId = record.canonicalId; if (!canonicalId) continue; + if (!(await this.shouldInstallSkillRecord(record))) { + this.skippedUnsupported = (this.skippedUnsupported || 0) + 1; + continue; + } + // Derive source directory from path column // path is like "_bmad/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/SKILL.md" // Strip bmadFolderName prefix and join with bmadDir, then get dirname @@ -467,6 +477,24 @@ class ConfigDrivenIdeSetup { return count; } + async shouldInstallSkillRecord(record) { + const moduleName = record.module; + if (!moduleName) return true; + + if (this.moduleTargetCache.has(moduleName)) { + const targets = this.moduleTargetCache.get(moduleName); + return !targets || targets.includes(this.name); + } + + const { ExternalModuleManager } = require('../modules/external-manager'); + this.externalModuleManager = this.externalModuleManager || new ExternalModuleManager(); + const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleName); + const targets = moduleInfo?.installTargets || null; + this.moduleTargetCache.set(moduleName, targets); + + return !targets || targets.includes(this.name); + } + /** * Print installation summary * @param {Object} results - Installation results @@ -490,6 +518,9 @@ class ConfigDrivenIdeSetup { await prompts.log.warn(` (${cmd.writeFailures} pointer writes failed — see warnings above)`); } } + if (results.skippedUnsupported > 0) { + await prompts.log.warn(`${this.name}: skipped ${results.skippedUnsupported} skill(s) that do not support this IDE`); + } } /** diff --git a/tools/installer/modules/external-manager.js b/tools/installer/modules/external-manager.js index 7d2add4fb..702068443 100644 --- a/tools/installer/modules/external-manager.js +++ b/tools/installer/modules/external-manager.js @@ -124,18 +124,34 @@ class ExternalModuleManager { key: key || mod.name, url: mod.repository || mod.url, moduleDefinition: mod.module_definition || mod['module-definition'], + sourceRoot: mod.source_root || mod['source-root'] || mod.sourceRoot || null, code: mod.code, name: mod.display_name || mod.name, description: mod.description || '', defaultSelected: mod.default_selected === true || mod.defaultSelected === true, type: mod.type || 'bmad-org', npmPackage: mod.npm_package || mod.npmPackage || null, + installTargets: mod.install_targets || mod['install-targets'] || mod.installTargets || null, + workerTargets: mod.worker_targets || mod['worker-targets'] || mod.workerTargets || [], + requirements: mod.requirements || [], + installNote: mod.install_note || mod['install-note'] || mod.installNote || null, defaultChannel: normalizeChannelName(mod.default_channel || mod.defaultChannel) || 'stable', builtIn: mod.built_in === true, isExternal: mod.built_in !== true, }; } + async _loadFallbackModules() { + try { + const content = await fs.readFile(FALLBACK_CONFIG_PATH, 'utf8'); + const config = yaml.parse(content); + if (Array.isArray(config.modules)) return config.modules.map((mod) => this._normalizeModule(mod)); + return Object.entries(config.modules || {}).map(([key, mod]) => this._normalizeModule(mod, key)); + } catch { + return []; + } + } + /** * Get list of available modules from the registry * @returns {Array} Array of module info objects @@ -145,7 +161,14 @@ class ExternalModuleManager { // Remote format: modules is an array if (Array.isArray(config.modules)) { - return config.modules.map((mod) => this._normalizeModule(mod)); + const modules = config.modules.map((mod) => this._normalizeModule(mod)); + const seenCodes = new Set(modules.map((mod) => mod.code)); + for (const fallbackMod of await this._loadFallbackModules()) { + if (!seenCodes.has(fallbackMod.code)) { + modules.push(fallbackMod); + } + } + return modules; } // Legacy bundled format: modules is an object map @@ -489,6 +512,14 @@ class ExternalModuleManager { // Clone the external module repo const cloneDir = await this.cloneExternalModule(moduleCode, options); + if (moduleInfo.sourceRoot) { + const sourceRoot = path.join(cloneDir, moduleInfo.sourceRoot); + if (!(await fs.pathExists(sourceRoot))) { + throw new Error(`External module '${moduleCode}' source-root not found: ${moduleInfo.sourceRoot}`); + } + return sourceRoot; + } + // The module-definition specifies the path to module.yaml relative to repo root // We need to return the directory containing module.yaml const moduleDefinitionPath = moduleInfo.moduleDefinition; // e.g., 'skills/module.yaml' diff --git a/tools/installer/ui.js b/tools/installer/ui.js index 5770206ef..8dd6be3cb 100644 --- a/tools/installer/ui.js +++ b/tools/installer/ui.js @@ -259,6 +259,7 @@ class UI { } else { selectedModules = await this.selectAllModules(installedModuleIds, installedModuleVersions, channelOptions); } + await this.showSelectedExternalModuleNotes(selectedModules); // Resolve custom sources from --custom-source flag if (options.customSource) { @@ -287,6 +288,7 @@ class UI { // Get tool selection const toolSelection = await this.promptToolSelection(confirmedDirectory, options); + await this.showSelectedModuleIdeWarnings(selectedModules, toolSelection.ides); const { moduleConfigs, setOverrides } = await this.collectModuleConfigs(confirmedDirectory, selectedModules, { ...options, @@ -343,6 +345,7 @@ class UI { } else { selectedModules = await this.selectAllModules(installedModuleIds, installedModuleVersions, channelOptions); } + await this.showSelectedExternalModuleNotes(selectedModules); // Resolve custom sources from --custom-source flag if (options.customSource) { @@ -366,6 +369,7 @@ class UI { await this._interactiveChannelGate({ options, channelOptions, selectedModules }); let toolSelection = await this.promptToolSelection(confirmedDirectory, options); + await this.showSelectedModuleIdeWarnings(selectedModules, toolSelection.ides); const { moduleConfigs, setOverrides } = await this.collectModuleConfigs(confirmedDirectory, selectedModules, { ...options, channelOptions, @@ -954,6 +958,41 @@ class UI { return result; } + async showSelectedExternalModuleNotes(selectedModuleIds, externalModules = null) { + if (!externalModules) { + const externalManager = new ExternalModuleManager(); + externalModules = await externalManager.listAvailable(); + } + + const notes = externalModules + .filter((mod) => selectedModuleIds.includes(mod.code) && mod.installNote) + .map((mod) => `${mod.name}: ${mod.installNote}`); + + for (const note of notes) { + await prompts.log.warn(note); + } + } + + async showSelectedModuleIdeWarnings(selectedModuleIds, selectedIdes = []) { + const externalManager = new ExternalModuleManager(); + const externalModules = await externalManager.listAvailable(); + + for (const mod of externalModules) { + if (!selectedModuleIds.includes(mod.code) || !mod.installTargets || mod.installTargets.length === 0) { + continue; + } + + const hasInstallTarget = mod.installTargets.some((target) => selectedIdes.includes(target)); + if (!hasInstallTarget) { + await prompts.log.warn( + `${mod.name}: runnable skills are installed only for ${mod.installTargets.join( + ', ', + )}. Add that tool selection to use this module.`, + ); + } + } + } + /** * Browse and select community modules using category drill-down. * Featured/promoted modules appear at the top. From 42988f598a4eeef0b6abd99a2f0fc9cca3552fa5 Mon Sep 17 00:00:00 2001 From: bmad Date: Tue, 28 Apr 2026 22:03:45 -0300 Subject: [PATCH 3/8] fix(installer): address automator review feedback --- docs/how-to/install-bmad.md | 4 ++ test/test-installation-components.js | 53 ++++++++++++++++++++- tools/installer/core/manifest-generator.js | 14 ++++++ tools/installer/ide/_config-driven.js | 2 +- tools/installer/modules/external-manager.js | 25 ++++++++-- 5 files changed, 92 insertions(+), 6 deletions(-) diff --git a/docs/how-to/install-bmad.md b/docs/how-to/install-bmad.md index f4115f483..97ab03a25 100644 --- a/docs/how-to/install-bmad.md +++ b/docs/how-to/install-bmad.md @@ -38,6 +38,10 @@ The interactive flow asks you five things: Accept the defaults and you land on the latest stable release of every module, configured for your chosen tool. +:::caution[BMad Automator constraints] +`bma` installs runnable Automator skills only for the Claude Code entrypoint. Codex is supported as a worker target only, and worker sessions currently require `tmux` on macOS. +::: + :::tip[Just want the newest prerelease?] ```bash diff --git a/test/test-installation-components.js b/test/test-installation-components.js index f79850609..1bcdd6d40 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -116,6 +116,7 @@ async function createAutomatorBmadFixture() { path.join(skillDir, 'SKILL.md'), ['---', `name: ${skillName}`, 'description: Automator skill', '---', '', 'Automator body.'].join('\n'), ); + await fs.writeFile(path.join(skillDir, 'workflow.md'), `# ${skillName}\n\nAutomator workflow body.\n`); } return fixtureDir; @@ -3573,6 +3574,28 @@ async function runTests() { automatorInfo42.installTargets.length === 1 && automatorInfo42.installTargets.includes('claude-code'), 'BMad Automator is limited to Claude Code skill installation', ); + const normalizedInfo42 = externalManager42._normalizeModule({ + name: 'bad-shapes', + code: 'bad', + repository: 'https://example.com/bad.git', + install_targets: 'claude-code', + worker_targets: { bad: true }, + requirements: ['tmux', { bad: true }, false], + }); + assert( + Array.isArray(normalizedInfo42.installTargets) && normalizedInfo42.installTargets.includes('claude-code'), + 'External module install targets normalize scalar values to arrays', + ); + assert( + Array.isArray(normalizedInfo42.workerTargets) && normalizedInfo42.workerTargets.length === 0, + 'External module worker targets drop invalid shapes', + ); + assert( + normalizedInfo42.requirements.length === 2 && + normalizedInfo42.requirements.includes('tmux') && + normalizedInfo42.requirements.includes('false'), + 'External module requirements normalize scalar array entries', + ); tempProjectDir42 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-target-')); installedBmadDir42 = await createAutomatorBmadFixture(); @@ -3593,6 +3616,30 @@ async function runTests() { !(await fs.pathExists(path.join(tempProjectDir42, '.agents', 'skills', 'bmad-story-automator', 'SKILL.md'))), 'Codex setup skips Claude Code-only automator skill', ); + assert( + !(await fs.pathExists(path.join(tempProjectDir42, '.agents', 'skills', 'bmad-story-automator-review', 'SKILL.md'))), + 'Codex setup skips Claude Code-only automator review skill', + ); + + const escapeRoot42 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-source-root-')); + const escapeRepo42 = path.join(escapeRoot42, 'repo'); + await fs.ensureDir(escapeRepo42); + const escapeManager42 = new ExternalModuleManager(); + escapeManager42.getModuleByCode = async () => ({ + code: 'escape', + builtIn: false, + sourceRoot: '../outside', + }); + escapeManager42.cloneExternalModule = async () => escapeRepo42; + let rejectedEscapingSourceRoot42 = false; + try { + await escapeManager42.findExternalModuleSource('escape'); + } catch (error) { + rejectedEscapingSourceRoot42 = error.message.includes('source-root escapes repository'); + } finally { + await fs.remove(escapeRoot42).catch(() => {}); + } + assert(rejectedEscapingSourceRoot42, 'External module source-root cannot escape cloned repository'); const claudeResult42 = await ideManager42.setup('claude-code', tempProjectDir42, installedBmadDir42, { silent: true, @@ -3607,8 +3654,12 @@ async function runTests() { await fs.pathExists(path.join(tempProjectDir42, '.claude', 'skills', 'bmad-story-automator-review', 'SKILL.md')), 'Claude Code setup installs automator review skill', ); + assert( + await fs.pathExists(path.join(tempProjectDir42, '.claude', 'skills', 'bmad-story-automator-review', 'workflow.md')), + 'Claude Code setup copies automator workflow files', + ); } catch (error) { - assert(false, 'Automator external skill-only module test succeeds', error.message); + assert(false, `Automator external skill-only module test succeeds: ${error.message}`); } finally { if (tempProjectDir42) await fs.remove(tempProjectDir42).catch(() => {}); if (installedBmadDir42) await fs.remove(path.dirname(installedBmadDir42)).catch(() => {}); diff --git a/tools/installer/core/manifest-generator.js b/tools/installer/core/manifest-generator.js index 87c669c98..1b081f35f 100644 --- a/tools/installer/core/manifest-generator.js +++ b/tools/installer/core/manifest-generator.js @@ -810,8 +810,22 @@ class ManifestGenerator { const modulePath = path.join(this.bmadDir, moduleName); if (!(await fs.pathExists(modulePath))) return false; if (await fs.pathExists(path.join(modulePath, 'module.yaml'))) return false; + if (!(await this._moduleUsesSourceRoot(moduleName))) return false; return this._hasSkillMdRecursive(modulePath); } + + async _moduleUsesSourceRoot(moduleName) { + if (!this.sourceRootModuleCodes) { + try { + const { ExternalModuleManager } = require('../modules/external-manager'); + const externalModules = await new ExternalModuleManager().listAvailable(); + this.sourceRootModuleCodes = new Set(externalModules.filter((mod) => mod.sourceRoot).map((mod) => mod.code)); + } catch { + this.sourceRootModuleCodes = new Set(); + } + } + return this.sourceRootModuleCodes.has(moduleName); + } } /** diff --git a/tools/installer/ide/_config-driven.js b/tools/installer/ide/_config-driven.js index 9fbf3d3ba..88fbbec34 100644 --- a/tools/installer/ide/_config-driven.js +++ b/tools/installer/ide/_config-driven.js @@ -489,7 +489,7 @@ class ConfigDrivenIdeSetup { const { ExternalModuleManager } = require('../modules/external-manager'); this.externalModuleManager = this.externalModuleManager || new ExternalModuleManager(); const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleName); - const targets = moduleInfo?.installTargets || null; + const targets = moduleInfo?.installTargets?.length ? moduleInfo.installTargets : null; this.moduleTargetCache.set(moduleName, targets); return !targets || targets.includes(this.name); diff --git a/tools/installer/modules/external-manager.js b/tools/installer/modules/external-manager.js index 702068443..37f404515 100644 --- a/tools/installer/modules/external-manager.js +++ b/tools/installer/modules/external-manager.js @@ -16,6 +16,15 @@ function normalizeChannelName(raw) { return VALID_CHANNELS.has(lower) ? lower : null; } +function normalizeStringList(raw) { + if (raw == null || raw === '') return []; + const values = Array.isArray(raw) ? raw : [raw]; + return values + .filter((value) => ['string', 'number', 'boolean'].includes(typeof value)) + .map((value) => String(value).trim()) + .filter(Boolean); +} + /** * Conservative quoting for tag names passed to git commands. Tags are * user-typed (--pin) or come from the GitHub API. Only allow the semver @@ -120,6 +129,9 @@ class ExternalModuleManager { * @returns {Object} Normalized module info */ _normalizeModule(mod, key) { + const installTargets = mod.install_targets ?? mod['install-targets'] ?? mod.installTargets; + const workerTargets = mod.worker_targets ?? mod['worker-targets'] ?? mod.workerTargets; + return { key: key || mod.name, url: mod.repository || mod.url, @@ -131,9 +143,9 @@ class ExternalModuleManager { defaultSelected: mod.default_selected === true || mod.defaultSelected === true, type: mod.type || 'bmad-org', npmPackage: mod.npm_package || mod.npmPackage || null, - installTargets: mod.install_targets || mod['install-targets'] || mod.installTargets || null, - workerTargets: mod.worker_targets || mod['worker-targets'] || mod.workerTargets || [], - requirements: mod.requirements || [], + installTargets: normalizeStringList(installTargets), + workerTargets: normalizeStringList(workerTargets), + requirements: normalizeStringList(mod.requirements), installNote: mod.install_note || mod['install-note'] || mod.installNote || null, defaultChannel: normalizeChannelName(mod.default_channel || mod.defaultChannel) || 'stable', builtIn: mod.built_in === true, @@ -513,7 +525,12 @@ class ExternalModuleManager { const cloneDir = await this.cloneExternalModule(moduleCode, options); if (moduleInfo.sourceRoot) { - const sourceRoot = path.join(cloneDir, moduleInfo.sourceRoot); + const repoRoot = path.resolve(cloneDir); + const sourceRoot = path.resolve(repoRoot, moduleInfo.sourceRoot); + const relativeSourceRoot = path.relative(repoRoot, sourceRoot); + if (relativeSourceRoot === '..' || relativeSourceRoot.startsWith(`..${path.sep}`) || path.isAbsolute(relativeSourceRoot)) { + throw new Error(`External module '${moduleCode}' source-root escapes repository: ${moduleInfo.sourceRoot}`); + } if (!(await fs.pathExists(sourceRoot))) { throw new Error(`External module '${moduleCode}' source-root not found: ${moduleInfo.sourceRoot}`); } From f1b9679232d85be4494b93b90244d48a5b32e13a Mon Sep 17 00:00:00 2001 From: bmad Date: Tue, 5 May 2026 02:17:45 -0300 Subject: [PATCH 4/8] fix(installer): guard automator registry lookups --- tools/installer/ide/_config-driven.js | 11 +++++++++-- tools/installer/ui.js | 18 ++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/tools/installer/ide/_config-driven.js b/tools/installer/ide/_config-driven.js index 88fbbec34..d169b7201 100644 --- a/tools/installer/ide/_config-driven.js +++ b/tools/installer/ide/_config-driven.js @@ -488,8 +488,15 @@ class ConfigDrivenIdeSetup { const { ExternalModuleManager } = require('../modules/external-manager'); this.externalModuleManager = this.externalModuleManager || new ExternalModuleManager(); - const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleName); - const targets = moduleInfo?.installTargets?.length ? moduleInfo.installTargets : null; + let targets = null; + try { + const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleName); + targets = moduleInfo?.installTargets?.length ? moduleInfo.installTargets : null; + } catch (error) { + await prompts.log.warn( + `ExternalModuleManager.getModuleByCode failed for module '${moduleName}' while installing ${this.name}; installing skill anyway. ${error.message}`, + ); + } this.moduleTargetCache.set(moduleName, targets); return !targets || targets.includes(this.name); diff --git a/tools/installer/ui.js b/tools/installer/ui.js index 8dd6be3cb..c3e0db27c 100644 --- a/tools/installer/ui.js +++ b/tools/installer/ui.js @@ -961,7 +961,14 @@ class UI { async showSelectedExternalModuleNotes(selectedModuleIds, externalModules = null) { if (!externalModules) { const externalManager = new ExternalModuleManager(); - externalModules = await externalManager.listAvailable(); + try { + externalModules = await externalManager.listAvailable(); + } catch (error) { + await prompts.log.warn( + `ExternalModuleManager.listAvailable failed while loading module notes; continuing without external module notes. ${error.message}`, + ); + externalModules = []; + } } const notes = externalModules @@ -975,7 +982,14 @@ class UI { async showSelectedModuleIdeWarnings(selectedModuleIds, selectedIdes = []) { const externalManager = new ExternalModuleManager(); - const externalModules = await externalManager.listAvailable(); + let externalModules = []; + try { + externalModules = await externalManager.listAvailable(); + } catch (error) { + await prompts.log.warn( + `ExternalModuleManager.listAvailable failed while loading IDE compatibility warnings; continuing without external module warnings. ${error.message}`, + ); + } for (const mod of externalModules) { if (!selectedModuleIds.includes(mod.code) || !mod.installTargets || mod.installTargets.length === 0) { From c792ef169d5c5d3217dc9bb43b8ce3de0339abe0 Mon Sep 17 00:00:00 2001 From: Dicky Moore Date: Wed, 6 May 2026 07:49:03 -0300 Subject: [PATCH 5/8] fix(installer): include automator runtime from source-root Co-authored-by: bmad --- test/test-installation-components.js | 42 +++++++++++++++++++++ tools/installer/modules/official-modules.js | 24 ++++++++++++ 2 files changed, 66 insertions(+) diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 1bcdd6d40..284b05c07 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -122,6 +122,27 @@ async function createAutomatorBmadFixture() { return fixtureDir; } +async function createAutomatorSourceRootFixture() { + const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-source-')); + const sourceRoot = path.join(repoRoot, 'payload', '.claude', 'skills'); + + for (const skillName of ['bmad-story-automator', 'bmad-story-automator-review']) { + const skillDir = path.join(sourceRoot, skillName); + await fs.ensureDir(skillDir); + await fs.writeFile( + path.join(skillDir, 'SKILL.md'), + ['---', `name: ${skillName}`, 'description: Automator skill', '---', '', 'Automator body.'].join('\n'), + ); + } + + await fs.ensureDir(path.join(repoRoot, 'source', 'scripts')); + await fs.writeFile(path.join(repoRoot, 'source', 'scripts', 'story-automator'), '#!/usr/bin/env bash\n'); + await fs.ensureDir(path.join(repoRoot, 'source', 'src', 'story_automator')); + await fs.writeFile(path.join(repoRoot, 'source', 'src', 'story_automator', 'cli.py'), 'def main():\n return 0\n'); + + return { repoRoot, sourceRoot }; +} + async function createSkillCollisionFixture() { const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-skill-collision-')); const fixtureDir = path.join(fixtureRoot, '_bmad'); @@ -3564,6 +3585,8 @@ async function runTests() { let tempProjectDir42; let installedBmadDir42; + let automatorSourceFixture42; + let runtimeTargetRoot42; try { const externalManager42 = new ExternalModuleManager(); const automatorInfo42 = await externalManager42.getModuleByCode('bma'); @@ -3597,6 +3620,23 @@ async function runTests() { 'External module requirements normalize scalar array entries', ); + automatorSourceFixture42 = await createAutomatorSourceRootFixture(); + runtimeTargetRoot42 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-runtime-target-')); + const runtimeBmadDir42 = path.join(runtimeTargetRoot42, '_bmad'); + const officialModules42 = new OfficialModules(); + officialModules42.findModuleSource = async () => automatorSourceFixture42.sourceRoot; + await officialModules42.install('bma', runtimeBmadDir42, null, { skipModuleInstaller: true, silent: true }); + assert( + await fs.pathExists(path.join(runtimeBmadDir42, 'bma', 'bmad-story-automator', 'scripts', 'story-automator')), + 'BMad Automator source-root install includes runtime helper', + ); + assert( + await fs.pathExists(path.join(runtimeBmadDir42, 'bma', 'bmad-story-automator', 'src', 'story_automator', 'cli.py')), + 'BMad Automator source-root install includes Python runtime source', + ); + await fs.remove(runtimeTargetRoot42).catch(() => {}); + runtimeTargetRoot42 = null; + tempProjectDir42 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-target-')); installedBmadDir42 = await createAutomatorBmadFixture(); @@ -3663,6 +3703,8 @@ async function runTests() { } finally { if (tempProjectDir42) await fs.remove(tempProjectDir42).catch(() => {}); if (installedBmadDir42) await fs.remove(path.dirname(installedBmadDir42)).catch(() => {}); + if (automatorSourceFixture42) await fs.remove(automatorSourceFixture42.repoRoot).catch(() => {}); + if (runtimeTargetRoot42) await fs.remove(runtimeTargetRoot42).catch(() => {}); } console.log(''); diff --git a/tools/installer/modules/official-modules.js b/tools/installer/modules/official-modules.js index 615daba86..9b4a9106d 100644 --- a/tools/installer/modules/official-modules.js +++ b/tools/installer/modules/official-modules.js @@ -301,6 +301,7 @@ class OfficialModules { } await this.copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback, options.moduleConfig); + await this.copyAutomatorRuntimeIfNeeded(moduleName, sourcePath, targetPath, fileTrackingCallback); if (!options.skipModuleInstaller) { await this.createModuleDirectories(moduleName, bmadDir, options); @@ -572,6 +573,29 @@ class OfficialModules { } } + async copyAutomatorRuntimeIfNeeded(moduleName, sourcePath, targetPath, fileTrackingCallback = null) { + if (moduleName !== 'bma') return; + + const storyTarget = path.join(targetPath, 'bmad-story-automator'); + if (!(await fs.pathExists(path.join(storyTarget, 'SKILL.md')))) return; + + const repoRoot = path.resolve(sourcePath, '..', '..', '..'); + const runtimeRoot = path.join(repoRoot, 'source'); + const runtimeParts = [ + ['scripts', 'scripts'], + ['src', 'src'], + ]; + + for (const [sourceRel, targetRel] of runtimeParts) { + const sourceDir = path.join(runtimeRoot, sourceRel); + const targetDir = path.join(storyTarget, targetRel); + if (!(await fs.pathExists(sourceDir))) { + throw new Error(`BMad Automator runtime source missing: source/${sourceRel}`); + } + await this.copyModuleWithFiltering(sourceDir, targetDir, fileTrackingCallback); + } + } + /** * Create directories declared in module.yaml's `directories` key * This replaces the security-risky module installer pattern with declarative config From a89b405008f1708c3c4cee1afaee8929089d1b84 Mon Sep 17 00:00:00 2001 From: bmad Date: Wed, 6 May 2026 20:48:31 -0300 Subject: [PATCH 6/8] fix(installer): default automator to next channel --- docs/how-to/install-bmad.md | 2 ++ docs/reference/modules.md | 1 + test/test-installation-components.js | 1 + tools/installer/modules/registry-fallback.yaml | 2 +- 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/how-to/install-bmad.md b/docs/how-to/install-bmad.md index 97ab03a25..4e80ce3bb 100644 --- a/docs/how-to/install-bmad.md +++ b/docs/how-to/install-bmad.md @@ -40,6 +40,8 @@ Accept the defaults and you land on the latest stable release of every module, c :::caution[BMad Automator constraints] `bma` installs runnable Automator skills only for the Claude Code entrypoint. Codex is supported as a worker target only, and worker sessions currently require `tmux` on macOS. + +While experimental, `bma` defaults to the `next` channel so installs pick up the latest Automator payload compatibility fixes. ::: :::tip[Just want the newest prerelease?] diff --git a/docs/reference/modules.md b/docs/reference/modules.md index c26d02816..0c307f1f0 100644 --- a/docs/reference/modules.md +++ b/docs/reference/modules.md @@ -78,6 +78,7 @@ Automates the BMad story build loop with a pure skill bundle sourced from the se - **Code:** `bma` - **npm:** [`bmad-story-automator`](https://www.npmjs.com/package/bmad-story-automator) - **GitHub:** [bmad-code-org/bmad-automator](https://github.com/bmad-code-org/bmad-automator) +- **Default channel:** `next` while experimental, so installs receive the latest Automator payload compatibility fixes. :::caution[Experimental Claude Code-only entrypoint] BMad Automator only runs from Claude Code. It currently supports Claude Code and Codex worker sessions, and requires tmux on macOS. diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 284b05c07..723f471e3 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -3593,6 +3593,7 @@ async function runTests() { assert(automatorInfo42 !== null, 'BMad Automator is registered as an external module'); assert(automatorInfo42.type === 'experimental', 'BMad Automator is marked experimental'); assert(automatorInfo42.sourceRoot === 'payload/.claude/skills', 'BMad Automator uses source-root for pure skill payload'); + assert(automatorInfo42.defaultChannel === 'next', 'BMad Automator defaults to next for latest payload compatibility fixes'); assert( automatorInfo42.installTargets.length === 1 && automatorInfo42.installTargets.includes('claude-code'), 'BMad Automator is limited to Claude Code skill installation', diff --git a/tools/installer/modules/registry-fallback.yaml b/tools/installer/modules/registry-fallback.yaml index f9ad2e564..a988602f4 100644 --- a/tools/installer/modules/registry-fallback.yaml +++ b/tools/installer/modules/registry-fallback.yaml @@ -60,7 +60,7 @@ modules: defaultSelected: false type: experimental npmPackage: bmad-story-automator - default_channel: stable + default_channel: next install-targets: - claude-code worker-targets: From 68f6bf223aaeb826b32e72e2f22c469fe38c9433 Mon Sep 17 00:00:00 2001 From: bmad Date: Thu, 7 May 2026 21:36:36 -0300 Subject: [PATCH 7/8] fix(installer): rename automator module code to baut --- docs/how-to/install-bmad.md | 8 ++++---- docs/reference/modules.md | 2 +- test/test-installation-components.js | 18 +++++++++--------- tools/installer/modules/official-modules.js | 2 +- tools/installer/modules/registry-fallback.yaml | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/how-to/install-bmad.md b/docs/how-to/install-bmad.md index 4e80ce3bb..4a5a08847 100644 --- a/docs/how-to/install-bmad.md +++ b/docs/how-to/install-bmad.md @@ -31,7 +31,7 @@ npx bmad-method install The interactive flow asks you five things: 1. Installation directory (defaults to the current working directory) -2. Which modules to install (checkboxes for core, bmm, bmb, cis, gds, tea, bma) +2. Which modules to install (checkboxes for core, bmm, bmb, cis, gds, tea, baut) 3. **"Ready to install (all stable)?"** — Yes accepts the latest released tag for every external module 4. Which AI tools/IDEs to integrate with (claude-code, cursor, and others) 5. Per-module config (name, language, output folder) @@ -39,9 +39,9 @@ The interactive flow asks you five things: Accept the defaults and you land on the latest stable release of every module, configured for your chosen tool. :::caution[BMad Automator constraints] -`bma` installs runnable Automator skills only for the Claude Code entrypoint. Codex is supported as a worker target only, and worker sessions currently require `tmux` on macOS. +`baut` installs runnable Automator skills only for the Claude Code entrypoint. Codex is supported as a worker target only, and worker sessions currently require `tmux` on macOS. -While experimental, `bma` defaults to the `next` channel so installs pick up the latest Automator payload compatibility fixes. +While experimental, `baut` defaults to the `next` channel so installs pick up the latest Automator payload compatibility fixes. ::: :::tip[Just want the newest prerelease?] @@ -59,7 +59,7 @@ Two independent axes control what ends up on disk. ### Axis 1: external module channels -Every external module — bmb, cis, gds, tea, bma, and any community module — installs on one of three channels: +Every external module — bmb, cis, gds, tea, baut, and any community module — installs on one of three channels: | Channel | What gets installed | Who picks this | | ------------------ | ---------------------------------------------------------------------------- | --------------------------------------- | diff --git a/docs/reference/modules.md b/docs/reference/modules.md index 0c307f1f0..43a15d704 100644 --- a/docs/reference/modules.md +++ b/docs/reference/modules.md @@ -75,7 +75,7 @@ Enterprise-grade test strategy, automation guidance, and release gate decisions Automates the BMad story build loop with a pure skill bundle sourced from the separate Automator repository. -- **Code:** `bma` +- **Code:** `baut` - **npm:** [`bmad-story-automator`](https://www.npmjs.com/package/bmad-story-automator) - **GitHub:** [bmad-code-org/bmad-automator](https://github.com/bmad-code-org/bmad-automator) - **Default channel:** `next` while experimental, so installs receive the latest Automator payload compatibility fixes. diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 723f471e3..503fcfb3a 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -96,8 +96,8 @@ async function createAutomatorBmadFixture() { [ 'canonicalId,name,description,module,path', '"bmad-master","bmad-master","Minimal core skill","core","_bmad/core/bmad-master/SKILL.md"', - '"bmad-story-automator","bmad-story-automator","Automator skill","bma","_bmad/bma/bmad-story-automator/SKILL.md"', - '"bmad-story-automator-review","bmad-story-automator-review","Automator review skill","bma","_bmad/bma/bmad-story-automator-review/SKILL.md"', + '"bmad-story-automator","bmad-story-automator","Automator skill","baut","_bmad/baut/bmad-story-automator/SKILL.md"', + '"bmad-story-automator-review","bmad-story-automator-review","Automator review skill","baut","_bmad/baut/bmad-story-automator-review/SKILL.md"', '', ].join('\n'), ); @@ -110,7 +110,7 @@ async function createAutomatorBmadFixture() { ); for (const skillName of ['bmad-story-automator', 'bmad-story-automator-review']) { - const skillDir = path.join(fixtureDir, 'bma', skillName); + const skillDir = path.join(fixtureDir, 'baut', skillName); await fs.ensureDir(skillDir); await fs.writeFile( path.join(skillDir, 'SKILL.md'), @@ -3589,7 +3589,7 @@ async function runTests() { let runtimeTargetRoot42; try { const externalManager42 = new ExternalModuleManager(); - const automatorInfo42 = await externalManager42.getModuleByCode('bma'); + const automatorInfo42 = await externalManager42.getModuleByCode('baut'); assert(automatorInfo42 !== null, 'BMad Automator is registered as an external module'); assert(automatorInfo42.type === 'experimental', 'BMad Automator is marked experimental'); assert(automatorInfo42.sourceRoot === 'payload/.claude/skills', 'BMad Automator uses source-root for pure skill payload'); @@ -3626,13 +3626,13 @@ async function runTests() { const runtimeBmadDir42 = path.join(runtimeTargetRoot42, '_bmad'); const officialModules42 = new OfficialModules(); officialModules42.findModuleSource = async () => automatorSourceFixture42.sourceRoot; - await officialModules42.install('bma', runtimeBmadDir42, null, { skipModuleInstaller: true, silent: true }); + await officialModules42.install('baut', runtimeBmadDir42, null, { skipModuleInstaller: true, silent: true }); assert( - await fs.pathExists(path.join(runtimeBmadDir42, 'bma', 'bmad-story-automator', 'scripts', 'story-automator')), + await fs.pathExists(path.join(runtimeBmadDir42, 'baut', 'bmad-story-automator', 'scripts', 'story-automator')), 'BMad Automator source-root install includes runtime helper', ); assert( - await fs.pathExists(path.join(runtimeBmadDir42, 'bma', 'bmad-story-automator', 'src', 'story_automator', 'cli.py')), + await fs.pathExists(path.join(runtimeBmadDir42, 'baut', 'bmad-story-automator', 'src', 'story_automator', 'cli.py')), 'BMad Automator source-root install includes Python runtime source', ); await fs.remove(runtimeTargetRoot42).catch(() => {}); @@ -3646,7 +3646,7 @@ async function runTests() { const codexResult42 = await ideManager42.setup('codex', tempProjectDir42, installedBmadDir42, { silent: true, - selectedModules: ['core', 'bma'], + selectedModules: ['core', 'baut'], }); assert(codexResult42.success === true, 'Codex setup succeeds with automator module selected'); assert( @@ -3684,7 +3684,7 @@ async function runTests() { const claudeResult42 = await ideManager42.setup('claude-code', tempProjectDir42, installedBmadDir42, { silent: true, - selectedModules: ['core', 'bma'], + selectedModules: ['core', 'baut'], }); assert(claudeResult42.success === true, 'Claude Code setup succeeds with automator module selected'); assert( diff --git a/tools/installer/modules/official-modules.js b/tools/installer/modules/official-modules.js index 9b4a9106d..b93d3c774 100644 --- a/tools/installer/modules/official-modules.js +++ b/tools/installer/modules/official-modules.js @@ -574,7 +574,7 @@ class OfficialModules { } async copyAutomatorRuntimeIfNeeded(moduleName, sourcePath, targetPath, fileTrackingCallback = null) { - if (moduleName !== 'bma') return; + if (moduleName !== 'baut') return; const storyTarget = path.join(targetPath, 'bmad-story-automator'); if (!(await fs.pathExists(path.join(storyTarget, 'SKILL.md')))) return; diff --git a/tools/installer/modules/registry-fallback.yaml b/tools/installer/modules/registry-fallback.yaml index a988602f4..941560e2b 100644 --- a/tools/installer/modules/registry-fallback.yaml +++ b/tools/installer/modules/registry-fallback.yaml @@ -54,7 +54,7 @@ modules: bmad-automator: url: https://github.com/bmad-code-org/bmad-automator source-root: payload/.claude/skills - code: bma + code: baut name: "BMad Automator (Experimental)" description: "Experimental pure-skill story automation. Runs only from Claude Code; supports Claude Code and Codex worker sessions; requires tmux on macOS." defaultSelected: false From 550ea6a8ddae3ec076c51c4c20d7fb36f1805fa6 Mon Sep 17 00:00:00 2001 From: bmad Date: Fri, 8 May 2026 07:37:14 -0300 Subject: [PATCH 8/8] fix(installer): add automator skill module --- docs/how-to/install-bmad.md | 10 +- docs/reference/modules.md | 19 --- test/test-installation-components.js | 136 +++--------------- tools/installer/README.md | 10 +- tools/installer/core/manifest-generator.js | 27 ---- tools/installer/ide/_config-driven.js | 38 ----- tools/installer/modules/external-manager.js | 36 +---- tools/installer/modules/official-modules.js | 24 ---- .../installer/modules/registry-fallback.yaml | 13 +- tools/installer/ui.js | 53 ------- 10 files changed, 24 insertions(+), 342 deletions(-) diff --git a/docs/how-to/install-bmad.md b/docs/how-to/install-bmad.md index 4a5a08847..224704a47 100644 --- a/docs/how-to/install-bmad.md +++ b/docs/how-to/install-bmad.md @@ -31,19 +31,13 @@ npx bmad-method install The interactive flow asks you five things: 1. Installation directory (defaults to the current working directory) -2. Which modules to install (checkboxes for core, bmm, bmb, cis, gds, tea, baut) +2. Which modules to install (checkboxes for core, bmm, bmb, cis, gds, tea) 3. **"Ready to install (all stable)?"** — Yes accepts the latest released tag for every external module 4. Which AI tools/IDEs to integrate with (claude-code, cursor, and others) 5. Per-module config (name, language, output folder) Accept the defaults and you land on the latest stable release of every module, configured for your chosen tool. -:::caution[BMad Automator constraints] -`baut` installs runnable Automator skills only for the Claude Code entrypoint. Codex is supported as a worker target only, and worker sessions currently require `tmux` on macOS. - -While experimental, `baut` defaults to the `next` channel so installs pick up the latest Automator payload compatibility fixes. -::: - :::tip[Just want the newest prerelease?] ```bash @@ -59,7 +53,7 @@ Two independent axes control what ends up on disk. ### Axis 1: external module channels -Every external module — bmb, cis, gds, tea, baut, and any community module — installs on one of three channels: +Every external module — bmb, cis, gds, tea, and any community module — installs on one of three channels: | Channel | What gets installed | Who picks this | | ------------------ | ---------------------------------------------------------------------------- | --------------------------------------- | diff --git a/docs/reference/modules.md b/docs/reference/modules.md index 43a15d704..6bdc64190 100644 --- a/docs/reference/modules.md +++ b/docs/reference/modules.md @@ -71,25 +71,6 @@ Enterprise-grade test strategy, automation guidance, and release gate decisions - NFR assessment, CI setup, and framework scaffolding - P0-P3 prioritization with optional Playwright Utils and MCP integrations -## BMad Automator (Experimental) - -Automates the BMad story build loop with a pure skill bundle sourced from the separate Automator repository. - -- **Code:** `baut` -- **npm:** [`bmad-story-automator`](https://www.npmjs.com/package/bmad-story-automator) -- **GitHub:** [bmad-code-org/bmad-automator](https://github.com/bmad-code-org/bmad-automator) -- **Default channel:** `next` while experimental, so installs receive the latest Automator payload compatibility fixes. - -:::caution[Experimental Claude Code-only entrypoint] -BMad Automator only runs from Claude Code. It currently supports Claude Code and Codex worker sessions, and requires tmux on macOS. -::: - -**Provides:** - -- Story build-cycle automation across story creation, development, QA automation, review, and retrospective -- Resumable tmux orchestration state -- Claude Code entry skill plus Claude Code/Codex worker-session coordination - ## Community Modules Community modules and a module marketplace are coming. Check the [BMad GitHub organization](https://github.com/bmad-code-org) for updates. diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 503fcfb3a..db715eadc 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -86,45 +86,9 @@ async function createTestBmadFixture() { return fixtureDir; } -async function createAutomatorBmadFixture() { - const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-fixture-')); - const fixtureDir = path.join(fixtureRoot, '_bmad'); - await fs.ensureDir(path.join(fixtureDir, '_config')); - - await fs.writeFile( - path.join(fixtureDir, '_config', 'skill-manifest.csv'), - [ - 'canonicalId,name,description,module,path', - '"bmad-master","bmad-master","Minimal core skill","core","_bmad/core/bmad-master/SKILL.md"', - '"bmad-story-automator","bmad-story-automator","Automator skill","baut","_bmad/baut/bmad-story-automator/SKILL.md"', - '"bmad-story-automator-review","bmad-story-automator-review","Automator review skill","baut","_bmad/baut/bmad-story-automator-review/SKILL.md"', - '', - ].join('\n'), - ); - - const coreSkillDir = path.join(fixtureDir, 'core', 'bmad-master'); - await fs.ensureDir(coreSkillDir); - await fs.writeFile( - path.join(coreSkillDir, 'SKILL.md'), - ['---', 'name: bmad-master', 'description: Minimal core skill', '---', '', 'Core skill body.'].join('\n'), - ); - - for (const skillName of ['bmad-story-automator', 'bmad-story-automator-review']) { - const skillDir = path.join(fixtureDir, 'baut', skillName); - await fs.ensureDir(skillDir); - await fs.writeFile( - path.join(skillDir, 'SKILL.md'), - ['---', `name: ${skillName}`, 'description: Automator skill', '---', '', 'Automator body.'].join('\n'), - ); - await fs.writeFile(path.join(skillDir, 'workflow.md'), `# ${skillName}\n\nAutomator workflow body.\n`); - } - - return fixtureDir; -} - async function createAutomatorSourceRootFixture() { const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-source-')); - const sourceRoot = path.join(repoRoot, 'payload', '.claude', 'skills'); + const sourceRoot = path.join(repoRoot, 'skills'); for (const skillName of ['bmad-story-automator', 'bmad-story-automator-review']) { const skillDir = path.join(sourceRoot, skillName); @@ -135,10 +99,11 @@ async function createAutomatorSourceRootFixture() { ); } - await fs.ensureDir(path.join(repoRoot, 'source', 'scripts')); - await fs.writeFile(path.join(repoRoot, 'source', 'scripts', 'story-automator'), '#!/usr/bin/env bash\n'); - await fs.ensureDir(path.join(repoRoot, 'source', 'src', 'story_automator')); - await fs.writeFile(path.join(repoRoot, 'source', 'src', 'story_automator', 'cli.py'), 'def main():\n return 0\n'); + const storySkillDir = path.join(sourceRoot, 'bmad-story-automator'); + await fs.ensureDir(path.join(storySkillDir, 'scripts')); + await fs.writeFile(path.join(storySkillDir, 'scripts', 'story-automator'), '#!/usr/bin/env bash\n'); + await fs.ensureDir(path.join(storySkillDir, 'src', 'story_automator')); + await fs.writeFile(path.join(storySkillDir, 'src', 'story_automator', 'cli.py'), 'def main():\n return 0\n'); return { repoRoot, sourceRoot }; } @@ -3583,43 +3548,22 @@ async function runTests() { // ============================================================ console.log(`${colors.yellow}Test Suite 45: Automator External Skill-Only Module${colors.reset}\n`); - let tempProjectDir42; - let installedBmadDir42; let automatorSourceFixture42; let runtimeTargetRoot42; try { + const yaml42 = require('yaml'); + const fallbackConfig42 = yaml42.parse( + await fs.readFile(path.join(__dirname, '..', 'tools', 'installer', 'modules', 'registry-fallback.yaml'), 'utf8'), + ); + const automatorEntry42 = fallbackConfig42.modules['bmad-automator']; + assert(automatorEntry42?.code === 'baut', 'BMad Automator fallback registry code is baut'); + assert(automatorEntry42?.['source-root'] === 'skills', 'BMad Automator fallback registry points at root skills'); + const externalManager42 = new ExternalModuleManager(); - const automatorInfo42 = await externalManager42.getModuleByCode('baut'); - assert(automatorInfo42 !== null, 'BMad Automator is registered as an external module'); + const automatorInfo42 = externalManager42._normalizeModule(automatorEntry42); assert(automatorInfo42.type === 'experimental', 'BMad Automator is marked experimental'); - assert(automatorInfo42.sourceRoot === 'payload/.claude/skills', 'BMad Automator uses source-root for pure skill payload'); + assert(automatorInfo42.sourceRoot === 'skills', 'BMad Automator uses root skills source-root for pure skill payload'); assert(automatorInfo42.defaultChannel === 'next', 'BMad Automator defaults to next for latest payload compatibility fixes'); - assert( - automatorInfo42.installTargets.length === 1 && automatorInfo42.installTargets.includes('claude-code'), - 'BMad Automator is limited to Claude Code skill installation', - ); - const normalizedInfo42 = externalManager42._normalizeModule({ - name: 'bad-shapes', - code: 'bad', - repository: 'https://example.com/bad.git', - install_targets: 'claude-code', - worker_targets: { bad: true }, - requirements: ['tmux', { bad: true }, false], - }); - assert( - Array.isArray(normalizedInfo42.installTargets) && normalizedInfo42.installTargets.includes('claude-code'), - 'External module install targets normalize scalar values to arrays', - ); - assert( - Array.isArray(normalizedInfo42.workerTargets) && normalizedInfo42.workerTargets.length === 0, - 'External module worker targets drop invalid shapes', - ); - assert( - normalizedInfo42.requirements.length === 2 && - normalizedInfo42.requirements.includes('tmux') && - normalizedInfo42.requirements.includes('false'), - 'External module requirements normalize scalar array entries', - ); automatorSourceFixture42 = await createAutomatorSourceRootFixture(); runtimeTargetRoot42 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-runtime-target-')); @@ -3629,39 +3573,15 @@ async function runTests() { await officialModules42.install('baut', runtimeBmadDir42, null, { skipModuleInstaller: true, silent: true }); assert( await fs.pathExists(path.join(runtimeBmadDir42, 'baut', 'bmad-story-automator', 'scripts', 'story-automator')), - 'BMad Automator source-root install includes runtime helper', + 'BMad Automator self-contained skill install includes runtime helper', ); assert( await fs.pathExists(path.join(runtimeBmadDir42, 'baut', 'bmad-story-automator', 'src', 'story_automator', 'cli.py')), - 'BMad Automator source-root install includes Python runtime source', + 'BMad Automator self-contained skill install includes Python runtime source', ); await fs.remove(runtimeTargetRoot42).catch(() => {}); runtimeTargetRoot42 = null; - tempProjectDir42 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-target-')); - installedBmadDir42 = await createAutomatorBmadFixture(); - - const ideManager42 = new IdeManager(); - await ideManager42.ensureInitialized(); - - const codexResult42 = await ideManager42.setup('codex', tempProjectDir42, installedBmadDir42, { - silent: true, - selectedModules: ['core', 'baut'], - }); - assert(codexResult42.success === true, 'Codex setup succeeds with automator module selected'); - assert( - await fs.pathExists(path.join(tempProjectDir42, '.agents', 'skills', 'bmad-master', 'SKILL.md')), - 'Codex setup still installs supported core skills', - ); - assert( - !(await fs.pathExists(path.join(tempProjectDir42, '.agents', 'skills', 'bmad-story-automator', 'SKILL.md'))), - 'Codex setup skips Claude Code-only automator skill', - ); - assert( - !(await fs.pathExists(path.join(tempProjectDir42, '.agents', 'skills', 'bmad-story-automator-review', 'SKILL.md'))), - 'Codex setup skips Claude Code-only automator review skill', - ); - const escapeRoot42 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-source-root-')); const escapeRepo42 = path.join(escapeRoot42, 'repo'); await fs.ensureDir(escapeRepo42); @@ -3681,29 +3601,9 @@ async function runTests() { await fs.remove(escapeRoot42).catch(() => {}); } assert(rejectedEscapingSourceRoot42, 'External module source-root cannot escape cloned repository'); - - const claudeResult42 = await ideManager42.setup('claude-code', tempProjectDir42, installedBmadDir42, { - silent: true, - selectedModules: ['core', 'baut'], - }); - assert(claudeResult42.success === true, 'Claude Code setup succeeds with automator module selected'); - assert( - await fs.pathExists(path.join(tempProjectDir42, '.claude', 'skills', 'bmad-story-automator', 'SKILL.md')), - 'Claude Code setup installs automator skill', - ); - assert( - await fs.pathExists(path.join(tempProjectDir42, '.claude', 'skills', 'bmad-story-automator-review', 'SKILL.md')), - 'Claude Code setup installs automator review skill', - ); - assert( - await fs.pathExists(path.join(tempProjectDir42, '.claude', 'skills', 'bmad-story-automator-review', 'workflow.md')), - 'Claude Code setup copies automator workflow files', - ); } catch (error) { assert(false, `Automator external skill-only module test succeeds: ${error.message}`); } finally { - if (tempProjectDir42) await fs.remove(tempProjectDir42).catch(() => {}); - if (installedBmadDir42) await fs.remove(path.dirname(installedBmadDir42)).catch(() => {}); if (automatorSourceFixture42) await fs.remove(automatorSourceFixture42.repoRoot).catch(() => {}); if (runtimeTargetRoot42) await fs.remove(runtimeTargetRoot42).catch(() => {}); } diff --git a/tools/installer/README.md b/tools/installer/README.md index 951011c1d..9e943d689 100644 --- a/tools/installer/README.md +++ b/tools/installer/README.md @@ -2,15 +2,9 @@ ## Installing external repo BMad official modules -For external official modules to be discoverable during install, ensure an entry for the external repo is added to the marketplace `registry/official.yaml` source of truth. Add the same entry to `modules/registry-fallback.yaml` only when BMAD-METHOD needs a bundled fallback or a staged registry supplement. +For external official modules to be discoverable during install, ensure an entry for the external repo is added to external-official-modules.yaml. -For community modules - this is handled through the marketplace community registry. - -Use `module-definition` for conventional module repos with `module.yaml`. -Use `source-root` for pure skill bundles that should be copied directly into `_bmad//`. -This keeps the external repo as the source of truth and avoids vendoring generated skill payloads into BMAD-METHOD. - -Experimental modules can set `type: experimental` and `install-targets` to limit which IDE integrations receive their skills. +For community modules - this will be handled in a different way. This file is only for registration of modules under the bmad-code-org. ## Post-Install Notes diff --git a/tools/installer/core/manifest-generator.js b/tools/installer/core/manifest-generator.js index 1b081f35f..f7b5d0084 100644 --- a/tools/installer/core/manifest-generator.js +++ b/tools/installer/core/manifest-generator.js @@ -246,9 +246,6 @@ class ManifestGenerator { for (const moduleName of this.updatedModules) { const moduleYamlPath = await resolveInstalledModuleYaml(moduleName); if (!moduleYamlPath) { - if (await this._isSkillOnlyModule(moduleName)) { - continue; - } // External modules live in ~/.bmad/cache/external-modules, not src/modules. // Warn rather than silently skip so missing agent rosters don't vanish // from config.toml without notice. @@ -444,9 +441,6 @@ class ManifestGenerator { for (const moduleName of this.updatedModules) { const moduleYamlPath = await resolveInstalledModuleYaml(moduleName); if (!moduleYamlPath) { - if (await this._isSkillOnlyModule(moduleName)) { - continue; - } console.warn( `[warn] writeCentralConfig: could not locate module.yaml for '${moduleName}'. ` + `Answers from this module will default to team scope — user-scoped keys may mis-file into config.toml.`, @@ -805,27 +799,6 @@ class ManifestGenerator { return false; } - - async _isSkillOnlyModule(moduleName) { - const modulePath = path.join(this.bmadDir, moduleName); - if (!(await fs.pathExists(modulePath))) return false; - if (await fs.pathExists(path.join(modulePath, 'module.yaml'))) return false; - if (!(await this._moduleUsesSourceRoot(moduleName))) return false; - return this._hasSkillMdRecursive(modulePath); - } - - async _moduleUsesSourceRoot(moduleName) { - if (!this.sourceRootModuleCodes) { - try { - const { ExternalModuleManager } = require('../modules/external-manager'); - const externalModules = await new ExternalModuleManager().listAvailable(); - this.sourceRootModuleCodes = new Set(externalModules.filter((mod) => mod.sourceRoot).map((mod) => mod.code)); - } catch { - this.sourceRootModuleCodes = new Set(); - } - } - return this.sourceRootModuleCodes.has(moduleName); - } } /** diff --git a/tools/installer/ide/_config-driven.js b/tools/installer/ide/_config-driven.js index d169b7201..bf6fffbc5 100644 --- a/tools/installer/ide/_config-driven.js +++ b/tools/installer/ide/_config-driven.js @@ -145,8 +145,6 @@ class ConfigDrivenIdeSetup { this.platformConfig = platformConfig; this.installerConfig = platformConfig.installer || null; this.bmadFolderName = BMAD_FOLDER_NAME; - this.externalModuleManager = null; - this.moduleTargetCache = new Map(); // Set configDir from target_dir so detect() works this.configDir = this.installerConfig?.target_dir || null; @@ -250,11 +248,9 @@ class ConfigDrivenIdeSetup { await fs.ensureDir(targetPath); this.skillWriteTracker = new Set(); - this.skippedUnsupported = 0; const results = { skills: 0 }; results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config); - results.skippedUnsupported = this.skippedUnsupported || 0; results.skillDirectories = this.skillWriteTracker.size; if (config.commands_target_dir) { @@ -263,7 +259,6 @@ class ConfigDrivenIdeSetup { await this.printSummary(results, target_dir, options); this.skillWriteTracker = null; - this.skippedUnsupported = 0; return { success: true, results }; } @@ -438,11 +433,6 @@ class ConfigDrivenIdeSetup { const canonicalId = record.canonicalId; if (!canonicalId) continue; - if (!(await this.shouldInstallSkillRecord(record))) { - this.skippedUnsupported = (this.skippedUnsupported || 0) + 1; - continue; - } - // Derive source directory from path column // path is like "_bmad/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/SKILL.md" // Strip bmadFolderName prefix and join with bmadDir, then get dirname @@ -477,31 +467,6 @@ class ConfigDrivenIdeSetup { return count; } - async shouldInstallSkillRecord(record) { - const moduleName = record.module; - if (!moduleName) return true; - - if (this.moduleTargetCache.has(moduleName)) { - const targets = this.moduleTargetCache.get(moduleName); - return !targets || targets.includes(this.name); - } - - const { ExternalModuleManager } = require('../modules/external-manager'); - this.externalModuleManager = this.externalModuleManager || new ExternalModuleManager(); - let targets = null; - try { - const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleName); - targets = moduleInfo?.installTargets?.length ? moduleInfo.installTargets : null; - } catch (error) { - await prompts.log.warn( - `ExternalModuleManager.getModuleByCode failed for module '${moduleName}' while installing ${this.name}; installing skill anyway. ${error.message}`, - ); - } - this.moduleTargetCache.set(moduleName, targets); - - return !targets || targets.includes(this.name); - } - /** * Print installation summary * @param {Object} results - Installation results @@ -525,9 +490,6 @@ class ConfigDrivenIdeSetup { await prompts.log.warn(` (${cmd.writeFailures} pointer writes failed — see warnings above)`); } } - if (results.skippedUnsupported > 0) { - await prompts.log.warn(`${this.name}: skipped ${results.skippedUnsupported} skill(s) that do not support this IDE`); - } } /** diff --git a/tools/installer/modules/external-manager.js b/tools/installer/modules/external-manager.js index 37f404515..e666f8612 100644 --- a/tools/installer/modules/external-manager.js +++ b/tools/installer/modules/external-manager.js @@ -16,15 +16,6 @@ function normalizeChannelName(raw) { return VALID_CHANNELS.has(lower) ? lower : null; } -function normalizeStringList(raw) { - if (raw == null || raw === '') return []; - const values = Array.isArray(raw) ? raw : [raw]; - return values - .filter((value) => ['string', 'number', 'boolean'].includes(typeof value)) - .map((value) => String(value).trim()) - .filter(Boolean); -} - /** * Conservative quoting for tag names passed to git commands. Tags are * user-typed (--pin) or come from the GitHub API. Only allow the semver @@ -129,9 +120,6 @@ class ExternalModuleManager { * @returns {Object} Normalized module info */ _normalizeModule(mod, key) { - const installTargets = mod.install_targets ?? mod['install-targets'] ?? mod.installTargets; - const workerTargets = mod.worker_targets ?? mod['worker-targets'] ?? mod.workerTargets; - return { key: key || mod.name, url: mod.repository || mod.url, @@ -143,27 +131,12 @@ class ExternalModuleManager { defaultSelected: mod.default_selected === true || mod.defaultSelected === true, type: mod.type || 'bmad-org', npmPackage: mod.npm_package || mod.npmPackage || null, - installTargets: normalizeStringList(installTargets), - workerTargets: normalizeStringList(workerTargets), - requirements: normalizeStringList(mod.requirements), - installNote: mod.install_note || mod['install-note'] || mod.installNote || null, defaultChannel: normalizeChannelName(mod.default_channel || mod.defaultChannel) || 'stable', builtIn: mod.built_in === true, isExternal: mod.built_in !== true, }; } - async _loadFallbackModules() { - try { - const content = await fs.readFile(FALLBACK_CONFIG_PATH, 'utf8'); - const config = yaml.parse(content); - if (Array.isArray(config.modules)) return config.modules.map((mod) => this._normalizeModule(mod)); - return Object.entries(config.modules || {}).map(([key, mod]) => this._normalizeModule(mod, key)); - } catch { - return []; - } - } - /** * Get list of available modules from the registry * @returns {Array} Array of module info objects @@ -173,14 +146,7 @@ class ExternalModuleManager { // Remote format: modules is an array if (Array.isArray(config.modules)) { - const modules = config.modules.map((mod) => this._normalizeModule(mod)); - const seenCodes = new Set(modules.map((mod) => mod.code)); - for (const fallbackMod of await this._loadFallbackModules()) { - if (!seenCodes.has(fallbackMod.code)) { - modules.push(fallbackMod); - } - } - return modules; + return config.modules.map((mod) => this._normalizeModule(mod)); } // Legacy bundled format: modules is an object map diff --git a/tools/installer/modules/official-modules.js b/tools/installer/modules/official-modules.js index b93d3c774..615daba86 100644 --- a/tools/installer/modules/official-modules.js +++ b/tools/installer/modules/official-modules.js @@ -301,7 +301,6 @@ class OfficialModules { } await this.copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback, options.moduleConfig); - await this.copyAutomatorRuntimeIfNeeded(moduleName, sourcePath, targetPath, fileTrackingCallback); if (!options.skipModuleInstaller) { await this.createModuleDirectories(moduleName, bmadDir, options); @@ -573,29 +572,6 @@ class OfficialModules { } } - async copyAutomatorRuntimeIfNeeded(moduleName, sourcePath, targetPath, fileTrackingCallback = null) { - if (moduleName !== 'baut') return; - - const storyTarget = path.join(targetPath, 'bmad-story-automator'); - if (!(await fs.pathExists(path.join(storyTarget, 'SKILL.md')))) return; - - const repoRoot = path.resolve(sourcePath, '..', '..', '..'); - const runtimeRoot = path.join(repoRoot, 'source'); - const runtimeParts = [ - ['scripts', 'scripts'], - ['src', 'src'], - ]; - - for (const [sourceRel, targetRel] of runtimeParts) { - const sourceDir = path.join(runtimeRoot, sourceRel); - const targetDir = path.join(storyTarget, targetRel); - if (!(await fs.pathExists(sourceDir))) { - throw new Error(`BMad Automator runtime source missing: source/${sourceRel}`); - } - await this.copyModuleWithFiltering(sourceDir, targetDir, fileTrackingCallback); - } - } - /** * Create directories declared in module.yaml's `directories` key * This replaces the security-risky module installer pattern with declarative config diff --git a/tools/installer/modules/registry-fallback.yaml b/tools/installer/modules/registry-fallback.yaml index 941560e2b..139045de0 100644 --- a/tools/installer/modules/registry-fallback.yaml +++ b/tools/installer/modules/registry-fallback.yaml @@ -53,7 +53,7 @@ modules: bmad-automator: url: https://github.com/bmad-code-org/bmad-automator - source-root: payload/.claude/skills + source-root: skills code: baut name: "BMad Automator (Experimental)" description: "Experimental pure-skill story automation. Runs only from Claude Code; supports Claude Code and Codex worker sessions; requires tmux on macOS." @@ -61,14 +61,3 @@ modules: type: experimental npmPackage: bmad-story-automator default_channel: next - install-targets: - - claude-code - worker-targets: - - claude-code - - codex - requirements: - - Claude Code entrypoint - - Claude Code or Codex worker sessions - - tmux - - macOS - install-note: "Experimental: BMad Automator only works from Claude Code. It currently supports Claude Code and Codex worker sessions, and requires tmux on macOS." diff --git a/tools/installer/ui.js b/tools/installer/ui.js index c3e0db27c..5770206ef 100644 --- a/tools/installer/ui.js +++ b/tools/installer/ui.js @@ -259,7 +259,6 @@ class UI { } else { selectedModules = await this.selectAllModules(installedModuleIds, installedModuleVersions, channelOptions); } - await this.showSelectedExternalModuleNotes(selectedModules); // Resolve custom sources from --custom-source flag if (options.customSource) { @@ -288,7 +287,6 @@ class UI { // Get tool selection const toolSelection = await this.promptToolSelection(confirmedDirectory, options); - await this.showSelectedModuleIdeWarnings(selectedModules, toolSelection.ides); const { moduleConfigs, setOverrides } = await this.collectModuleConfigs(confirmedDirectory, selectedModules, { ...options, @@ -345,7 +343,6 @@ class UI { } else { selectedModules = await this.selectAllModules(installedModuleIds, installedModuleVersions, channelOptions); } - await this.showSelectedExternalModuleNotes(selectedModules); // Resolve custom sources from --custom-source flag if (options.customSource) { @@ -369,7 +366,6 @@ class UI { await this._interactiveChannelGate({ options, channelOptions, selectedModules }); let toolSelection = await this.promptToolSelection(confirmedDirectory, options); - await this.showSelectedModuleIdeWarnings(selectedModules, toolSelection.ides); const { moduleConfigs, setOverrides } = await this.collectModuleConfigs(confirmedDirectory, selectedModules, { ...options, channelOptions, @@ -958,55 +954,6 @@ class UI { return result; } - async showSelectedExternalModuleNotes(selectedModuleIds, externalModules = null) { - if (!externalModules) { - const externalManager = new ExternalModuleManager(); - try { - externalModules = await externalManager.listAvailable(); - } catch (error) { - await prompts.log.warn( - `ExternalModuleManager.listAvailable failed while loading module notes; continuing without external module notes. ${error.message}`, - ); - externalModules = []; - } - } - - const notes = externalModules - .filter((mod) => selectedModuleIds.includes(mod.code) && mod.installNote) - .map((mod) => `${mod.name}: ${mod.installNote}`); - - for (const note of notes) { - await prompts.log.warn(note); - } - } - - async showSelectedModuleIdeWarnings(selectedModuleIds, selectedIdes = []) { - const externalManager = new ExternalModuleManager(); - let externalModules = []; - try { - externalModules = await externalManager.listAvailable(); - } catch (error) { - await prompts.log.warn( - `ExternalModuleManager.listAvailable failed while loading IDE compatibility warnings; continuing without external module warnings. ${error.message}`, - ); - } - - for (const mod of externalModules) { - if (!selectedModuleIds.includes(mod.code) || !mod.installTargets || mod.installTargets.length === 0) { - continue; - } - - const hasInstallTarget = mod.installTargets.some((target) => selectedIdes.includes(target)); - if (!hasInstallTarget) { - await prompts.log.warn( - `${mod.name}: runnable skills are installed only for ${mod.installTargets.join( - ', ', - )}. Add that tool selection to use this module.`, - ); - } - } - } - /** * Browse and select community modules using category drill-down. * Featured/promoted modules appear at the top.