Compare commits

...

13 Commits

Author SHA1 Message Date
Alex Verkhovsky 63a03b6945
Merge branch 'main' into docs/branching-strategy 2026-02-12 12:29:07 -07:00
Pablo LION c8ca083316
fix: remove unnecessary quotes and fix grammar in bmad-master principles (#1600)
- Remove YAML quotes from principles list item (consistency with other agents)
- Add missing comma: "at runtime never" → "at runtime, never" (run-on fix)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 12:10:33 -06:00
Pablo LION 5b79330f72
fix: fix typos in tech-writer and ux-designer agent definitions (#1599)
- tech-writer: "1000s works" → "1000s of words" (idiom typo)
- ux-designer: "PRovides" → "Provides", "that" → "than" (typos in description)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 12:09:12 -06:00
Alex Verkhovsky b4d118c897
fix: remove output_folder/story_dir aliases, flatten variables sections (#1608)
* fix: remove output_folder/story_dir aliases, flatten variables sections (#1602)

Drop pointless alias variables (output_folder, story_dir, story_directory)
from Phase 4, Quick Flow, and QA workflows. Replace all references with
the canonical {implementation_artifacts} or {planning_artifacts} variables.

Also flatten unnecessary `variables:` YAML nesting in all affected
workflow.yaml files — the workflow engine treats all keys as top-level,
so the nesting added complexity with no semantic value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add missing config declarations, remove remaining aliases and dead variables

- Add missing document_output_language and user_skill_level to create-story
  (referenced in instructions.xml but never declared)
- Remove retrospectives_folder alias, replace with canonical implementation_artifacts
- Remove unused sprint_status and duplicate validation alias from correct-course
- Remove unused date, planning_artifacts, tracking_system from sprint-status

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review findings and rename shadowed output_folder variable

- Fix single/double brace mismatch for {implementation_artifacts} in
  create-story instructions.xml (F1)
- Remove escaped asterisks in glob patterns in retrospective
  instructions.md (F2)
- Eliminate redundant {config_source} re-resolution for story_location
  in sprint-planning workflow.yaml (F5)
- Add explicit instruction to discover previous_story_num by scanning
  artifacts instead of leaving it undefined (#7)
- Rename output_folder to project_knowledge in document-project
  workflows to stop shadowing the canonical core config variable (#13)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use plural retrospectives in previous retro search instructions

The glob pattern can match multiple retrospective files for the same
epic (e.g., partial mid-sprint retro and full completion retro). Use
plural "retrospectives" to make clear the LLM should load all matches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 10:59:18 -06:00
Alex Verkhovsky 3e35057b89
fix: auto-discover PRD in validate-prd instead of always asking (#1619)
When only one PRD exists in planning_artifacts, use it automatically
instead of prompting the user for its path. Still asks when multiple
PRDs are found or falls back to manual input when none are discovered.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 18:02:23 -06:00
Alex Verkhovsky 0659aac02c
fix(bmm): add missing project-context references to workflows (#1597)
* fix(bmm): add missing project-context references to workflows

correct-course, retrospective, and sprint-status workflows were missing
project_context entirely. quick-spec referenced it in step files but not
in the initialization sequence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(bmm): add explicit project-context load instructions to workflows

Add Load project_context if exists action to instruction files for
correct-course, retrospective, sprint-status, and sprint-planning.
The workflow.yaml declarations alone do not cause the file to be loaded;
the instruction files must explicitly reference it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 09:50:00 -06:00
Davor Racic 0bf8e0edfb
fix: installer fixes and non-interactive mode improvements (#1612)
* fix: remove duplicate 'recompilation complete' message in compile-agents output

The spinner.stop() in compileAgents() already displays this message,
so the additional log.success() call produced a redundant line.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove deselected IDE configurations during installer update

When updating an existing installation, IDEs that were previously
configured but unchecked in the new selection are now detected and
cleaned up after user confirmation, mirroring the existing module
removal flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove unreachable return statements after process.exit calls

Remove 4 dead `return;` statements that immediately follow `process.exit(0)`
calls in install.js. Since process.exit() terminates the process, the
subsequent return statements are unreachable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: batch directory creation log messages for cleaner installer output

Collect all created directory names during module directory setup and
emit them as a single log message instead of one per directory. This
eliminates the excessive blank-line spacing that @clack/prompts adds
between individual log.message() calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: consolidate directory creation output across all modules

Move directory creation logging from ModuleManager.createModuleDirectories
into the installer caller. The method now returns created directory info
instead of logging directly, allowing the installer to batch all module
directories into a single log message under one spinner. Also adds spacing
before the final "Installation complete" status line.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: propagate skipPrompts flag to IDE collectConfiguration calls

The --yes flag (skipPrompts) was not being passed through to IDE handler
collectConfiguration() calls, causing the Codex installer to hang on its
interactive prompt in non-interactive mode (CI/CD, --yes flag).

- Add skipPrompts parameter to collectToolConfigurations() and forward it
  to handler.collectConfiguration() options
- Add early return in CodexSetup.collectConfiguration() when skipPrompts
  is true, defaulting to global install location
- Guard IDE removal confirmation prompt with skipPrompts check, defaulting
  to preserve existing configs (consistent with prompt default: false)

Fixes #1610

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address code review findings from PR #1612

- Restore saved IDE configurations when removal is cancelled or skipped
  in non-interactive mode, preventing silent config downgrade (e.g.,
  Codex 'project' install reverting to 'global')
- Short-circuit IDE removal block when skipPrompts is true to eliminate
  unnecessary warning/cancelled log output in --yes mode
- Add null guard on config.ides before pushing re-added IDEs
- Return empty result object from createModuleDirectories early exits
  instead of undefined for a uniform return contract

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: guard remaining unguarded prompts with skipPrompts for non-interactive mode

Add skipPrompts guards to 4 remaining unguarded interactive prompts in
installer.js that caused hangs in non-interactive mode (--yes flag):
- Module removal confirmation: preserves modules by default
- Update action selection: defaults to 'update'
- Custom module missing sources: keeps all modules
- Custom module delete confirmation: unreachable via early return

Additional robustness fixes:
- Defensive type-check before .trim() on prompt result (symbol guard)
- Console.log suppression scoped per-IDE instead of global try/finally
- process.exit flush via setImmediate for legacy v4 exit path
- JSDoc updated for new skipPrompts parameter

Follows established pattern from commit f967fdde (IDE skipPrompts guards).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: migrate installer console.warn/error calls to prompts.log API

Replace all user-facing console.warn(), console.error(), and console.log()
calls with their prompts.log.* equivalents for consistent @clack/prompts
styled output across the installer codebase.

- Migrate 13 console.warn/error calls across 5 files to prompts.log.*
- Convert moduleLogger callbacks to async prompts.log.* in installer.js
- Replace blank-line console.log() with prompts.log.message('')
- Add prompts import to 5 files that lacked it
- Remove redundant "Warning:" prefixes (prompts.log.warn adds styling)
- Remove redundant local prompts require in installer.js L454
- Add missing await on logger.log call in manager.js
- Debug-gated console.log calls in manifest-generator.js left as-is

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: replace installer spinner with tasks() progress and consolidate summary

Replace the serial spinner during non-interactive install phases with
@clack/prompts tasks() component for clearer progress visibility. The
install flow now uses two tasks() blocks (pre-IDE and post-IDE) with
the IDE setup retaining its own spinner since it may prompt.

- Refactor install phases into tasks() callbacks with message() updates
- Merge next-steps content into the "BMAD is ready to use!" summary note
- Fix spinner.stop() tense: "Reviewing..." → past tense ("reviewed")
- Move directory creation output after tasks() to avoid breaking progress
- Remove dead showInstallSummary() from ui.js
- Harden error handling: try/finally on IDE spinner, safe catch block

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: consolidate install messages into single start banner

Combine start and end marketing messages into one banner shown before
installation begins. Remove the post-install end message and its
displayEndMessage() calls — the install summary note now serves as
the final output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: apply prettier formatting to install command files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: rename "Module installers" label to "Module directories" in summary

The old script-based module installer pattern was replaced with
declarative directory creation, but the task title and summary label
were never updated to reflect that change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add batch module configuration gateway with express/customize modes

Replace N individual "Accept Defaults?" confirm prompts with a single
select gateway ("Express Setup" vs "Customize"). When customizing, a
multiselect shows only modules with configurable options. All others
silently receive defaults via spinner progress.

- Add scanModuleSchemas() to pre-scan module metadata and display names
- Add select/multiselect gateway in collectAllConfigurations()
- Replace per-module confirm with modulesToCustomize Set check
- Suppress UI output during silent default config via _silentConfig flag
- Reorder installer tasks: module dirs before config generation
- Add resolution null guards for edge-case safety
- Cache ModuleManager instance via _getModuleManager() for reuse

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: move module directories instead of creating new ones on path change

When users modify a module's directory path during installer update/modify,
the old directory is now moved to the new location instead of creating an
empty directory while leaving the old one (with its documents) behind.

Includes: cross-device fs.move, error handling with fallback, path
normalization, parent/child path guard, and warning when both dirs exist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add --yes flag guards to all unguarded prompts in update/modify path

Guard 5 interactive prompts in ui.js that caused the installer to hang
when --yes flag was used with existing installations. Add skipPrompts
field to 3 return objects that were missing it, ensuring installer.js
downstream guards work correctly for all update paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 19:32:38 -06:00
jheyworth 36c21dbada
feat: custom GitHub Copilot installer handler (#1606)
* feat: add custom GitHub Copilot installer handler

Adds a dedicated GitHub Copilot handler that generates:
- Agent files with .agent.md extension and enriched descriptions
- Prompt files (.prompt.md) for workflows, tasks, and agent activators
- copilot-instructions.md with project config and agent reference table

Replaces the generic config-driven handler with a custom one that
properly supports Copilot's agent/prompt file conventions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: escape YAML descriptions and preserve user copilot-instructions

- Escape single quotes in YAML frontmatter descriptions across all prompt
  generators (createWorkflowPromptContent, createTechWriterPromptContent,
  createAgentActivatorPromptContent) to match createAgentContent behavior
- Make copilot-instructions.md non-destructive using BMAD markers
  (<!-- BMAD:START --> / <!-- BMAD:END -->) to preserve user content
- On cleanup, only remove content between markers; skip files without markers
- Back up existing unmarked files before overwriting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add error handling for CSV read/parse in loadAgentManifest and loadBmadHelp

Wrap file read and csv.parse in try/catch blocks so malformed or
unreadable CSV files gracefully degrade instead of aborting setup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use specific detection paths instead of .github configDir

Set configDir to null and use detectionPaths with
.github/copilot-instructions.md and .github/agents/ so the base
detect() doesn't false-positive on every GitHub repo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add comments explaining hardcoded bmm/config.yaml in prompts

Clarify that bmm/config.yaml is safe to hardcode in generated prompt
content because these prompts are only created when bmm module data
(bmad-help.csv, agent artifacts) exists.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: guard against missing workflow-file in bmad-help.csv entries

Skip entries where workflow-file is empty/undefined to prevent
workflowFile.endsWith() from throwing during prompt generation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: guard escapeYamlSingleQuote against undefined input

Default to empty string when value is undefined/null to prevent
replaceAll from throwing on missing CSV fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: preserve customised tool permissions across reinstalls

Before this change, reinstalling would overwrite any user-customised
tools arrays in agent and prompt frontmatter with the hardcoded default.

Now the installer reads existing tool permissions from .agent.md and
.prompt.md files before cleanup, and re-applies them to the regenerated
files. Falls back to the default ['read', 'edit', 'search', 'execute']
for new files or files without prior customisation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: prevent cleanup from stripping copilot-instructions.md markers before generation

The cleanup() method was removing the BMAD marker section from
copilot-instructions.md, leaving user content without markers.
generateCopilotInstructions() then treated the markerless file as
legacy, backed it up, and overwrote user content.

Fix: remove the copilot-instructions.md block from cleanup() entirely.
generateCopilotInstructions() already handles marker-based replacement
in a single read-modify-write pass that correctly preserves user content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: update manager.js comments to include github-copilot.js

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: make agent capabilities data-driven via agent YAML metadata

Replace the hardcoded getAgentCapabilities() map with a data-driven
pipeline. Capabilities are now defined in each .agent.yaml source file,
compiled into the XML output, extracted into agent-manifest.csv by the
manifest generator, and read by the GitHub Copilot handler at install
time. New agents automatically get their capabilities without code
changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use this.bmadFolderName instead of hardcoded _bmad in template paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 13:41:11 -06:00
Brian Madison 2754d66042 6.0.0-Beta.8 2026-02-08 20:06:40 -06:00
Brian Madison 60c3477a3a chore: remove unused changelog-social skill 2026-02-08 20:06:17 -06:00
Brian Madison 622b627430 docs: update CHANGELOG for v6.0.0-Beta.8 2026-02-08 20:05:47 -06:00
Brian Madison c563cef0c2 refactor: replace module installer scripts with declarative directories config
Removes the security-risky _module-installer pattern (code execution at
install time) in favor of a declarative `directories` key in module.yaml.
The main installer now handles directory creation centrally based on this
config, eliminating per-module installer.js scripts and their CJS/ESM issues.

Changes:
- Delete src/bmm/_module-installer/installer.js
- Delete src/core/_module-installer/installer.js
- Add `directories` key to src/bmm/module.yaml
- Rename runModuleInstaller() -> createModuleDirectories()
- Remove _module-installer from ESLint overrides
- Remove _module-installer from file-ref validator skip dirs
2026-02-08 19:21:48 -06:00
Davor Racic 90ea3cbed7
Minor installer fixes (#1590)
* fix: remove redundant "None" skip option from module selection

The "None - Skip module installation" option was unnecessary since
core is always locked/selected, satisfying the required constraint.
Users can simply press Enter with only core selected to skip modules.
Also removes dead code: selectModules(), getExternalModuleChoices(),
and selectExternalModules() methods that were never called.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: support ESM and .cjs module installers in ModuleManager

Module installer loading now handles three cases:
- .cjs files loaded via require() (always CommonJS regardless of package type)
- .js files loaded via dynamic import() (works for both CJS and ESM)
- CJS default export unwrapped automatically for consistent API

This fixes errors when external modules set "type":"module" in their
package.json. Those modules must still rename installer.js to
installer.cjs if it uses require() internally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address code review findings from PR #1590

- Filter 'core' from CLI --modules in update path for consistency
- Update selectAllModules() JSDoc to reflect core exclusion
- Fix ESM default-export unwrap to handle function/class exports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clarify module post-install script errors as non-fatal warnings

Change error display from log.error to log.warn and explain that the
module was installed successfully — only the optional post-install
script could not run. Prevents users from thinking the module
installation itself failed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress non-fatal module post-install script errors

Post-install scripts fail due to CJS/ESM incompatibility but module
files are already copied successfully. Silently catch the error instead
of showing a warning that alarms users into thinking installation failed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove redundant modules and tools lines from install summary

The checkmark list already shows each installed module and IDE tool.
Keep only the install path and file-warning lines in the summary footer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Brian <bmadcode@gmail.com>
2026-02-08 15:41:51 -06:00
65 changed files with 1851 additions and 922 deletions

View File

@ -1,5 +1,58 @@
# Changelog # Changelog
## [6.0.0-Beta.8]
**Release: February 8, 2026**
### 🌟 Key Highlights
1. **Non-Interactive Installation** — Full CI/CD support with 10 new CLI flags for automated deployments
2. **Complete @clack/prompts Migration** — Unified CLI experience with consolidated installer output
3. **CSV File Reference Validation** — Extended Layer 1 validator to catch broken workflow references in CSV files
4. **Kiro IDE Support** — Standardized config-driven installation, replacing custom installer
### 🎁 Features
* **Non-Interactive Installation** — Added `--directory`, `--modules`, `--tools`, `--custom-content`, `--user-name`, `--communication-language`, `--document-output-language`, `--output-folder`, and `-y/--yes` flags for CI/CD automation (#1520)
* **CSV File Reference Validation** — Extended validator to scan `.csv` files for broken workflow references, checking 501 references across 212 files (#1573)
* **Kiro IDE Support** — Replaced broken custom installer with config-driven templates using `#[[file:...]]` syntax and `inclusion: manual` frontmatter (#1589)
* **OpenCode Template Consolidation** — Combined split templates with `mode: primary` frontmatter for Tab-switching support, fixing agent discovery (#1556)
* **Modules Reference Page** — Added official external modules reference documentation (#1540)
### 🐛 Bug Fixes
* **Installer Streamlining** — Removed "None - Skip module installation" option, eliminated ~100 lines of dead code, and added ESM/.cjs support for module installers (#1590)
* **CodeRabbit Workflow** — Changed `pull_request` to `pull_request_target` to fix 403 errors and enable reviews on fork PRs (#1583)
* **Party Mode Return Protocol** — Added RETURN PROTOCOL to prevent lost-in-the-middle failures after Party Mode completes (#1569)
* **Spacebar Toggle** — Fixed SPACE key not working in autocomplete multiselect prompts for tool/IDE selection (#1557)
* **OpenCode Agent Routing** — Fixed agents installing to wrong directory by adding `targets` array for routing `.opencode/agent/` vs `.opencode/command/` (#1549)
* **Technical Research Workflow** — Fixed step-05 routing to step-06 and corrected `stepsCompleted` values (#1547)
* **Forbidden Variable Removal** — Removed `workflow_path` variable from 16 workflow step files (#1546)
* **Kilo Installer** — Fixed YAML formatting issues by trimming activation header and converting to yaml.parse/stringify (#1537)
* **bmad-help** — Now reads project-specific docs and respects `communication_language` setting (#1535)
* **Cache Errors** — Removed `--prefer-offline` npm flag to prevent stale cache errors during installation (#1531)
### ♻️ Refactoring
* **Complete @clack/prompts Migration** — Migrated 24 files from legacy libraries (ora, chalk, boxen, figlet, etc.), replaced ~100 console.log+chalk calls, consolidated installer output to single spinner, and removed 5 dependencies (#1586)
* **Downloads Page Removal** — Removed downloads page, bundle generation, and archiver dependency in favor of GitHub's native archives (#1577)
* **Workflow Verb Standardization** — Replaced "invoke/run" with "load and follow/load" in review workflow prompts (#1570)
* **Documentation Language** — Renamed "brownfield" to "established projects" and flattened directory structure for accessibility (#1539)
### 📚 Documentation
* **Comprehensive Site Review** — Fixed broken directory tree diagram, corrected grammar/capitalization, added SEO descriptions, and reordered how-to guides (#1578)
* **SEO Metadata** — Added description front matter to 9 documentation pages for search engine optimization (#1566)
* **PR Template** — Added pull request template for consistent PR descriptions (#1554)
* **Manual Release Cleanup** — Removed broken manual-release workflow and related scripts (#1576)
### 🔧 Maintenance
* **Dual-Mode AI Code Review** — Configured Augment Code (audit mode) and CodeRabbit (adversarial mode) for improved code quality (#1511)
* **Package-Lock Sync** — Cleaned up 471 lines of orphaned dependencies after archiver removal (#1580)
---
## [6.0.0-Beta.7] ## [6.0.0-Beta.7]
**Release: February 4, 2026** **Release: February 4, 2026**

View File

@ -114,17 +114,6 @@ export default [
}, },
}, },
// Module installer scripts use CommonJS for compatibility
{
files: ['**/_module-installer/**/*.js'],
rules: {
// Allow CommonJS patterns for installer scripts
'unicorn/prefer-module': 'off',
'n/no-missing-require': 'off',
'n/no-unpublished-require': 'off',
},
},
// ESLint config file should not be checked for publish-related Node rules // ESLint config file should not be checked for publish-related Node rules
{ {
files: ['eslint.config.mjs'], files: ['eslint.config.mjs'],

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "bmad-method", "name": "bmad-method",
"version": "6.0.0-Beta.7", "version": "6.0.0-Beta.8",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "bmad-method", "name": "bmad-method",
"version": "6.0.0-Beta.7", "version": "6.0.0-Beta.8",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@clack/core": "^1.0.0", "@clack/core": "^1.0.0",

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "bmad-method", "name": "bmad-method",
"version": "6.0.0-Beta.7", "version": "6.0.0-Beta.8",
"description": "Breakthrough Method of Agile AI-driven Development", "description": "Breakthrough Method of Agile AI-driven Development",
"keywords": [ "keywords": [
"agile", "agile",

View File

@ -1,48 +0,0 @@
const fs = require('fs-extra');
const path = require('node:path');
const chalk = require('chalk');
// Directories to create from config
const DIRECTORIES = ['output_folder', 'planning_artifacts', 'implementation_artifacts'];
/**
* BMM Module Installer
* Creates output directories configured in module config
*
* @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from module.yaml
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
* @param {Object} options.logger - Logger instance for output
* @returns {Promise<boolean>} - Success status
*/
async function install(options) {
const { projectRoot, config, logger } = options;
try {
logger.log(chalk.blue('🚀 Installing BMM Module...'));
// Create configured directories
for (const configKey of DIRECTORIES) {
const configValue = config[configKey];
if (!configValue) continue;
const dirPath = configValue.replace('{project-root}/', '');
const fullPath = path.join(projectRoot, dirPath);
if (!(await fs.pathExists(fullPath))) {
const dirName = configKey.replace('_', ' ');
logger.log(chalk.yellow(`Creating ${dirName} directory: ${dirPath}`));
await fs.ensureDir(fullPath);
}
}
logger.log(chalk.green('✓ BMM Module installation complete'));
return true;
} catch (error) {
logger.error(chalk.red(`Error installing BMM module: ${error.message}`));
return false;
}
}
module.exports = { install };

View File

@ -5,6 +5,7 @@ agent:
title: Business Analyst title: Business Analyst
icon: 📊 icon: 📊
module: bmm module: bmm
capabilities: "market research, competitive analysis, requirements elicitation, domain expertise"
hasSidecar: false hasSidecar: false
persona: persona:

View File

@ -7,6 +7,7 @@ agent:
title: Architect title: Architect
icon: 🏗️ icon: 🏗️
module: bmm module: bmm
capabilities: "distributed systems, cloud infrastructure, API design, scalable patterns"
hasSidecar: false hasSidecar: false
persona: persona:

View File

@ -7,6 +7,7 @@ agent:
title: Developer Agent title: Developer Agent
icon: 💻 icon: 💻
module: bmm module: bmm
capabilities: "story execution, test-driven development, code implementation"
hasSidecar: false hasSidecar: false
persona: persona:

View File

@ -5,6 +5,7 @@ agent:
title: Product Manager title: Product Manager
icon: 📋 icon: 📋
module: bmm module: bmm
capabilities: "PRD creation, requirements discovery, stakeholder alignment, user interviews"
hasSidecar: false hasSidecar: false
persona: persona:

View File

@ -5,6 +5,7 @@ agent:
title: QA Engineer title: QA Engineer
icon: 🧪 icon: 🧪
module: bmm module: bmm
capabilities: "test automation, API testing, E2E testing, coverage analysis"
hasSidecar: false hasSidecar: false
persona: persona:

View File

@ -7,6 +7,7 @@ agent:
title: Quick Flow Solo Dev title: Quick Flow Solo Dev
icon: 🚀 icon: 🚀
module: bmm module: bmm
capabilities: "rapid spec creation, lean implementation, minimum ceremony"
hasSidecar: false hasSidecar: false
persona: persona:

View File

@ -7,6 +7,7 @@ agent:
title: Scrum Master title: Scrum Master
icon: 🏃 icon: 🏃
module: bmm module: bmm
capabilities: "sprint planning, story preparation, agile ceremonies, backlog management"
hasSidecar: false hasSidecar: false
persona: persona:

View File

@ -7,6 +7,7 @@ agent:
title: Technical Writer title: Technical Writer
icon: 📚 icon: 📚
module: bmm module: bmm
capabilities: "documentation, Mermaid diagrams, standards compliance, concept explanation"
hasSidecar: true hasSidecar: true
persona: persona:
@ -15,7 +16,7 @@ agent:
communication_style: "Patient educator who explains like teaching a friend. Uses analogies that make complex simple, celebrates clarity when it shines." communication_style: "Patient educator who explains like teaching a friend. Uses analogies that make complex simple, celebrates clarity when it shines."
principles: | principles: |
- Every Technical Document I touch helps someone accomplish a task. Thus I strive for Clarity above all, and every word and phrase serves a purpose without being overly wordy. - Every Technical Document I touch helps someone accomplish a task. Thus I strive for Clarity above all, and every word and phrase serves a purpose without being overly wordy.
- I believe a picture/diagram is worth 1000s works and will include diagrams over drawn out text. - I believe a picture/diagram is worth 1000s of words and will include diagrams over drawn out text.
- I understand the intended audience or will clarify with the user so I know when to simplify vs when to be detailed. - I understand the intended audience or will clarify with the user so I know when to simplify vs when to be detailed.
- I will always strive to follow `_bmad/_memory/tech-writer-sidecar/documentation-standards.md` best practices. - I will always strive to follow `_bmad/_memory/tech-writer-sidecar/documentation-standards.md` best practices.

View File

@ -7,6 +7,7 @@ agent:
title: UX Designer title: UX Designer
icon: 🎨 icon: 🎨
module: bmm module: bmm
capabilities: "user research, interaction design, UI patterns, experience strategy"
hasSidecar: false hasSidecar: false
persona: persona:
@ -23,4 +24,4 @@ agent:
menu: menu:
- trigger: CU or fuzzy match on ux-design - trigger: CU or fuzzy match on ux-design
exec: "{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md" exec: "{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md"
description: "[CU] Create UX: Guidance through realizing the plan for your UX to inform architecture and implementation. PRovides more details that what was discovered in the PRD" description: "[CU] Create UX: Guidance through realizing the plan for your UX to inform architecture and implementation. Provides more details than what was discovered in the PRD"

View File

@ -42,3 +42,9 @@ project_knowledge: # Artifacts from research, document-project output, other lon
prompt: "Where should long-term project knowledge be stored? (docs, research, references)" prompt: "Where should long-term project knowledge be stored? (docs, research, references)"
default: "docs" default: "docs"
result: "{project-root}/{value}" result: "{project-root}/{value}"
# Directories to create during installation (declarative, no code execution)
directories:
- "{planning_artifacts}"
- "{implementation_artifacts}"
- "{project_knowledge}"

View File

@ -70,14 +70,22 @@ This file contains the BMAD PRD philosophy, standards, and validation criteria t
**If PRD path provided as invocation parameter:** **If PRD path provided as invocation parameter:**
- Use provided path - Use provided path
**If no PRD path provided:** **If no PRD path provided, auto-discover:**
"**PRD Validation Workflow** - Search `{planning_artifacts}` for files matching `*prd*.md`
- Also check for sharded PRDs: `{planning_artifacts}/*prd*/*.md`
Which PRD would you like to validate? **If exactly ONE PRD found:**
- Use it automatically
- Inform user: "Found PRD: {discovered_path} — using it for validation."
Please provide the path to the PRD file you want to validate." **If MULTIPLE PRDs found:**
- List all discovered PRDs with numbered options
- "I found multiple PRDs. Which one would you like to validate?"
- Wait for user selection
**Wait for user to provide PRD path.** **If NO PRDs found:**
- "I couldn't find any PRD files in {planning_artifacts}. Please provide the path to the PRD file you want to validate."
- Wait for user to provide PRD path.
### 3. Validate PRD Exists and Load ### 3. Validate PRD Exists and Load

View File

@ -60,6 +60,4 @@ Load and read full config from {main_config} and resolve:
"**Validate Mode: Validating an existing PRD against BMAD standards.**" "**Validate Mode: Validating an existing PRD against BMAD standards.**"
Prompt for PRD path: "Which PRD would you like to validate? Please provide the path to the PRD.md file."
Then read fully and follow: `{validateWorkflow}` (steps-v/step-v-01-discovery.md) Then read fully and follow: `{validateWorkflow}` (steps-v/step-v-01-discovery.md)

View File

@ -12,7 +12,6 @@ document_output_language: "{config_source}:document_output_language"
date: system-generated date: system-generated
planning_artifacts: "{config_source}:planning_artifacts" planning_artifacts: "{config_source}:planning_artifacts"
implementation_artifacts: "{config_source}:implementation_artifacts" implementation_artifacts: "{config_source}:implementation_artifacts"
output_folder: "{implementation_artifacts}"
sprint_status: "{implementation_artifacts}/sprint-status.yaml" sprint_status: "{implementation_artifacts}/sprint-status.yaml"
# Workflow components # Workflow components
@ -21,10 +20,7 @@ instructions: "{installed_path}/instructions.xml"
validation: "{installed_path}/checklist.md" validation: "{installed_path}/checklist.md"
template: false template: false
variables: project_context: "**/project-context.md"
# Project context
project_context: "**/project-context.md"
story_dir: "{implementation_artifacts}"
# Smart input file references - handles both whole docs and sharded docs # Smart input file references - handles both whole docs and sharded docs
# Priority: Whole document first, then sharded version # Priority: Whole document first, then sharded version

View File

@ -10,6 +10,7 @@
<workflow> <workflow>
<step n="1" goal="Initialize Change Navigation"> <step n="1" goal="Initialize Change Navigation">
<action>Load {project_context} for coding standards and project-wide patterns (if exists)</action>
<action>Confirm change trigger and gather user description of the issue</action> <action>Confirm change trigger and gather user description of the issue</action>
<action>Ask: "What specific issue or change has been identified that requires navigation?"</action> <action>Ask: "What specific issue or change has been identified that requires navigation?"</action>
<action>Verify access to required project documents:</action> <action>Verify access to required project documents:</action>

View File

@ -12,8 +12,7 @@ date: system-generated
implementation_artifacts: "{config_source}:implementation_artifacts" implementation_artifacts: "{config_source}:implementation_artifacts"
planning_artifacts: "{config_source}:planning_artifacts" planning_artifacts: "{config_source}:planning_artifacts"
project_knowledge: "{config_source}:project_knowledge" project_knowledge: "{config_source}:project_knowledge"
output_folder: "{implementation_artifacts}" project_context: "**/project-context.md"
sprint_status: "{implementation_artifacts}/sprint-status.yaml"
# Smart input file references - handles both whole docs and sharded docs # Smart input file references - handles both whole docs and sharded docs
# Priority: Whole document first, then sharded version # Priority: Whole document first, then sharded version
@ -51,6 +50,5 @@ input_file_patterns:
installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/correct-course" installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/correct-course"
template: false template: false
instructions: "{installed_path}/instructions.md" instructions: "{installed_path}/instructions.md"
validation: "{installed_path}/checklist.md"
checklist: "{installed_path}/checklist.md" checklist: "{installed_path}/checklist.md"
default_output_file: "{planning_artifacts}/sprint-change-proposal-{date}.md" default_output_file: "{planning_artifacts}/sprint-change-proposal-{date}.md"

View File

@ -49,7 +49,7 @@ This is a COMPETITION to create the **ULTIMATE story context** that makes LLM de
### **Required Inputs:** ### **Required Inputs:**
- **Story file**: The story file to review and improve - **Story file**: The story file to review and improve
- **Workflow variables**: From workflow.yaml (story_dir, output_folder, epics_file, etc.) - **Workflow variables**: From workflow.yaml (implementation_artifacts, epics_file, etc.)
- **Source documents**: Epics, architecture, etc. (discovered or provided) - **Source documents**: Epics, architecture, etc. (discovered or provided)
- **Validation framework**: `validate-workflow.xml` (handles checklist execution) - **Validation framework**: `validate-workflow.xml` (handles checklist execution)
@ -65,7 +65,7 @@ You will systematically re-do the entire story creation process, but with a crit
2. **Load the story file**: `{story_file_path}` (provided by user or discovered) 2. **Load the story file**: `{story_file_path}` (provided by user or discovered)
3. **Load validation framework**: `{project-root}/_bmad/core/tasks/validate-workflow.xml` 3. **Load validation framework**: `{project-root}/_bmad/core/tasks/validate-workflow.xml`
4. **Extract metadata**: epic_num, story_num, story_key, story_title from story file 4. **Extract metadata**: epic_num, story_num, story_key, story_title from story file
5. **Resolve all workflow variables**: story_dir, output_folder, epics_file, architecture_file, etc. 5. **Resolve all workflow variables**: implementation_artifacts, epics_file, architecture_file, etc.
6. **Understand current status**: What story implementation guidance is currently provided? 6. **Understand current status**: What story implementation guidance is currently provided?
**Note:** If running in fresh context, user should provide the story file path being reviewed. If running from create-story workflow, the validation framework will automatically discover the checklist and story file. **Note:** If running in fresh context, user should provide the story file path being reviewed. If running from create-story workflow, the validation framework will automatically discover the checklist and story file.

View File

@ -192,7 +192,8 @@
(As a, I want, so that) - Detailed acceptance criteria (already BDD formatted) - Technical requirements specific to this story - (As a, I want, so that) - Detailed acceptance criteria (already BDD formatted) - Technical requirements specific to this story -
Business context and value - Success criteria <!-- Previous story analysis for context continuity --> Business context and value - Success criteria <!-- Previous story analysis for context continuity -->
<check if="story_num > 1"> <check if="story_num > 1">
<action>Load previous story file: {{story_dir}}/{{epic_num}}-{{previous_story_num}}-*.md</action> **PREVIOUS STORY INTELLIGENCE:** - <action>Find {{previous_story_num}}: scan {implementation_artifacts} for the story file in epic {{epic_num}} with the highest story number less than {{story_num}}</action>
<action>Load previous story file: {implementation_artifacts}/{{epic_num}}-{{previous_story_num}}-*.md</action> **PREVIOUS STORY INTELLIGENCE:** -
Dev notes and learnings from previous story - Review feedback and corrections needed - Files that were created/modified and their Dev notes and learnings from previous story - Review feedback and corrections needed - Files that were created/modified and their
patterns - Testing approaches that worked/didn't work - Problems encountered and solutions found - Code patterns established <action>Extract patterns - Testing approaches that worked/didn't work - Problems encountered and solutions found - Code patterns established <action>Extract
all learnings that could impact current story implementation</action> all learnings that could impact current story implementation</action>

View File

@ -6,11 +6,11 @@ author: "BMad"
config_source: "{project-root}/_bmad/bmm/config.yaml" config_source: "{project-root}/_bmad/bmm/config.yaml"
user_name: "{config_source}:user_name" user_name: "{config_source}:user_name"
communication_language: "{config_source}:communication_language" communication_language: "{config_source}:communication_language"
document_output_language: "{config_source}:document_output_language"
user_skill_level: "{config_source}:user_skill_level"
date: system-generated date: system-generated
planning_artifacts: "{config_source}:planning_artifacts" planning_artifacts: "{config_source}:planning_artifacts"
implementation_artifacts: "{config_source}:implementation_artifacts" implementation_artifacts: "{config_source}:implementation_artifacts"
output_folder: "{implementation_artifacts}"
story_dir: "{implementation_artifacts}"
# Workflow components # Workflow components
installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/create-story" installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/create-story"
@ -19,18 +19,14 @@ instructions: "{installed_path}/instructions.xml"
validation: "{installed_path}/checklist.md" validation: "{installed_path}/checklist.md"
# Variables and inputs # Variables and inputs
variables: sprint_status: "{implementation_artifacts}/sprint-status.yaml" # Primary source for story tracking
sprint_status: "{implementation_artifacts}/sprint-status.yaml" # Primary source for story tracking epics_file: "{planning_artifacts}/epics.md" # Enhanced epics+stories with BDD and source hints
epics_file: "{planning_artifacts}/epics.md" # Enhanced epics+stories with BDD and source hints prd_file: "{planning_artifacts}/prd.md" # Fallback for requirements (if not in epics file)
prd_file: "{planning_artifacts}/prd.md" # Fallback for requirements (if not in epics file) architecture_file: "{planning_artifacts}/architecture.md" # Fallback for constraints (if not in epics file)
architecture_file: "{planning_artifacts}/architecture.md" # Fallback for constraints (if not in epics file) ux_file: "{planning_artifacts}/*ux*.md" # Fallback for UX requirements (if not in epics file)
ux_file: "{planning_artifacts}/*ux*.md" # Fallback for UX requirements (if not in epics file) story_title: "" # Will be elicited if not derivable
story_title: "" # Will be elicited if not derivable
# Project context
project_context: "**/project-context.md" project_context: "**/project-context.md"
default_output_file: "{implementation_artifacts}/{{story_key}}.md"
default_output_file: "{story_dir}/{{story_key}}.md"
# Smart input file references - Simplified for enhanced approach # Smart input file references - Simplified for enhanced approach
# The epics+stories file should contain everything needed with source hints # The epics+stories file should contain everything needed with source hints

View File

@ -78,7 +78,7 @@
<!-- Non-sprint story discovery --> <!-- Non-sprint story discovery -->
<check if="{{sprint_status}} file does NOT exist"> <check if="{{sprint_status}} file does NOT exist">
<action>Search {story_dir} for stories directly</action> <action>Search {implementation_artifacts} for stories directly</action>
<action>Find stories with "ready-for-dev" status in files</action> <action>Find stories with "ready-for-dev" status in files</action>
<action>Look for story files matching pattern: *-*-*.md</action> <action>Look for story files matching pattern: *-*-*.md</action>
<action>Read each candidate story file to check Status section</action> <action>Read each candidate story file to check Status section</action>
@ -114,7 +114,7 @@
</check> </check>
<action>Store the found story_key (e.g., "1-2-user-authentication") for later status updates</action> <action>Store the found story_key (e.g., "1-2-user-authentication") for later status updates</action>
<action>Find matching story file in {story_dir} using story_key pattern: {{story_key}}.md</action> <action>Find matching story file in {implementation_artifacts} using story_key pattern: {{story_key}}.md</action>
<action>Read COMPLETE story file from discovered path</action> <action>Read COMPLETE story file from discovered path</action>
<anchor id="task_check" /> <anchor id="task_check" />

View File

@ -4,12 +4,10 @@ author: "BMad"
# Critical variables from config # Critical variables from config
config_source: "{project-root}/_bmad/bmm/config.yaml" config_source: "{project-root}/_bmad/bmm/config.yaml"
output_folder: "{config_source}:output_folder"
user_name: "{config_source}:user_name" user_name: "{config_source}:user_name"
communication_language: "{config_source}:communication_language" communication_language: "{config_source}:communication_language"
user_skill_level: "{config_source}:user_skill_level" user_skill_level: "{config_source}:user_skill_level"
document_output_language: "{config_source}:document_output_language" document_output_language: "{config_source}:document_output_language"
story_dir: "{config_source}:implementation_artifacts"
date: system-generated date: system-generated
# Workflow components # Workflow components

View File

@ -31,6 +31,7 @@ PARTY MODE PROTOCOL:
<step n="1" goal="Epic Discovery - Find Completed Epic with Priority Logic"> <step n="1" goal="Epic Discovery - Find Completed Epic with Priority Logic">
<action>Load {project_context} for project-wide patterns and conventions (if exists)</action>
<action>Explain to {user_name} the epic discovery process using natural dialogue</action> <action>Explain to {user_name} the epic discovery process using natural dialogue</action>
<output> <output>
@ -80,7 +81,7 @@ Bob (Scrum Master): "I'm having trouble detecting the completed epic from {sprin
<check if="{{epic_number}} still not determined"> <check if="{{epic_number}} still not determined">
<action>PRIORITY 3: Fallback to stories folder</action> <action>PRIORITY 3: Fallback to stories folder</action>
<action>Scan {story_directory} for highest numbered story files</action> <action>Scan {implementation_artifacts} for highest numbered story files</action>
<action>Extract epic numbers from story filenames (pattern: epic-X-Y-story-name.md)</action> <action>Extract epic numbers from story filenames (pattern: epic-X-Y-story-name.md)</action>
<action>Set {{detected_epic}} = highest epic number found</action> <action>Set {{detected_epic}} = highest epic number found</action>
@ -170,7 +171,7 @@ Bob (Scrum Master): "Before we start the team discussion, let me review all the
Charlie (Senior Dev): "Good idea - those dev notes always have gold in them." Charlie (Senior Dev): "Good idea - those dev notes always have gold in them."
</output> </output>
<action>For each story in epic {{epic_number}}, read the complete story file from {story_directory}/{{epic_number}}-{{story_num}}-\*.md</action> <action>For each story in epic {{epic_number}}, read the complete story file from {implementation_artifacts}/{{epic_number}}-{{story_num}}-*.md</action>
<action>Extract and analyze from each story:</action> <action>Extract and analyze from each story:</action>
@ -261,14 +262,14 @@ Bob (Scrum Master): "We'll get to all of it. But first, let me load the previous
<action>Calculate previous epic number: {{prev_epic_num}} = {{epic_number}} - 1</action> <action>Calculate previous epic number: {{prev_epic_num}} = {{epic_number}} - 1</action>
<check if="{{prev_epic_num}} >= 1"> <check if="{{prev_epic_num}} >= 1">
<action>Search for previous retrospective using pattern: {retrospectives_folder}/epic-{{prev_epic_num}}-retro-*.md</action> <action>Search for previous retrospectives using pattern: {implementation_artifacts}/epic-{{prev_epic_num}}-retro-*.md</action>
<check if="previous retro found"> <check if="previous retrospectives found">
<output> <output>
Bob (Scrum Master): "I found our retrospective from Epic {{prev_epic_num}}. Let me see what we committed to back then..." Bob (Scrum Master): "I found our retrospectives from Epic {{prev_epic_num}}. Let me see what we committed to back then..."
</output> </output>
<action>Read the complete previous retrospective file</action> <action>Read the previous retrospectives</action>
<action>Extract key elements:</action> <action>Extract key elements:</action>
- **Action items committed**: What did the team agree to improve? - **Action items committed**: What did the team agree to improve?
@ -365,7 +366,7 @@ Alice (Product Owner): "Good thinking - helps us connect what we learned to what
<action>Attempt to load next epic using selective loading strategy:</action> <action>Attempt to load next epic using selective loading strategy:</action>
**Try sharded first (more specific):** **Try sharded first (more specific):**
<action>Check if file exists: {planning_artifacts}/epic\*/epic-{{next_epic_num}}.md</action> <action>Check if file exists: {planning_artifacts}/epic*/epic-{{next_epic_num}}.md</action>
<check if="sharded epic file found"> <check if="sharded epic file found">
<action>Load {planning_artifacts}/*epic*/epic-{{next_epic_num}}.md</action> <action>Load {planning_artifacts}/*epic*/epic-{{next_epic_num}}.md</action>
@ -374,7 +375,7 @@ Alice (Product Owner): "Good thinking - helps us connect what we learned to what
**Fallback to whole document:** **Fallback to whole document:**
<check if="sharded epic not found"> <check if="sharded epic not found">
<action>Check if file exists: {planning_artifacts}/epic\*.md</action> <action>Check if file exists: {planning_artifacts}/epic*.md</action>
<check if="whole epic file found"> <check if="whole epic file found">
<action>Load entire epics document</action> <action>Load entire epics document</action>
@ -1302,7 +1303,7 @@ Bob (Scrum Master): "See you all when prep work is done. Meeting adjourned!"
<step n="11" goal="Save Retrospective and Update Sprint Status"> <step n="11" goal="Save Retrospective and Update Sprint Status">
<action>Ensure retrospectives folder exists: {retrospectives_folder}</action> <action>Ensure retrospectives folder exists: {implementation_artifacts}</action>
<action>Create folder if it doesn't exist</action> <action>Create folder if it doesn't exist</action>
<action>Generate comprehensive retrospective summary document including:</action> <action>Generate comprehensive retrospective summary document including:</action>
@ -1322,11 +1323,11 @@ Bob (Scrum Master): "See you all when prep work is done. Meeting adjourned!"
- Commitments and next steps - Commitments and next steps
<action>Format retrospective document as readable markdown with clear sections</action> <action>Format retrospective document as readable markdown with clear sections</action>
<action>Set filename: {retrospectives_folder}/epic-{{epic_number}}-retro-{date}.md</action> <action>Set filename: {implementation_artifacts}/epic-{{epic_number}}-retro-{date}.md</action>
<action>Save retrospective document</action> <action>Save retrospective document</action>
<output> <output>
✅ Retrospective document saved: {retrospectives_folder}/epic-{{epic_number}}-retro-{date}.md ✅ Retrospective document saved: {implementation_artifacts}/epic-{{epic_number}}-retro-{date}.md
</output> </output>
<action>Update {sprint_status_file} to mark retrospective as completed</action> <action>Update {sprint_status_file} to mark retrospective as completed</action>
@ -1365,7 +1366,7 @@ Retrospective document was saved successfully, but {sprint_status_file} may need
- Epic {{epic_number}}: {{epic_title}} reviewed - Epic {{epic_number}}: {{epic_title}} reviewed
- Retrospective Status: completed - Retrospective Status: completed
- Retrospective saved: {retrospectives_folder}/epic-{{epic_number}}-retro-{date}.md - Retrospective saved: {implementation_artifacts}/epic-{{epic_number}}-retro-{date}.md
**Commitments Made:** **Commitments Made:**
@ -1375,7 +1376,7 @@ Retrospective document was saved successfully, but {sprint_status_file} may need
**Next Steps:** **Next Steps:**
1. **Review retrospective summary**: {retrospectives_folder}/epic-{{epic_number}}-retro-{date}.md 1. **Review retrospective summary**: {implementation_artifacts}/epic-{{epic_number}}-retro-{date}.md
2. **Execute preparation sprint** (Est: {{prep_days}} days) 2. **Execute preparation sprint** (Est: {{prep_days}} days)
- Complete {{critical_count}} critical path items - Complete {{critical_count}} critical path items

View File

@ -4,7 +4,6 @@ description: "Run after epic completion to review overall success, extract lesso
author: "BMad" author: "BMad"
config_source: "{project-root}/_bmad/bmm/config.yaml" config_source: "{project-root}/_bmad/bmm/config.yaml"
output_folder: "{config_source}:implementation_artifacts}"
user_name: "{config_source}:user_name" user_name: "{config_source}:user_name"
communication_language: "{config_source}:communication_language" communication_language: "{config_source}:communication_language"
user_skill_level: "{config_source}:user_skill_level" user_skill_level: "{config_source}:user_skill_level"
@ -12,6 +11,7 @@ document_output_language: "{config_source}:document_output_language"
date: system-generated date: system-generated
planning_artifacts: "{config_source}:planning_artifacts" planning_artifacts: "{config_source}:planning_artifacts"
implementation_artifacts: "{config_source}:implementation_artifacts" implementation_artifacts: "{config_source}:implementation_artifacts"
project_context: "**/project-context.md"
installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/retrospective" installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/retrospective"
template: false template: false
@ -51,5 +51,3 @@ input_file_patterns:
# Required files # Required files
sprint_status_file: "{implementation_artifacts}/sprint-status.yaml" sprint_status_file: "{implementation_artifacts}/sprint-status.yaml"
story_directory: "{implementation_artifacts}"
retrospectives_folder: "{implementation_artifacts}"

View File

@ -23,6 +23,7 @@
<workflow> <workflow>
<step n="1" goal="Parse epic files and extract all work items"> <step n="1" goal="Parse epic files and extract all work items">
<action>Load {project_context} for project-wide patterns and conventions (if exists)</action>
<action>Communicate in {communication_language} with {user_name}</action> <action>Communicate in {communication_language} with {user_name}</action>
<action>Look for all files matching `{epics_pattern}` in {epics_location}</action> <action>Look for all files matching `{epics_pattern}` in {epics_location}</action>
<action>Could be a single `epics.md` file or multiple `epic-1.md`, `epic-2.md` files</action> <action>Could be a single `epics.md` file or multiple `epic-1.md`, `epic-2.md` files</action>

View File

@ -9,7 +9,6 @@ communication_language: "{config_source}:communication_language"
date: system-generated date: system-generated
implementation_artifacts: "{config_source}:implementation_artifacts" implementation_artifacts: "{config_source}:implementation_artifacts"
planning_artifacts: "{config_source}:planning_artifacts" planning_artifacts: "{config_source}:planning_artifacts"
output_folder: "{implementation_artifacts}"
# Workflow components # Workflow components
installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/sprint-planning" installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/sprint-planning"
@ -18,24 +17,21 @@ template: "{installed_path}/sprint-status-template.yaml"
validation: "{installed_path}/checklist.md" validation: "{installed_path}/checklist.md"
# Variables and inputs # Variables and inputs
variables: project_context: "**/project-context.md"
# Project context project_name: "{config_source}:project_name"
project_context: "**/project-context.md"
# Project identification
project_name: "{config_source}:project_name"
# Tracking system configuration # Tracking system configuration
tracking_system: "file-system" # Options: file-system, Future will support other options from config of mcp such as jira, linear, trello tracking_system: "file-system" # Options: file-system, Future will support other options from config of mcp such as jira, linear, trello
project_key: "NOKEY" # Placeholder for tracker integrations; file-system uses a no-op key project_key: "NOKEY" # Placeholder for tracker integrations; file-system uses a no-op key
story_location: "{config_source}:implementation_artifacts" # Relative path for file-system, Future will support URL for Jira/Linear/Trello story_location: "{implementation_artifacts}" # Relative path for file-system, Future will support URL for Jira/Linear/Trello
story_location_absolute: "{config_source}:implementation_artifacts" # Absolute path for file operations story_location_absolute: "{implementation_artifacts}" # Absolute path for file operations
# Source files (file-system only) # Source files (file-system only)
epics_location: "{planning_artifacts}" # Directory containing epic*.md files epics_location: "{planning_artifacts}" # Directory containing epic*.md files
epics_pattern: "epic*.md" # Pattern to find epic files epics_pattern: "epic*.md" # Pattern to find epic files
# Output configuration # Output configuration
status_file: "{implementation_artifacts}/sprint-status.yaml" status_file: "{implementation_artifacts}/sprint-status.yaml"
# Smart input file references - handles both whole docs and sharded docs # Smart input file references - handles both whole docs and sharded docs
# Priority: Whole document first, then sharded version # Priority: Whole document first, then sharded version
@ -43,8 +39,8 @@ variables:
input_file_patterns: input_file_patterns:
epics: epics:
description: "All epics with user stories" description: "All epics with user stories"
whole: "{output_folder}/*epic*.md" whole: "{planning_artifacts}/*epic*.md"
sharded: "{output_folder}/*epic*/*.md" sharded: "{planning_artifacts}/*epic*/*.md"
load_strategy: "FULL_LOAD" load_strategy: "FULL_LOAD"
# Output configuration # Output configuration

View File

@ -24,6 +24,7 @@
</step> </step>
<step n="1" goal="Locate sprint status file"> <step n="1" goal="Locate sprint status file">
<action>Load {project_context} for project-wide patterns and conventions (if exists)</action>
<action>Try {sprint_status_file}</action> <action>Try {sprint_status_file}</action>
<check if="file not found"> <check if="file not found">
<output>❌ sprint-status.yaml not found. <output>❌ sprint-status.yaml not found.

View File

@ -5,22 +5,17 @@ author: "BMad"
# Critical variables from config # Critical variables from config
config_source: "{project-root}/_bmad/bmm/config.yaml" config_source: "{project-root}/_bmad/bmm/config.yaml"
output_folder: "{config_source}:output_folder"
user_name: "{config_source}:user_name" user_name: "{config_source}:user_name"
communication_language: "{config_source}:communication_language" communication_language: "{config_source}:communication_language"
document_output_language: "{config_source}:document_output_language" document_output_language: "{config_source}:document_output_language"
date: system-generated
implementation_artifacts: "{config_source}:implementation_artifacts" implementation_artifacts: "{config_source}:implementation_artifacts"
planning_artifacts: "{config_source}:planning_artifacts"
# Workflow components # Workflow components
installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/sprint-status" installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/sprint-status"
instructions: "{installed_path}/instructions.md" instructions: "{installed_path}/instructions.md"
# Inputs # Inputs
variables: sprint_status_file: "{implementation_artifacts}/sprint-status.yaml"
sprint_status_file: "{implementation_artifacts}/sprint-status.yaml"
tracking_system: "file-system"
# Smart input file references # Smart input file references
input_file_patterns: input_file_patterns:

View File

@ -28,7 +28,7 @@ This uses **step-file architecture** for focused execution:
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve: Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
- `user_name`, `communication_language`, `user_skill_level` - `user_name`, `communication_language`, `user_skill_level`
- `output_folder`, `planning_artifacts`, `implementation_artifacts` - `planning_artifacts`, `implementation_artifacts`
- `date` as system-generated current datetime - `date` as system-generated current datetime
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}` - ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`

View File

@ -76,7 +76,7 @@ a) **Before asking detailed questions, do a rapid scan to understand the landsca
b) **Check for existing context docs:** b) **Check for existing context docs:**
- Check `{output_folder}` and `{planning_artifacts}`for planning documents (PRD, architecture, epics, research) - Check `{implementation_artifacts}` and `{planning_artifacts}`for planning documents (PRD, architecture, epics, research)
- Check for `**/project-context.md` - if it exists, skim for patterns and conventions - Check for `**/project-context.md` - if it exists, skim for patterns and conventions
- Check for any existing stories or specs related to user's request - Check for any existing stories or specs related to user's request

View File

@ -68,9 +68,10 @@ This uses **step-file architecture** for disciplined execution:
Load and read full config from `{main_config}` and resolve: Load and read full config from `{main_config}` and resolve:
- `project_name`, `output_folder`, `planning_artifacts`, `implementation_artifacts`, `user_name` - `project_name`, `planning_artifacts`, `implementation_artifacts`, `user_name`
- `communication_language`, `document_output_language`, `user_skill_level` - `communication_language`, `document_output_language`, `user_skill_level`
- `date` as system-generated current datetime - `date` as system-generated current datetime
- `project_context` = `**/project-context.md` (load if exists)
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}` - ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
### 2. First Step Execution ### 2. First Step Execution

View File

@ -57,7 +57,7 @@
<step n="2" goal="Check for resumability and determine workflow mode"> <step n="2" goal="Check for resumability and determine workflow mode">
<critical>SMART LOADING STRATEGY: Check state file FIRST before loading any CSV files</critical> <critical>SMART LOADING STRATEGY: Check state file FIRST before loading any CSV files</critical>
<action>Check for existing state file at: {output_folder}/project-scan-report.json</action> <action>Check for existing state file at: {project_knowledge}/project-scan-report.json</action>
<check if="project-scan-report.json exists"> <check if="project-scan-report.json exists">
<action>Read state file and extract: timestamps, mode, scan_level, current_step, completed_steps, project_classification</action> <action>Read state file and extract: timestamps, mode, scan_level, current_step, completed_steps, project_classification</action>
@ -107,8 +107,8 @@ Your choice [1/2/3]:
</check> </check>
<check if="user selects 2"> <check if="user selects 2">
<action>Create archive directory: {output_folder}/.archive/</action> <action>Create archive directory: {project_knowledge}/.archive/</action>
<action>Move old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action> <action>Move old state file to: {project_knowledge}/.archive/project-scan-report-{{timestamp}}.json</action>
<action>Set resume_mode = false</action> <action>Set resume_mode = false</action>
<action>Continue to Step 0.5</action> <action>Continue to Step 0.5</action>
</check> </check>
@ -120,7 +120,7 @@ Your choice [1/2/3]:
<check if="state file age >= 24 hours"> <check if="state file age >= 24 hours">
<action>Display: "Found old state file (>24 hours). Starting fresh scan."</action> <action>Display: "Found old state file (>24 hours). Starting fresh scan."</action>
<action>Archive old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action> <action>Archive old state file to: {project_knowledge}/.archive/project-scan-report-{{timestamp}}.json</action>
<action>Set resume_mode = false</action> <action>Set resume_mode = false</action>
<action>Continue to Step 0.5</action> <action>Continue to Step 0.5</action>
</check> </check>
@ -128,7 +128,7 @@ Your choice [1/2/3]:
</step> </step>
<step n="3" goal="Check for existing documentation and determine workflow mode" if="resume_mode == false"> <step n="3" goal="Check for existing documentation and determine workflow mode" if="resume_mode == false">
<action>Check if {output_folder}/index.md exists</action> <action>Check if {project_knowledge}/index.md exists</action>
<check if="index.md exists"> <check if="index.md exists">
<action>Read existing index.md to extract metadata (date, project structure, parts count)</action> <action>Read existing index.md to extract metadata (date, project structure, parts count)</action>
@ -195,7 +195,7 @@ Your choice [1/2/3]:
- Mode: {{workflow_mode}} - Mode: {{workflow_mode}}
- Scan Level: {{scan_level}} - Scan Level: {{scan_level}}
- Output: {output_folder}/index.md and related files - Output: {project_knowledge}/index.md and related files
{{#if status_file_found}} {{#if status_file_found}}
**Status Updated:** **Status Updated:**

View File

@ -45,9 +45,9 @@
"type": "string", "type": "string",
"description": "Absolute path to project root directory" "description": "Absolute path to project root directory"
}, },
"output_folder": { "project_knowledge": {
"type": "string", "type": "string",
"description": "Absolute path to output folder" "description": "Absolute path to project knowledge folder"
}, },
"completed_steps": { "completed_steps": {
"type": "array", "type": "array",

View File

@ -6,7 +6,7 @@ author: "BMad"
# Critical variables # Critical variables
config_source: "{project-root}/_bmad/bmm/config.yaml" config_source: "{project-root}/_bmad/bmm/config.yaml"
output_folder: "{config_source}:project_knowledge" project_knowledge: "{config_source}:project_knowledge"
user_name: "{config_source}:user_name" user_name: "{config_source}:user_name"
communication_language: "{config_source}:communication_language" communication_language: "{config_source}:communication_language"
document_output_language: "{config_source}:document_output_language" document_output_language: "{config_source}:document_output_language"

View File

@ -194,7 +194,7 @@ This will read EVERY file in this area. Proceed? [y/n]
<action>Load complete deep-dive template from: {installed_path}/templates/deep-dive-template.md</action> <action>Load complete deep-dive template from: {installed_path}/templates/deep-dive-template.md</action>
<action>Fill template with all collected data from steps 13b-13d</action> <action>Fill template with all collected data from steps 13b-13d</action>
<action>Write filled template to: {output_folder}/deep-dive-{{sanitized_target_name}}.md</action> <action>Write filled template to: {project_knowledge}/deep-dive-{{sanitized_target_name}}.md</action>
<action>Validate deep-dive document completeness</action> <action>Validate deep-dive document completeness</action>
<template-output>deep_dive_documentation</template-output> <template-output>deep_dive_documentation</template-output>
@ -241,7 +241,7 @@ Detailed exhaustive analysis of specific areas:
## Deep-Dive Documentation Complete! ✓ ## Deep-Dive Documentation Complete! ✓
**Generated:** {output_folder}/deep-dive-{{target_name}}.md **Generated:** {project_knowledge}/deep-dive-{{target_name}}.md
**Files Analyzed:** {{file_count}} **Files Analyzed:** {{file_count}}
**Lines of Code Scanned:** {{total_loc}} **Lines of Code Scanned:** {{total_loc}}
**Time Taken:** ~{{duration}} **Time Taken:** ~{{duration}}
@ -255,7 +255,7 @@ Detailed exhaustive analysis of specific areas:
- Related code and reuse opportunities - Related code and reuse opportunities
- Implementation guidance - Implementation guidance
**Index Updated:** {output_folder}/index.md now includes link to this deep-dive **Index Updated:** {project_knowledge}/index.md now includes link to this deep-dive
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
</action> </action>
@ -278,7 +278,7 @@ Your choice [1/2]:
All deep-dive documentation complete! All deep-dive documentation complete!
**Master Index:** {output_folder}/index.md **Master Index:** {project_knowledge}/index.md
**Deep-Dives Generated:** {{deep_dive_count}} **Deep-Dives Generated:** {{deep_dive_count}}
These comprehensive docs are now ready for: These comprehensive docs are now ready for:

View File

@ -8,7 +8,7 @@ parent_workflow: "{project-root}/_bmad/bmm/workflows/document-project/workflow.y
# Critical variables inherited from parent # Critical variables inherited from parent
config_source: "{project-root}/_bmad/bmb/config.yaml" config_source: "{project-root}/_bmad/bmb/config.yaml"
output_folder: "{config_source}:output_folder" project_knowledge: "{config_source}:project_knowledge"
user_name: "{config_source}:user_name" user_name: "{config_source}:user_name"
date: system-generated date: system-generated

View File

@ -43,7 +43,7 @@ This workflow uses a single comprehensive CSV file to intelligently document you
</step> </step>
<step n="0.6" goal="Check for existing documentation and determine workflow mode"> <step n="0.6" goal="Check for existing documentation and determine workflow mode">
<action>Check if {output_folder}/index.md exists</action> <action>Check if {project_knowledge}/index.md exists</action>
<check if="index.md exists"> <check if="index.md exists">
<action>Read existing index.md to extract metadata (date, project structure, parts count)</action> <action>Read existing index.md to extract metadata (date, project structure, parts count)</action>
@ -127,7 +127,7 @@ Your choice [1/2/3] (default: 1):
<action>Display: "Using Exhaustive Scan (reading all source files)"</action> <action>Display: "Using Exhaustive Scan (reading all source files)"</action>
</action> </action>
<action>Initialize state file: {output_folder}/project-scan-report.json</action> <action>Initialize state file: {project_knowledge}/project-scan-report.json</action>
<critical>Every time you touch the state file, record: step id, human-readable summary (what you actually did), precise timestamp, and any outputs written. Vague phrases are unacceptable.</critical> <critical>Every time you touch the state file, record: step id, human-readable summary (what you actually did), precise timestamp, and any outputs written. Vague phrases are unacceptable.</critical>
<action>Write initial state: <action>Write initial state:
{ {
@ -136,7 +136,7 @@ Your choice [1/2/3] (default: 1):
"mode": "{{workflow_mode}}", "mode": "{{workflow_mode}}",
"scan_level": "{{scan_level}}", "scan_level": "{{scan_level}}",
"project_root": "{{project_root_path}}", "project_root": "{{project_root_path}}",
"output_folder": "{{output_folder}}", "project_knowledge": "{{project_knowledge}}",
"completed_steps": [], "completed_steps": [],
"current_step": "step_1", "current_step": "step_1",
"findings": {}, "findings": {},
@ -325,7 +325,7 @@ findings.batches_completed: [
</check> </check>
<action>Build API contracts catalog</action> <action>Build API contracts catalog</action>
<action>IMMEDIATELY write to: {output_folder}/api-contracts-{part_id}.md</action> <action>IMMEDIATELY write to: {project_knowledge}/api-contracts-{part_id}.md</action>
<action>Validate document has all required sections</action> <action>Validate document has all required sections</action>
<action>Update state file with output generated</action> <action>Update state file with output generated</action>
<action>PURGE detailed API data, keep only: "{{api_count}} endpoints documented"</action> <action>PURGE detailed API data, keep only: "{{api_count}} endpoints documented"</action>
@ -346,7 +346,7 @@ findings.batches_completed: [
</check> </check>
<action>Build database schema documentation</action> <action>Build database schema documentation</action>
<action>IMMEDIATELY write to: {output_folder}/data-models-{part_id}.md</action> <action>IMMEDIATELY write to: {project_knowledge}/data-models-{part_id}.md</action>
<action>Validate document completeness</action> <action>Validate document completeness</action>
<action>Update state file with output generated</action> <action>Update state file with output generated</action>
<action>PURGE detailed schema data, keep only: "{{table_count}} tables documented"</action> <action>PURGE detailed schema data, keep only: "{{table_count}} tables documented"</action>
@ -805,7 +805,7 @@ When a document SHOULD be generated but wasn't (due to quick scan, missing data,
<step n="11" goal="Validate and review generated documentation" if="workflow_mode != deep_dive"> <step n="11" goal="Validate and review generated documentation" if="workflow_mode != deep_dive">
<action>Show summary of all generated files: <action>Show summary of all generated files:
Generated in {{output_folder}}/: Generated in {{project_knowledge}}/:
{{file_list_with_sizes}} {{file_list_with_sizes}}
</action> </action>
@ -823,7 +823,7 @@ Generated in {{output_folder}}/:
3. Extract document metadata from each match for user selection 3. Extract document metadata from each match for user selection
</critical> </critical>
<action>Read {output_folder}/index.md</action> <action>Read {project_knowledge}/index.md</action>
<action>Scan for incomplete documentation markers: <action>Scan for incomplete documentation markers:
Step 1: Search for exact pattern "_(To be generated)_" (case-sensitive) Step 1: Search for exact pattern "_(To be generated)_" (case-sensitive)
@ -1065,9 +1065,9 @@ Enter number(s) separated by commas (e.g., "1,3,5"), or type 'all':
## Project Documentation Complete! ✓ ## Project Documentation Complete! ✓
**Location:** {{output_folder}}/ **Location:** {{project_knowledge}}/
**Master Index:** {{output_folder}}/index.md **Master Index:** {{project_knowledge}}/index.md
👆 This is your primary entry point for AI-assisted development 👆 This is your primary entry point for AI-assisted development
**Generated Documentation:** **Generated Documentation:**
@ -1076,9 +1076,9 @@ Enter number(s) separated by commas (e.g., "1,3,5"), or type 'all':
**Next Steps:** **Next Steps:**
1. Review the index.md to familiarize yourself with the documentation structure 1. Review the index.md to familiarize yourself with the documentation structure
2. When creating a brownfield PRD, point the PRD workflow to: {{output_folder}}/index.md 2. When creating a brownfield PRD, point the PRD workflow to: {{project_knowledge}}/index.md
3. For UI-only features: Reference {{output_folder}}/architecture-{{ui_part_id}}.md 3. For UI-only features: Reference {{project_knowledge}}/architecture-{{ui_part_id}}.md
4. For API-only features: Reference {{output_folder}}/architecture-{{api_part_id}}.md 4. For API-only features: Reference {{project_knowledge}}/architecture-{{api_part_id}}.md
5. For full-stack features: Reference both part architectures + integration-architecture.md 5. For full-stack features: Reference both part architectures + integration-architecture.md
**Verification Recap:** **Verification Recap:**
@ -1101,6 +1101,6 @@ When ready to plan new features, run the PRD workflow and provide this index as
- Write final state file - Write final state file
</action> </action>
<action>Display: "State file saved: {{output_folder}}/project-scan-report.json"</action> <action>Display: "State file saved: {{project_knowledge}}/project-scan-report.json"</action>
</workflow> </workflow>

View File

@ -8,7 +8,7 @@ parent_workflow: "{project-root}/_bmad/bmm/workflows/document-project/workflow.y
# Critical variables inherited from parent # Critical variables inherited from parent
config_source: "{project-root}/_bmad/bmb/config.yaml" config_source: "{project-root}/_bmad/bmb/config.yaml"
output_folder: "{config_source}:output_folder" project_knowledge: "{config_source}:project_knowledge"
user_name: "{config_source}:user_name" user_name: "{config_source}:user_name"
date: system-generated date: system-generated

View File

@ -5,7 +5,6 @@ author: "BMad"
# Critical variables from config # Critical variables from config
config_source: "{project-root}/_bmad/bmm/config.yaml" config_source: "{project-root}/_bmad/bmm/config.yaml"
output_folder: "{config_source}:output_folder"
implementation_artifacts: "{config_source}:implementation_artifacts" implementation_artifacts: "{config_source}:implementation_artifacts"
user_name: "{config_source}:user_name" user_name: "{config_source}:user_name"
communication_language: "{config_source}:communication_language" communication_language: "{config_source}:communication_language"
@ -19,10 +18,8 @@ validation: "{installed_path}/checklist.md"
template: false template: false
# Variables and inputs # Variables and inputs
variables: test_dir: "{project-root}/tests" # Root test directory
# Directory paths source_dir: "{project-root}" # Source code directory
test_dir: "{project-root}/tests" # Root test directory
source_dir: "{project-root}" # Source code directory
# Output configuration # Output configuration
default_output_file: "{implementation_artifacts}/tests/test-summary.md" default_output_file: "{implementation_artifacts}/tests/test-summary.md"

View File

@ -1,60 +0,0 @@
const chalk = require('chalk');
/**
* Core Module Installer
* Standard module installer function that executes after IDE installations
*
* @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from module.yaml
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
* @param {Object} options.logger - Logger instance for output
* @returns {Promise<boolean>} - Success status
*/
async function install(options) {
const { projectRoot, config, installedIDEs, logger } = options;
try {
logger.log(chalk.blue('🏗️ Installing Core Module...'));
// Core agent configs are created by the main installer's createAgentConfigs method
// No need to create them here - they'll be handled along with all other agents
// Handle IDE-specific configurations if needed
if (installedIDEs && installedIDEs.length > 0) {
logger.log(chalk.cyan(`Configuring Core for IDEs: ${installedIDEs.join(', ')}`));
// Add any IDE-specific Core configurations here
for (const ide of installedIDEs) {
await configureForIDE(ide, projectRoot, config, logger);
}
}
logger.log(chalk.green('✓ Core Module installation complete'));
return true;
} catch (error) {
logger.error(chalk.red(`Error installing Core module: ${error.message}`));
return false;
}
}
/**
* Configure Core module for specific IDE
* @private
*/
async function configureForIDE(ide) {
// Add IDE-specific configurations here
switch (ide) {
case 'claude-code': {
// Claude Code specific Core configurations
break;
}
// Add more IDEs as needed
default: {
// No specific configuration needed
break;
}
}
}
module.exports = { install };

View File

@ -7,6 +7,7 @@ agent:
name: "BMad Master" name: "BMad Master"
title: "BMad Master Executor, Knowledge Custodian, and Workflow Orchestrator" title: "BMad Master Executor, Knowledge Custodian, and Workflow Orchestrator"
icon: "🧙" icon: "🧙"
capabilities: "runtime resource management, workflow orchestration, task execution, knowledge custodian"
hasSidecar: false hasSidecar: false
persona: persona:
@ -14,7 +15,7 @@ agent:
identity: "Master-level expert in the BMAD Core Platform and all loaded modules with comprehensive knowledge of all resources, tasks, and workflows. Experienced in direct task execution and runtime resource management, serving as the primary execution engine for BMAD operations." identity: "Master-level expert in the BMAD Core Platform and all loaded modules with comprehensive knowledge of all resources, tasks, and workflows. Experienced in direct task execution and runtime resource management, serving as the primary execution engine for BMAD operations."
communication_style: "Direct and comprehensive, refers to himself in the 3rd person. Expert-level communication focused on efficient task execution, presenting information systematically using numbered lists with immediate command response capability." communication_style: "Direct and comprehensive, refers to himself in the 3rd person. Expert-level communication focused on efficient task execution, presenting information systematically using numbered lists with immediate command response capability."
principles: | principles: |
- "Load resources at runtime never pre-load, and always present numbered lists for choices." - Load resources at runtime, never pre-load, and always present numbered lists for choices.
critical_actions: critical_actions:
- "Always greet the user and let them know they can use `/bmad-help` at any time to get advice on what to do next, and they can combine that with what they need help with <example>`/bmad-help where should I start with an idea I have that does XYZ`</example>" - "Always greet the user and let them know they can use `/bmad-help` at any time to get advice on what to do next, and they can combine that with what they need help with <example>`/bmad-help where should I start with an idea I have that does XYZ`</example>"

View File

@ -39,7 +39,6 @@ module.exports = {
if (config.actionType === 'cancel') { if (config.actionType === 'cancel') {
await prompts.log.warn('Installation cancelled.'); await prompts.log.warn('Installation cancelled.');
process.exit(0); process.exit(0);
return;
} }
// Handle quick update separately // Handle quick update separately
@ -47,23 +46,14 @@ module.exports = {
const result = await installer.quickUpdate(config); const result = await installer.quickUpdate(config);
await prompts.log.success('Quick update complete!'); await prompts.log.success('Quick update complete!');
await prompts.log.info(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`); await prompts.log.info(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`);
// Display version-specific end message
const { MessageLoader } = require('../installers/lib/message-loader');
const messageLoader = new MessageLoader();
await messageLoader.displayEndMessage();
process.exit(0); process.exit(0);
return;
} }
// Handle compile agents separately // Handle compile agents separately
if (config.actionType === 'compile-agents') { if (config.actionType === 'compile-agents') {
const result = await installer.compileAgents(config); const result = await installer.compileAgents(config);
await prompts.log.success('Agent recompilation complete!');
await prompts.log.info(`Recompiled ${result.agentCount} agents with customizations applied`); await prompts.log.info(`Recompiled ${result.agentCount} agents with customizations applied`);
process.exit(0); process.exit(0);
return;
} }
// Regular install/update flow // Regular install/update flow
@ -72,16 +62,10 @@ module.exports = {
// Check if installation was cancelled // Check if installation was cancelled
if (result && result.cancelled) { if (result && result.cancelled) {
process.exit(0); process.exit(0);
return;
} }
// Check if installation succeeded // Check if installation succeeded
if (result && result.success) { if (result && result.success) {
// Display version-specific end message from install-messages.yaml
const { MessageLoader } = require('../installers/lib/message-loader');
const messageLoader = new MessageLoader();
await messageLoader.displayEndMessage();
process.exit(0); process.exit(0);
} }
} catch (error) { } catch (error) {

View File

@ -42,13 +42,12 @@ modules:
type: bmad-org type: bmad-org
npmPackage: bmad-method-test-architecture-enterprise npmPackage: bmad-method-test-architecture-enterprise
# TODO: Enable once fixes applied: # whiteport-design-system:
# url: https://github.com/bmad-code-org/bmad-method-wds-expansion
# whiteport-design-system: # module-definition: src/module.yaml
# url: https://github.com/bmad-code-org/bmad-method-wds-expansion # code: wds
# module-definition: src/module.yaml # name: "Whiteport UX Design System"
# code: WDS # description: "UX design framework with Figma integration"
# name: "Whiteport UX Design System" # defaultSelected: false
# description: "UX design framework with Figma integration" # type: community
# defaultSelected: false # npmPackage: bmad-method-wds-expansion
# type: community

View File

@ -14,28 +14,10 @@ startMessage: |
but anticipate no massive breaking changes but anticipate no massive breaking changes
- Groundwork in place for customization and community modules - Groundwork in place for customization and community modules
📚 New Docs Site: http://docs.bmad-method.org/
- High quality tutorials, guided walkthrough, and articles coming soon!
- Everything is free. No paywalls. No gated content.
- Knowledge should be shared, not sold.
💡 Love BMad? Please star us on GitHub & subscribe on YouTube!
- GitHub: https://github.com/bmad-code-org/BMAD-METHOD/
- YouTube: https://www.youtube.com/@BMadCode
Latest updates: https://github.com/bmad-code-org/BMAD-METHOD/CHANGELOG.md
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Display at the END of installation (after all setup completes)
endMessage: |
════════════════════════════════════════════════════════════════════════════════
✨ BMAD V6 BETA IS INSTALLED! Thank you for being part of this journey!
🌟 BMad is 100% free and open source. 🌟 BMad is 100% free and open source.
- No gated Discord. No paywalls. - No gated Discord. No paywalls. No gated content.
- We believe in empowering everyone, not just those who can pay. - We believe in empowering everyone, not just those who can pay.
- Knowledge should be shared, not sold.
🙏 SUPPORT BMAD DEVELOPMENT: 🙏 SUPPORT BMAD DEVELOPMENT:
- During the Beta, please give us feedback and raise issues on GitHub! - During the Beta, please give us feedback and raise issues on GitHub!
@ -47,13 +29,14 @@ endMessage: |
- Topics: AI-Native Transformation, Spec and Context Engineering, BMad Method - Topics: AI-Native Transformation, Spec and Context Engineering, BMad Method
- For speaking inquiries or interviews, reach out to BMad on Discord! - For speaking inquiries or interviews, reach out to BMad on Discord!
📚 RESOURCES: ⭐ HELP US GROW:
- Docs: http://docs.bmad-method.org/ (bookmark it!)
- Changelog: https://github.com/bmad-code-org/BMAD-METHOD/CHANGELOG.md
⭐⭐⭐ HELP US GROW:
- Star us on GitHub: https://github.com/bmad-code-org/BMAD-METHOD/ - Star us on GitHub: https://github.com/bmad-code-org/BMAD-METHOD/
- Subscribe on YouTube: https://www.youtube.com/@BMadCode - Subscribe on YouTube: https://www.youtube.com/@BMadCode
- Every star & sub helps us reach more developers! - Every star & sub helps us reach more developers!
════════════════════════════════════════════════════════════════════════════════ Latest updates: https://github.com/bmad-code-org/BMAD-METHOD/CHANGELOG.md
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# No end message - install summary and next steps are rendered by the installer
endMessage: ""

View File

@ -10,6 +10,19 @@ class ConfigCollector {
this.collectedConfig = {}; this.collectedConfig = {};
this.existingConfig = null; this.existingConfig = null;
this.currentProjectDir = null; this.currentProjectDir = null;
this._moduleManagerInstance = null;
}
/**
* Get or create a cached ModuleManager instance (lazy initialization)
* @returns {Object} ModuleManager instance
*/
_getModuleManager() {
if (!this._moduleManagerInstance) {
const { ModuleManager } = require('../modules/manager');
this._moduleManagerInstance = new ModuleManager();
}
return this._moduleManagerInstance;
} }
/** /**
@ -129,6 +142,70 @@ class ConfigCollector {
return foundAny; return foundAny;
} }
/**
* Pre-scan module schemas to gather metadata for the configuration gateway prompt.
* Returns info about which modules have configurable options.
* @param {Array} modules - List of non-core module names
* @returns {Promise<Array>} Array of {moduleName, displayName, questionCount, hasFieldsWithoutDefaults}
*/
async scanModuleSchemas(modules) {
const metadataFields = new Set(['code', 'name', 'header', 'subheader', 'default_selected']);
const results = [];
for (const moduleName of modules) {
// Resolve module.yaml path - custom paths first, then standard location, then ModuleManager search
let moduleConfigPath = null;
const customPath = this.customModulePaths?.get(moduleName);
if (customPath) {
moduleConfigPath = path.join(customPath, 'module.yaml');
} else {
const standardPath = path.join(getModulePath(moduleName), 'module.yaml');
if (await fs.pathExists(standardPath)) {
moduleConfigPath = standardPath;
} else {
const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
if (moduleSourcePath) {
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
}
}
}
if (!moduleConfigPath || !(await fs.pathExists(moduleConfigPath))) {
continue;
}
try {
const content = await fs.readFile(moduleConfigPath, 'utf8');
const moduleConfig = yaml.parse(content);
if (!moduleConfig) continue;
const displayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`;
const configKeys = Object.keys(moduleConfig).filter((key) => key !== 'prompt');
const questionKeys = configKeys.filter((key) => {
if (metadataFields.has(key)) return false;
const item = moduleConfig[key];
return item && typeof item === 'object' && item.prompt;
});
const hasFieldsWithoutDefaults = questionKeys.some((key) => {
const item = moduleConfig[key];
return item.default === undefined || item.default === null || item.default === '';
});
results.push({
moduleName,
displayName,
questionCount: questionKeys.length,
hasFieldsWithoutDefaults,
});
} catch (error) {
await prompts.log.warn(`Could not read schema for module "${moduleName}": ${error.message}`);
}
}
return results;
}
/** /**
* Collect configuration for all modules * Collect configuration for all modules
* @param {Array} modules - List of modules to configure (including 'core') * @param {Array} modules - List of modules to configure (including 'core')
@ -141,6 +218,7 @@ class ConfigCollector {
// Store custom module paths for use in collectModuleConfig // Store custom module paths for use in collectModuleConfig
this.customModulePaths = options.customModulePaths || new Map(); this.customModulePaths = options.customModulePaths || new Map();
this.skipPrompts = options.skipPrompts || false; this.skipPrompts = options.skipPrompts || false;
this.modulesToCustomize = undefined;
await this.loadExistingConfig(projectDir); await this.loadExistingConfig(projectDir);
// Check if core was already collected (e.g., in early collection phase) // Check if core was already collected (e.g., in early collection phase)
@ -154,10 +232,95 @@ class ConfigCollector {
this.allAnswers = {}; this.allAnswers = {};
} }
for (const moduleName of allModules) { // Split processing: core first, then gateway, then remaining modules
const coreModules = allModules.filter((m) => m === 'core');
const nonCoreModules = allModules.filter((m) => m !== 'core');
// Collect core config first (always fully prompted)
for (const moduleName of coreModules) {
await this.collectModuleConfig(moduleName, projectDir); await this.collectModuleConfig(moduleName, projectDir);
} }
// Show batch configuration gateway for non-core modules
// Scan all non-core module schemas for display names and config metadata
let scannedModules = [];
if (!this.skipPrompts && nonCoreModules.length > 0) {
scannedModules = await this.scanModuleSchemas(nonCoreModules);
const customizableModules = scannedModules.filter((m) => m.questionCount > 0);
if (customizableModules.length > 0) {
const configMode = await prompts.select({
message: 'Module configuration',
choices: [
{ name: 'Express Setup', value: 'express', hint: 'accept all defaults (recommended)' },
{ name: 'Customize', value: 'customize', hint: 'choose modules to configure' },
],
default: 'express',
});
if (configMode === 'customize') {
const choices = customizableModules.map((m) => ({
name: `${m.displayName} (${m.questionCount} option${m.questionCount === 1 ? '' : 's'})`,
value: m.moduleName,
hint: m.hasFieldsWithoutDefaults ? 'has fields without defaults' : undefined,
checked: m.hasFieldsWithoutDefaults,
}));
const selected = await prompts.multiselect({
message: 'Select modules to customize:',
choices,
required: false,
});
this.modulesToCustomize = new Set(selected);
} else {
// Express mode: no modules to customize
this.modulesToCustomize = new Set();
}
} else {
// All non-core modules have zero config - no gateway needed
this.modulesToCustomize = new Set();
}
}
// Collect remaining non-core modules
if (this.modulesToCustomize === undefined) {
// No gateway was shown (skipPrompts, no non-core modules, or direct call) - process all normally
for (const moduleName of nonCoreModules) {
await this.collectModuleConfig(moduleName, projectDir);
}
} else {
// Split into default modules (tasks progress) and customized modules (interactive)
const defaultModules = nonCoreModules.filter((m) => !this.modulesToCustomize.has(m));
const customizeModules = nonCoreModules.filter((m) => this.modulesToCustomize.has(m));
// Run default modules with a single spinner
if (defaultModules.length > 0) {
// Build display name map from all scanned modules for pre-call spinner messages
const displayNameMap = new Map();
for (const m of scannedModules) {
displayNameMap.set(m.moduleName, m.displayName);
}
const configSpinner = await prompts.spinner();
configSpinner.start('Configuring modules...');
for (const moduleName of defaultModules) {
const displayName = displayNameMap.get(moduleName) || moduleName.toUpperCase();
configSpinner.message(`Configuring ${displayName}...`);
try {
this._silentConfig = true;
await this.collectModuleConfig(moduleName, projectDir);
} finally {
this._silentConfig = false;
}
}
configSpinner.stop('Module configuration complete');
}
// Run customized modules individually (may show interactive prompts)
for (const moduleName of customizeModules) {
await this.collectModuleConfig(moduleName, projectDir);
}
}
// Add metadata // Add metadata
this.collectedConfig._meta = { this.collectedConfig._meta = {
version: require(path.join(getProjectRoot(), 'package.json')).version, version: require(path.join(getProjectRoot(), 'package.json')).version,
@ -188,20 +351,15 @@ class ConfigCollector {
this.allAnswers = {}; this.allAnswers = {};
} }
// Load module's install config schema // Load module's config schema from module.yaml
// First, try the standard src/modules location // First, try the standard src/modules location
let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
let moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml'); let moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
// If not found in src/modules, we need to find it by searching the project // If not found in src/modules, we need to find it by searching the project
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) { if (!(await fs.pathExists(moduleConfigPath))) {
// Use the module manager to find the module source const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
const { ModuleManager } = require('../modules/manager');
const moduleManager = new ModuleManager();
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
if (moduleSourcePath) { if (moduleSourcePath) {
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml'); moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
} }
} }
@ -211,19 +369,14 @@ class ConfigCollector {
if (await fs.pathExists(moduleConfigPath)) { if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath; configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else { } else {
// Check if this is a custom module with custom.yaml // Check if this is a custom module with custom.yaml
const { ModuleManager } = require('../modules/manager'); const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
const moduleManager = new ModuleManager();
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
if (moduleSourcePath) { if (moduleSourcePath) {
const rootCustomConfigPath = path.join(moduleSourcePath, 'custom.yaml'); const rootCustomConfigPath = path.join(moduleSourcePath, 'custom.yaml');
const moduleInstallerCustomPath = path.join(moduleSourcePath, '_module-installer', 'custom.yaml');
if ((await fs.pathExists(rootCustomConfigPath)) || (await fs.pathExists(moduleInstallerCustomPath))) { if (await fs.pathExists(rootCustomConfigPath)) {
isCustomModule = true; isCustomModule = true;
// For custom modules, we don't have an install-config schema, so just use existing values // For custom modules, we don't have an install-config schema, so just use existing values
// The custom.yaml values will be loaded and merged during installation // The custom.yaml values will be loaded and merged during installation
@ -500,28 +653,21 @@ class ConfigCollector {
} }
// Load module's config // Load module's config
// First, check if we have a custom module path for this module // First, check if we have a custom module path for this module
let installerConfigPath = null;
let moduleConfigPath = null; let moduleConfigPath = null;
if (this.customModulePaths && this.customModulePaths.has(moduleName)) { if (this.customModulePaths && this.customModulePaths.has(moduleName)) {
const customPath = this.customModulePaths.get(moduleName); const customPath = this.customModulePaths.get(moduleName);
installerConfigPath = path.join(customPath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(customPath, 'module.yaml'); moduleConfigPath = path.join(customPath, 'module.yaml');
} else { } else {
// Try the standard src/modules location // Try the standard src/modules location
installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml'); moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
} }
// If not found in src/modules or custom paths, search the project // If not found in src/modules or custom paths, search the project
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) { if (!(await fs.pathExists(moduleConfigPath))) {
// Use the module manager to find the module source const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
const { ModuleManager } = require('../modules/manager');
const moduleManager = new ModuleManager();
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
if (moduleSourcePath) { if (moduleSourcePath) {
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml'); moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
} }
} }
@ -529,8 +675,6 @@ class ConfigCollector {
let configPath = null; let configPath = null;
if (await fs.pathExists(moduleConfigPath)) { if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath; configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else { } else {
// No config for this module // No config for this module
return; return;
@ -590,12 +734,12 @@ class ConfigCollector {
} }
} }
} else { } else {
await prompts.log.step(moduleDisplayName); if (!this._silentConfig) await prompts.log.step(`Configuring ${moduleDisplayName}`);
let customize = true; let useDefaults = true;
if (moduleName === 'core') { if (moduleName === 'core') {
// Core module: no confirm prompt, continues directly useDefaults = false; // Core: always show all questions
} else { } else if (this.modulesToCustomize === undefined) {
// Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing) // Fallback: original per-module confirm (backward compat for direct calls)
const customizeAnswer = await prompts.prompt([ const customizeAnswer = await prompts.prompt([
{ {
type: 'confirm', type: 'confirm',
@ -604,10 +748,13 @@ class ConfigCollector {
default: true, default: true,
}, },
]); ]);
customize = customizeAnswer.customize; useDefaults = customizeAnswer.customize;
} else {
// Batch mode: use defaults unless module was selected for customization
useDefaults = !this.modulesToCustomize.has(moduleName);
} }
if (customize && moduleName !== 'core') { if (useDefaults && moduleName !== 'core') {
// Accept defaults - only ask questions that have NO default value // Accept defaults - only ask questions that have NO default value
const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === ''); const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === '');
@ -737,6 +884,7 @@ class ConfigCollector {
const actualConfigKeys = configKeys.filter((key) => !metadataFields.has(key)); const actualConfigKeys = configKeys.filter((key) => !metadataFields.has(key));
const hasNoConfig = actualConfigKeys.length === 0; const hasNoConfig = actualConfigKeys.length === 0;
if (!this._silentConfig) {
if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) { if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
await prompts.log.step(moduleDisplayName); await prompts.log.step(moduleDisplayName);
if (moduleConfig.subheader) { if (moduleConfig.subheader) {
@ -749,6 +897,7 @@ class ConfigCollector {
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`); await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`);
} }
} }
}
// If we have no collected config for this module, but we have a module schema, // If we have no collected config for this module, but we have a module schema,
// ensure we have at least an empty object // ensure we have at least an empty object

View File

@ -7,6 +7,7 @@
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('node:path'); const path = require('node:path');
const crypto = require('node:crypto'); const crypto = require('node:crypto');
const prompts = require('../../../lib/prompts');
class CustomModuleCache { class CustomModuleCache {
constructor(bmadDir) { constructor(bmadDir) {
@ -195,7 +196,7 @@ class CustomModuleCache {
// Verify cache integrity // Verify cache integrity
const currentCacheHash = await this.calculateHash(cacheDir); const currentCacheHash = await this.calculateHash(cacheDir);
if (currentCacheHash !== cached.cacheHash) { if (currentCacheHash !== cached.cacheHash) {
console.warn(`Warning: Cache integrity check failed for ${moduleId}`); await prompts.log.warn(`Cache integrity check failed for ${moduleId}`);
} }
return { return {

View File

@ -1,6 +1,7 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const yaml = require('yaml'); const yaml = require('yaml');
const prompts = require('../../../lib/prompts');
/** /**
* Manages IDE configuration persistence * Manages IDE configuration persistence
@ -93,7 +94,7 @@ class IdeConfigManager {
const config = yaml.parse(content); const config = yaml.parse(content);
return config; return config;
} catch (error) { } catch (error) {
console.warn(`Warning: Failed to load IDE config for ${ideName}:`, error.message); await prompts.log.warn(`Failed to load IDE config for ${ideName}: ${error.message}`);
return null; return null;
} }
} }
@ -123,7 +124,7 @@ class IdeConfigManager {
} }
} }
} catch (error) { } catch (error) {
console.warn('Warning: Failed to load IDE configs:', error.message); await prompts.log.warn(`Failed to load IDE configs: ${error.message}`);
} }
return configs; return configs;

View File

@ -109,9 +109,17 @@ class Installer {
* @param {boolean} isFullReinstall - Whether this is a full reinstall * @param {boolean} isFullReinstall - Whether this is a full reinstall
* @param {Array} previousIdes - Previously configured IDEs (for reinstalls) * @param {Array} previousIdes - Previously configured IDEs (for reinstalls)
* @param {Array} preSelectedIdes - Pre-selected IDEs from early prompt (optional) * @param {Array} preSelectedIdes - Pre-selected IDEs from early prompt (optional)
* @param {boolean} skipPrompts - Skip prompts and use defaults (for --yes flag)
* @returns {Object} Tool/IDE selection and configurations * @returns {Object} Tool/IDE selection and configurations
*/ */
async collectToolConfigurations(projectDir, selectedModules, isFullReinstall = false, previousIdes = [], preSelectedIdes = null) { async collectToolConfigurations(
projectDir,
selectedModules,
isFullReinstall = false,
previousIdes = [],
preSelectedIdes = null,
skipPrompts = false,
) {
// Use pre-selected IDEs if provided, otherwise prompt // Use pre-selected IDEs if provided, otherwise prompt
let toolConfig; let toolConfig;
if (preSelectedIdes === null) { if (preSelectedIdes === null) {
@ -182,6 +190,7 @@ class Installer {
selectedModules: selectedModules || [], selectedModules: selectedModules || [],
projectDir, projectDir,
bmadDir, bmadDir,
skipPrompts,
}); });
} else { } else {
// Config-driven IDEs don't need configuration - mark as ready // Config-driven IDEs don't need configuration - mark as ready
@ -406,6 +415,9 @@ class Installer {
let action = null; let action = null;
if (config.actionType === 'update') { if (config.actionType === 'update') {
action = 'update'; action = 'update';
} else if (config.skipPrompts) {
// Non-interactive mode: default to update
action = 'update';
} else { } else {
// Fallback: Ask the user (backwards compatibility for other code paths) // Fallback: Ask the user (backwards compatibility for other code paths)
await prompts.log.warn('Existing BMAD installation detected'); await prompts.log.warn('Existing BMAD installation detected');
@ -431,9 +443,16 @@ class Installer {
// If there are modules to remove, ask for confirmation // If there are modules to remove, ask for confirmation
if (modulesToRemove.length > 0) { if (modulesToRemove.length > 0) {
const prompts = require('../../../lib/prompts'); if (config.skipPrompts) {
// Non-interactive mode: preserve modules (matches prompt default: false)
for (const moduleId of modulesToRemove) {
if (!config.modules) config.modules = [];
config.modules.push(moduleId);
}
spinner.start('Preparing update...');
} else {
if (spinner.isSpinning) { if (spinner.isSpinning) {
spinner.stop('Reviewing module changes'); spinner.stop('Module changes reviewed');
} }
await prompts.log.warn('Modules to be removed:'); await prompts.log.warn('Modules to be removed:');
@ -474,6 +493,7 @@ class Installer {
spinner.start('Preparing update...'); spinner.start('Preparing update...');
} }
}
// Detect custom and modified files BEFORE updating (compare current files vs files-manifest.csv) // Detect custom and modified files BEFORE updating (compare current files vs files-manifest.csv)
const existingFilesManifest = await this.readFilesManifest(bmadDir); const existingFilesManifest = await this.readFilesManifest(bmadDir);
@ -684,6 +704,7 @@ class Installer {
config._isFullReinstall || false, config._isFullReinstall || false,
config._previouslyConfiguredIdes || [], config._previouslyConfiguredIdes || [],
preSelectedIdes, preSelectedIdes,
config.skipPrompts || false,
); );
} }
@ -692,14 +713,80 @@ class Installer {
config.skipIde = toolSelection.skipIde; config.skipIde = toolSelection.skipIde;
const ideConfigurations = toolSelection.configurations; const ideConfigurations = toolSelection.configurations;
// Detect IDEs that were previously installed but are NOT in the new selection (to be removed)
if (config._isUpdate && config._existingInstall) {
const previouslyInstalledIdes = new Set(config._existingInstall.ides || []);
const newlySelectedIdes = new Set(config.ides || []);
const idesToRemove = [...previouslyInstalledIdes].filter((ide) => !newlySelectedIdes.has(ide));
if (idesToRemove.length > 0) {
if (config.skipPrompts) {
// Non-interactive mode: silently preserve existing IDE configs
if (!config.ides) config.ides = [];
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
for (const ide of idesToRemove) {
config.ides.push(ide);
if (savedIdeConfigs[ide] && !ideConfigurations[ide]) {
ideConfigurations[ide] = savedIdeConfigs[ide];
}
}
} else {
if (spinner.isSpinning) {
spinner.stop('IDE changes reviewed');
}
await prompts.log.warn('IDEs to be removed:');
for (const ide of idesToRemove) {
await prompts.log.error(` - ${ide}`);
}
const confirmRemoval = await prompts.confirm({
message: `Remove BMAD configuration for ${idesToRemove.length} IDE(s)?`,
default: false,
});
if (confirmRemoval) {
await this.ideManager.ensureInitialized();
for (const ide of idesToRemove) {
try {
const handler = this.ideManager.handlers.get(ide);
if (handler) {
await handler.cleanup(projectDir);
}
await this.ideConfigManager.deleteIdeConfig(bmadDir, ide);
await prompts.log.message(` Removed: ${ide}`);
} catch (error) {
await prompts.log.warn(` Warning: Failed to remove ${ide}: ${error.message}`);
}
}
await prompts.log.success(` Removed ${idesToRemove.length} IDE(s)`);
} else {
await prompts.log.message(' IDE removal cancelled');
// Add IDEs back to selection and restore their saved configurations
if (!config.ides) config.ides = [];
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
for (const ide of idesToRemove) {
config.ides.push(ide);
if (savedIdeConfigs[ide] && !ideConfigurations[ide]) {
ideConfigurations[ide] = savedIdeConfigs[ide];
}
}
}
spinner.start('Preparing installation...');
}
}
}
// Results collector for consolidated summary // Results collector for consolidated summary
const results = []; const results = [];
const addResult = (step, status, detail = '') => results.push({ step, status, detail }); const addResult = (step, status, detail = '') => results.push({ step, status, detail });
if (spinner.isSpinning) { if (spinner.isSpinning) {
spinner.message('Installing...'); spinner.message('Preparing installation...');
} else { } else {
spinner.start('Installing...'); spinner.start('Preparing installation...');
} }
// Create bmad directory structure // Create bmad directory structure
@ -728,20 +815,10 @@ class Installer {
const projectRoot = getProjectRoot(); const projectRoot = getProjectRoot();
// Step 1: Install core module first (if requested)
if (config.installCore) {
spinner.message('Installing BMAD core...');
await this.installCoreWithDependencies(bmadDir, { core: {} });
addResult('Core', 'ok', 'installed');
// Generate core config file
await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} });
}
// Custom content is already handled in UI before module selection // Custom content is already handled in UI before module selection
let finalCustomContent = config.customContent; const finalCustomContent = config.customContent;
// Step 3: Prepare modules list including cached custom modules // Prepare modules list including cached custom modules
let allModules = [...(config.modules || [])]; let allModules = [...(config.modules || [])];
// During quick update, we might have custom module sources from the manifest // During quick update, we might have custom module sources from the manifest
@ -780,8 +857,6 @@ class Installer {
allModules = allModules.filter((m) => m !== 'core'); allModules = allModules.filter((m) => m !== 'core');
} }
const modulesToInstall = allModules;
// For dependency resolution, we only need regular modules (not custom modules) // For dependency resolution, we only need regular modules (not custom modules)
// Custom modules are already installed in _bmad and don't need dependency resolution from source // Custom modules are already installed in _bmad and don't need dependency resolution from source
const regularModulesForResolution = allModules.filter((module) => { const regularModulesForResolution = allModules.filter((module) => {
@ -796,70 +871,91 @@ class Installer {
return !isCustom; return !isCustom;
}); });
// For dependency resolution, we need to pass the project root // Stop spinner before tasks() takes over progress display
spinner.stop('Preparation complete');
// ─────────────────────────────────────────────────────────────────────────
// FIRST TASKS BLOCK: Core installation through manifests (non-interactive)
// ─────────────────────────────────────────────────────────────────────────
const isQuickUpdate = config._quickUpdate || false;
// Shared resolution result across task callbacks (closure-scoped, not on `this`)
let taskResolution;
// Collect directory creation results for output after tasks() completes
const dirResults = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
// Build task list conditionally
const installTasks = [];
// Core installation task
if (config.installCore) {
installTasks.push({
title: isQuickUpdate ? 'Updating BMAD core' : 'Installing BMAD core',
task: async (message) => {
await this.installCoreWithDependencies(bmadDir, { core: {} });
addResult('Core', 'ok', isQuickUpdate ? 'updated' : 'installed');
await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} });
return isQuickUpdate ? 'Core updated' : 'Core installed';
},
});
}
// Dependency resolution task
installTasks.push({
title: 'Resolving dependencies',
task: async (message) => {
// Create a temporary module manager that knows about custom content locations // Create a temporary module manager that knows about custom content locations
const tempModuleManager = new ModuleManager({ const tempModuleManager = new ModuleManager({
bmadDir: bmadDir, // Pass bmadDir so we can check cache bmadDir: bmadDir,
}); });
spinner.message('Resolving dependencies...'); taskResolution = await this.dependencyResolver.resolve(projectRoot, regularModulesForResolution, {
const resolution = await this.dependencyResolver.resolve(projectRoot, regularModulesForResolution, {
verbose: config.verbose, verbose: config.verbose,
moduleManager: tempModuleManager, moduleManager: tempModuleManager,
}); });
return 'Dependencies resolved';
},
});
// Install modules with their dependencies // Module installation task
if (allModules && allModules.length > 0) { if (allModules && allModules.length > 0) {
installTasks.push({
title: isQuickUpdate ? `Updating ${allModules.length} module(s)` : `Installing ${allModules.length} module(s)`,
task: async (message) => {
const resolution = taskResolution;
const installedModuleNames = new Set(); const installedModuleNames = new Set();
for (const moduleName of allModules) { for (const moduleName of allModules) {
// Skip if already installed if (installedModuleNames.has(moduleName)) continue;
if (installedModuleNames.has(moduleName)) {
continue;
}
installedModuleNames.add(moduleName); installedModuleNames.add(moduleName);
// Show appropriate message based on whether this is a quick update message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
const isQuickUpdate = config._quickUpdate || false;
spinner.message(`${isQuickUpdate ? 'Updating' : 'Installing'} module: ${moduleName}...`);
// Check if this is a custom module // Check if this is a custom module
let isCustomModule = false; let isCustomModule = false;
let customInfo = null; let customInfo = null;
let useCache = false;
// First check if we have a cached version // First check if we have a cached version
if (finalCustomContent && finalCustomContent.cachedModules) { if (finalCustomContent && finalCustomContent.cachedModules) {
const cachedModule = finalCustomContent.cachedModules.find((m) => m.id === moduleName); const cachedModule = finalCustomContent.cachedModules.find((m) => m.id === moduleName);
if (cachedModule) { if (cachedModule) {
isCustomModule = true; isCustomModule = true;
customInfo = { customInfo = { id: moduleName, path: cachedModule.cachePath, config: {} };
id: moduleName,
path: cachedModule.cachePath,
config: {},
};
useCache = true;
} }
} }
// Then check if we have custom module sources from the manifest (for quick update) // Then check custom module sources from manifest (for quick update)
if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) { if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) {
customInfo = config._customModuleSources.get(moduleName); customInfo = config._customModuleSources.get(moduleName);
isCustomModule = true; isCustomModule = true;
// Check if this is a cached module (source path starts with _config)
if ( if (
customInfo.sourcePath && customInfo.sourcePath &&
(customInfo.sourcePath.startsWith('_config') || customInfo.sourcePath.includes('_config/custom')) (customInfo.sourcePath.startsWith('_config') || customInfo.sourcePath.includes('_config/custom')) &&
) { !customInfo.path
useCache = true; )
// Make sure we have the right path structure
if (!customInfo.path) {
customInfo.path = customInfo.sourcePath; customInfo.path = customInfo.sourcePath;
} }
}
}
// Finally check regular custom content // Finally check regular custom content
if (!isCustomModule && finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) { if (!isCustomModule && finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
@ -875,16 +971,12 @@ class Installer {
} }
if (isCustomModule && customInfo) { if (isCustomModule && customInfo) {
// Custom modules are now installed via ModuleManager just like standard modules
// The custom module path should already be in customModulePaths from earlier setup
if (!customModulePaths.has(moduleName) && customInfo.path) { if (!customModulePaths.has(moduleName) && customInfo.path) {
customModulePaths.set(moduleName, customInfo.path); customModulePaths.set(moduleName, customInfo.path);
this.moduleManager.setCustomModulePaths(customModulePaths); this.moduleManager.setCustomModulePaths(customModulePaths);
} }
const collectedModuleConfig = moduleConfigs[moduleName] || {}; const collectedModuleConfig = moduleConfigs[moduleName] || {};
// Use ModuleManager to install the custom module
await this.moduleManager.install( await this.moduleManager.install(
moduleName, moduleName,
bmadDir, bmadDir,
@ -894,19 +986,19 @@ class Installer {
{ {
isCustom: true, isCustom: true,
moduleConfig: collectedModuleConfig, moduleConfig: collectedModuleConfig,
isQuickUpdate: config._quickUpdate || false, isQuickUpdate: isQuickUpdate,
installer: this, installer: this,
silent: true, silent: true,
}, },
); );
// Create module config (include collected config from module.yaml prompts)
await this.generateModuleConfigs(bmadDir, { await this.generateModuleConfigs(bmadDir, {
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig }, [moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
}); });
} else { } else {
// Regular module installation if (!resolution || !resolution.byModule) {
// Special case for core module addResult(`Module: ${moduleName}`, 'warn', 'skipped (no resolution data)');
continue;
}
if (moduleName === 'core') { if (moduleName === 'core') {
await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]); await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]);
} else { } else {
@ -918,6 +1010,9 @@ class Installer {
} }
// Install partial modules (only dependencies) // Install partial modules (only dependencies)
if (!resolution || !resolution.byModule) {
return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
}
for (const [module, files] of Object.entries(resolution.byModule)) { for (const [module, files] of Object.entries(resolution.byModule)) {
if (!allModules.includes(module) && module !== 'core') { if (!allModules.includes(module) && module !== 'core') {
const totalFiles = const totalFiles =
@ -928,107 +1023,185 @@ class Installer {
files.data.length + files.data.length +
files.other.length; files.other.length;
if (totalFiles > 0) { if (totalFiles > 0) {
spinner.message(`Installing ${module} dependencies...`); message(`Installing ${module} dependencies...`);
await this.installPartialModule(module, bmadDir, files); await this.installPartialModule(module, bmadDir, files);
} }
} }
} }
return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
},
});
} }
// All content is now installed as modules - no separate custom content handling needed // Module directory creation task
installTasks.push({
title: 'Creating module directories',
task: async (message) => {
const resolution = taskResolution;
if (!resolution || !resolution.byModule) {
addResult('Module directories', 'warn', 'no resolution data');
return 'Module directories skipped (no resolution data)';
}
const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose;
const moduleLogger = {
log: async (msg) => (verboseMode ? await prompts.log.message(msg) : undefined),
error: async (msg) => await prompts.log.error(msg),
warn: async (msg) => await prompts.log.warn(msg),
};
// Core module directories
if (config.installCore || resolution.byModule.core) {
const result = await this.moduleManager.createModuleDirectories('core', bmadDir, {
installedIDEs: config.ides || [],
moduleConfig: moduleConfigs.core || {},
existingModuleConfig: this.configCollector.existingConfig?.core || {},
coreConfig: moduleConfigs.core || {},
logger: moduleLogger,
silent: true,
});
if (result) {
dirResults.createdDirs.push(...result.createdDirs);
dirResults.movedDirs.push(...(result.movedDirs || []));
dirResults.createdWdsFolders.push(...result.createdWdsFolders);
}
}
// User-selected module directories
if (config.modules && config.modules.length > 0) {
for (const moduleName of config.modules) {
message(`Setting up ${moduleName}...`);
const result = await this.moduleManager.createModuleDirectories(moduleName, bmadDir, {
installedIDEs: config.ides || [],
moduleConfig: moduleConfigs[moduleName] || {},
existingModuleConfig: this.configCollector.existingConfig?.[moduleName] || {},
coreConfig: moduleConfigs.core || {},
logger: moduleLogger,
silent: true,
});
if (result) {
dirResults.createdDirs.push(...result.createdDirs);
dirResults.movedDirs.push(...(result.movedDirs || []));
dirResults.createdWdsFolders.push(...result.createdWdsFolders);
}
}
}
addResult('Module directories', 'ok');
return 'Module directories created';
},
});
// Configuration generation task (stored as named reference for deferred execution)
const configTask = {
title: 'Generating configurations',
task: async (message) => {
// Generate clean config.yaml files for each installed module // Generate clean config.yaml files for each installed module
spinner.message('Generating module configurations...');
await this.generateModuleConfigs(bmadDir, moduleConfigs); await this.generateModuleConfigs(bmadDir, moduleConfigs);
addResult('Configurations', 'ok', 'generated'); addResult('Configurations', 'ok', 'generated');
// Create agent configuration files // Pre-register manifest files
// Note: Legacy createAgentConfigs removed - using YAML customize system instead
// Customize templates are now created in processAgentFiles when building YAML agents
// Pre-register manifest files that will be created (except files-manifest.csv to avoid recursion)
const cfgDir = path.join(bmadDir, '_config'); const cfgDir = path.join(bmadDir, '_config');
this.installedFiles.add(path.join(cfgDir, 'manifest.yaml')); this.installedFiles.add(path.join(cfgDir, 'manifest.yaml'));
this.installedFiles.add(path.join(cfgDir, 'workflow-manifest.csv')); this.installedFiles.add(path.join(cfgDir, 'workflow-manifest.csv'));
this.installedFiles.add(path.join(cfgDir, 'agent-manifest.csv')); this.installedFiles.add(path.join(cfgDir, 'agent-manifest.csv'));
this.installedFiles.add(path.join(cfgDir, 'task-manifest.csv')); this.installedFiles.add(path.join(cfgDir, 'task-manifest.csv'));
// Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes BEFORE IDE setup // Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes
// This must happen BEFORE mergeModuleHelpCatalogs because it depends on agent-manifest.csv // This must happen BEFORE mergeModuleHelpCatalogs because it depends on agent-manifest.csv
spinner.message('Generating workflow and agent manifests...'); message('Generating manifests...');
const manifestGen = new ManifestGenerator(); const manifestGen = new ManifestGenerator();
// For quick update, we need ALL installed modules in the manifest
// Not just the ones being updated
const allModulesForManifest = config._quickUpdate const allModulesForManifest = config._quickUpdate
? config._existingModules || allModules || [] ? config._existingModules || allModules || []
: config._preserveModules : config._preserveModules
? [...allModules, ...config._preserveModules] ? [...allModules, ...config._preserveModules]
: allModules || []; : allModules || [];
// For regular installs (including when called from quick update), use what we have
let modulesForCsvPreserve; let modulesForCsvPreserve;
if (config._quickUpdate) { if (config._quickUpdate) {
// Quick update - use existing modules or fall back to modules being updated
modulesForCsvPreserve = config._existingModules || allModules || []; modulesForCsvPreserve = config._existingModules || allModules || [];
} else { } else {
// Regular install - use the modules we're installing plus any preserved ones
modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules; modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
} }
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], { const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], {
ides: config.ides || [], ides: config.ides || [],
preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir preservedModules: modulesForCsvPreserve,
}); });
// Custom modules are now included in the main modules list - no separate tracking needed
addResult( addResult(
'Manifests', 'Manifests',
'ok', 'ok',
`${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools`, `${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools`,
); );
// Merge all module-help.csv files into bmad-help.csv // Merge help catalogs
// This must happen AFTER generateManifests because it depends on agent-manifest.csv message('Generating help catalog...');
spinner.message('Generating workflow help catalog...');
await this.mergeModuleHelpCatalogs(bmadDir); await this.mergeModuleHelpCatalogs(bmadDir);
addResult('Help catalog', 'ok'); addResult('Help catalog', 'ok');
// Configure IDEs and copy documentation return 'Configurations generated';
if (!config.skipIde && config.ides && config.ides.length > 0) { },
// Ensure IDE manager is initialized (handlers may not be loaded in quick update flow) };
await this.ideManager.ensureInitialized(); installTasks.push(configTask);
// Filter out any undefined/null values from the IDE list // Run all tasks except config (which runs after directory output)
const mainTasks = installTasks.filter((t) => t !== configTask);
await prompts.tasks(mainTasks);
// Render directory creation output right after directory task
const color = await prompts.getColor();
if (dirResults.movedDirs.length > 0) {
const lines = dirResults.movedDirs.map((d) => ` ${d}`).join('\n');
await prompts.log.message(color.cyan(`Moved directories:\n${lines}`));
}
if (dirResults.createdDirs.length > 0) {
const lines = dirResults.createdDirs.map((d) => ` ${d}`).join('\n');
await prompts.log.message(color.yellow(`Created directories:\n${lines}`));
}
if (dirResults.createdWdsFolders.length > 0) {
const lines = dirResults.createdWdsFolders.map((f) => color.dim(` \u2713 ${f}/`)).join('\n');
await prompts.log.message(color.cyan(`Created WDS folder structure:\n${lines}`));
}
// Now run configuration generation
await prompts.tasks([configTask]);
// Resolution is now available via closure-scoped taskResolution
const resolution = taskResolution;
// ─────────────────────────────────────────────────────────────────────────
// IDE SETUP: Keep as spinner since it may prompt for user input
// ─────────────────────────────────────────────────────────────────────────
if (!config.skipIde && config.ides && config.ides.length > 0) {
await this.ideManager.ensureInitialized();
const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string'); const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string');
if (validIdes.length === 0) { if (validIdes.length === 0) {
addResult('IDE configuration', 'warn', 'no valid IDEs selected'); addResult('IDE configuration', 'warn', 'no valid IDEs selected');
} else { } else {
// Check if any IDE might need prompting (no pre-collected config)
const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]); const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]);
const ideSpinner = await prompts.spinner();
// Temporarily suppress console output if not verbose ideSpinner.start('Configuring tools...');
const originalLog = console.log;
if (!config.verbose) {
console.log = () => {};
}
try { try {
for (const ide of validIdes) { for (const ide of validIdes) {
if (!needsPrompting || ideConfigurations[ide]) { if (!needsPrompting || ideConfigurations[ide]) {
// All IDEs pre-configured, or this specific IDE has config: keep spinner running ideSpinner.message(`Configuring ${ide}...`);
spinner.message(`Configuring ${ide}...`);
} else { } else {
// This IDE needs prompting: stop spinner to allow user interaction if (ideSpinner.isSpinning) {
if (spinner.isSpinning) { ideSpinner.stop('Ready for IDE configuration');
spinner.stop('Ready for IDE configuration');
} }
} }
// Silent when this IDE has pre-collected config (no prompts for THIS IDE) // Suppress stray console output for pre-configured IDEs (no user interaction)
const ideHasConfig = Boolean(ideConfigurations[ide]); const ideHasConfig = Boolean(ideConfigurations[ide]);
const originalLog = console.log;
if (!config.verbose && ideHasConfig) {
console.log = () => {};
}
try {
const setupResult = await this.ideManager.setup(ide, projectDir, bmadDir, { const setupResult = await this.ideManager.setup(ide, projectDir, bmadDir, {
selectedModules: allModules || [], selectedModules: allModules || [],
preCollectedConfig: ideConfigurations[ide] || null, preCollectedConfig: ideConfigurations[ide] || null,
@ -1036,80 +1209,49 @@ class Installer {
silent: ideHasConfig, silent: ideHasConfig,
}); });
// Save IDE configuration for future updates
if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) { if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) {
await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]); await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]);
} }
// Collect result for summary
if (setupResult.success) { if (setupResult.success) {
addResult(ide, 'ok', setupResult.detail || ''); addResult(ide, 'ok', setupResult.detail || '');
} else { } else {
addResult(ide, 'error', setupResult.error || 'failed'); addResult(ide, 'error', setupResult.error || 'failed');
} }
// Restart spinner if we stopped it for prompting
if (needsPrompting && !spinner.isSpinning) {
spinner.start('Configuring IDEs...');
}
}
} finally { } finally {
console.log = originalLog; console.log = originalLog;
} }
if (needsPrompting && !ideSpinner.isSpinning) {
ideSpinner.start('Configuring tools...');
}
}
} finally {
if (ideSpinner.isSpinning) {
ideSpinner.stop('Tool configuration complete');
}
}
} }
} }
// Run module-specific installers after IDE setup // ─────────────────────────────────────────────────────────────────────────
spinner.message('Running module-specific installers...'); // SECOND TASKS BLOCK: Post-IDE operations (non-interactive)
// ─────────────────────────────────────────────────────────────────────────
const postIdeTasks = [];
// Create a conditional logger based on verbose mode // File restoration task (only for updates)
const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose; if (
const moduleLogger = { config._isUpdate &&
log: (msg) => (verboseMode ? console.log(msg) : {}), // Only log in verbose mode ((config._customFiles && config._customFiles.length > 0) || (config._modifiedFiles && config._modifiedFiles.length > 0))
error: (msg) => console.error(msg), // Always show errors ) {
warn: (msg) => console.warn(msg), // Always show warnings postIdeTasks.push({
}; title: 'Finalizing installation',
task: async (message) => {
// Run core module installer if core was installed
if (config.installCore || resolution.byModule.core) {
spinner.message('Running core module installer...');
await this.moduleManager.runModuleInstaller('core', bmadDir, {
installedIDEs: config.ides || [],
moduleConfig: moduleConfigs.core || {},
coreConfig: moduleConfigs.core || {},
logger: moduleLogger,
silent: true,
});
}
// Run installers for user-selected modules
if (config.modules && config.modules.length > 0) {
for (const moduleName of config.modules) {
spinner.message(`Running ${moduleName} module installer...`);
// Pass installed IDEs and module config to module installer
await this.moduleManager.runModuleInstaller(moduleName, bmadDir, {
installedIDEs: config.ides || [],
moduleConfig: moduleConfigs[moduleName] || {},
coreConfig: moduleConfigs.core || {},
logger: moduleLogger,
silent: true,
});
}
}
addResult('Module installers', 'ok');
// Note: Manifest files are already created by ManifestGenerator above
// No need to create legacy manifest.csv anymore
// If this was an update, restore custom files
let customFiles = []; let customFiles = [];
let modifiedFiles = []; let modifiedFiles = [];
if (config._isUpdate) {
if (config._customFiles && config._customFiles.length > 0) { if (config._customFiles && config._customFiles.length > 0) {
spinner.message(`Restoring ${config._customFiles.length} custom files...`); message(`Restoring ${config._customFiles.length} custom files...`);
for (const originalPath of config._customFiles) { for (const originalPath of config._customFiles) {
const relativePath = path.relative(bmadDir, originalPath); const relativePath = path.relative(bmadDir, originalPath);
@ -1121,7 +1263,6 @@ class Installer {
} }
} }
// Clean up temp backup
if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) { if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) {
await fs.remove(config._tempBackupDir); await fs.remove(config._tempBackupDir);
} }
@ -1132,9 +1273,8 @@ class Installer {
if (config._modifiedFiles && config._modifiedFiles.length > 0) { if (config._modifiedFiles && config._modifiedFiles.length > 0) {
modifiedFiles = config._modifiedFiles; modifiedFiles = config._modifiedFiles;
// Restore modified files as .bak files
if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) { if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) {
spinner.message(`Restoring ${modifiedFiles.length} modified files as .bak...`); message(`Restoring ${modifiedFiles.length} modified files as .bak...`);
for (const modifiedFile of modifiedFiles) { for (const modifiedFile of modifiedFiles) {
const relativePath = path.relative(bmadDir, modifiedFile.path); const relativePath = path.relative(bmadDir, modifiedFile.path);
@ -1147,14 +1287,24 @@ class Installer {
} }
} }
// Clean up temp backup
await fs.remove(config._tempModifiedBackupDir); await fs.remove(config._tempModifiedBackupDir);
} }
} }
// Store for summary access
config._restoredCustomFiles = customFiles;
config._restoredModifiedFiles = modifiedFiles;
return 'Installation finalized';
},
});
} }
// Stop the single installation spinner await prompts.tasks(postIdeTasks);
spinner.stop('Installation complete');
// Retrieve restored file info for summary
const customFiles = config._restoredCustomFiles || [];
const modifiedFiles = config._restoredModifiedFiles || [];
// Render consolidated summary // Render consolidated summary
await this.renderInstallSummary(results, { await this.renderInstallSummary(results, {
@ -1173,7 +1323,15 @@ class Installer {
projectDir: projectDir, projectDir: projectDir,
}; };
} catch (error) { } catch (error) {
try {
if (spinner.isSpinning) {
spinner.error('Installation failed'); spinner.error('Installation failed');
} else {
await prompts.log.error('Installation failed');
}
} catch {
// Ensure the original error is never swallowed by a logging failure
}
throw error; throw error;
} }
} }
@ -1201,19 +1359,11 @@ class Installer {
lines.push(` ${icon} ${r.step}${detail}`); lines.push(` ${icon} ${r.step}${detail}`);
} }
// Add context info // Context and warnings
lines.push(''); lines.push('');
if (context.bmadDir) { if (context.bmadDir) {
lines.push(` Installed to: ${color.dim(context.bmadDir)}`); lines.push(` Installed to: ${color.dim(context.bmadDir)}`);
} }
if (context.modules && context.modules.length > 0) {
lines.push(` Modules: ${color.dim(context.modules.join(', '))}`);
}
if (context.ides && context.ides.length > 0) {
lines.push(` Tools: ${color.dim(context.ides.join(', '))}`);
}
// Custom/modified file warnings
if (context.customFiles && context.customFiles.length > 0) { if (context.customFiles && context.customFiles.length > 0) {
lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`); lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`);
} }
@ -1221,6 +1371,14 @@ class Installer {
lines.push(` ${color.yellow(`Modified files backed up (.bak): ${context.modifiedFiles.length}`)}`); lines.push(` ${color.yellow(`Modified files backed up (.bak): ${context.modifiedFiles.length}`)}`);
} }
// Next steps
lines.push(
'',
' Next steps:',
` Docs: ${color.dim('https://docs.bmad-method.org/')}`,
` Run ${color.cyan('/bmad-help')} in your IDE to get started`,
);
await prompts.note(lines.join('\n'), 'BMAD is ready to use!'); await prompts.note(lines.join('\n'), 'BMAD is ready to use!');
} }
@ -1305,6 +1463,7 @@ class Installer {
projectRoot, projectRoot,
'update', 'update',
existingInstall.modules.map((m) => m.id), existingInstall.modules.map((m) => m.id),
config.skipPrompts || false,
); );
spinner.start('Preparing update...'); spinner.start('Preparing update...');
@ -1912,8 +2071,8 @@ class Installer {
continue; continue;
} }
// Skip _module-installer directory - it's only needed at install time // Skip module.yaml at root - it's only needed at install time
if (file.startsWith('_module-installer/') || file === 'module.yaml') { if (file === 'module.yaml') {
continue; continue;
} }
@ -1966,10 +2125,6 @@ class Installer {
const fullPath = path.join(dir, entry.name); const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) { if (entry.isDirectory()) {
// Skip _module-installer directories
if (entry.name === '_module-installer') {
continue;
}
const subFiles = await this.getFileList(fullPath, baseDir); const subFiles = await this.getFileList(fullPath, baseDir);
files.push(...subFiles); files.push(...subFiles);
} else { } else {
@ -2172,6 +2327,7 @@ class Installer {
projectRoot, projectRoot,
'update', 'update',
installedModules, installedModules,
config.skipPrompts || false,
); );
const { validCustomModules, keptModulesWithoutSources } = customModuleResult; const { validCustomModules, keptModulesWithoutSources } = customModuleResult;
@ -2429,7 +2585,9 @@ class Installer {
if (proceed === 'exit') { if (proceed === 'exit') {
await prompts.log.info('Please remove the .bmad-method folder and any v4 rules/commands, then run the installer again.'); await prompts.log.info('Please remove the .bmad-method folder and any v4 rules/commands, then run the installer again.');
process.exit(0); // Allow event loop to flush pending I/O before exit
setImmediate(() => process.exit(0));
return;
} }
await prompts.log.warn('Proceeding with installation despite legacy v4 folder'); await prompts.log.warn('Proceeding with installation despite legacy v4 folder');
@ -2613,9 +2771,10 @@ class Installer {
* @param {string} projectRoot - Project root directory * @param {string} projectRoot - Project root directory
* @param {string} operation - Current operation ('update', 'compile', etc.) * @param {string} operation - Current operation ('update', 'compile', etc.)
* @param {Array} installedModules - Array of installed module IDs (will be modified) * @param {Array} installedModules - Array of installed module IDs (will be modified)
* @param {boolean} [skipPrompts=false] - Skip interactive prompts and keep all modules with missing sources
* @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array * @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array
*/ */
async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules) { async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules, skipPrompts = false) {
const validCustomModules = []; const validCustomModules = [];
const keptModulesWithoutSources = []; // Track modules kept without sources const keptModulesWithoutSources = []; // Track modules kept without sources
const customModulesWithMissingSources = []; const customModulesWithMissingSources = [];
@ -2658,6 +2817,14 @@ class Installer {
}; };
} }
// Non-interactive mode: keep all modules with missing sources
if (skipPrompts) {
for (const missing of customModulesWithMissingSources) {
keptModulesWithoutSources.push(missing.id);
}
return { validCustomModules, keptModulesWithoutSources };
}
await prompts.log.warn(`Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`); await prompts.log.warn(`Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`);
let keptCount = 0; let keptCount = 0;
@ -2722,6 +2889,13 @@ class Installer {
}, },
}); });
// Defensive: handleCancel should have exited, but guard against symbol propagation
if (typeof newSourcePath !== 'string') {
keptCount++;
keptModulesWithoutSources.push(missing.id);
continue;
}
// Update the source in manifest // Update the source in manifest
const resolvedPath = path.resolve(newSourcePath.trim()); const resolvedPath = path.resolve(newSourcePath.trim());
missing.info.sourcePath = resolvedPath; missing.info.sourcePath = resolvedPath;

View File

@ -4,6 +4,7 @@ const yaml = require('yaml');
const crypto = require('node:crypto'); const crypto = require('node:crypto');
const csv = require('csv-parse/sync'); const csv = require('csv-parse/sync');
const { getSourcePath, getModulePath } = require('../../../lib/project-root'); const { getSourcePath, getModulePath } = require('../../../lib/project-root');
const prompts = require('../../../lib/prompts');
// Load package.json for version info // Load package.json for version info
const packageJson = require('../../../../../package.json'); const packageJson = require('../../../../../package.json');
@ -241,7 +242,7 @@ class ManifestGenerator {
} }
} }
} catch (error) { } catch (error) {
console.warn(`Warning: Failed to parse workflow at ${fullPath}: ${error.message}`); await prompts.log.warn(`Failed to parse workflow at ${fullPath}: ${error.message}`);
} }
} }
} }
@ -321,6 +322,7 @@ class ManifestGenerator {
const nameMatch = content.match(/name="([^"]+)"/); const nameMatch = content.match(/name="([^"]+)"/);
const titleMatch = content.match(/title="([^"]+)"/); const titleMatch = content.match(/title="([^"]+)"/);
const iconMatch = content.match(/icon="([^"]+)"/); const iconMatch = content.match(/icon="([^"]+)"/);
const capabilitiesMatch = content.match(/capabilities="([^"]+)"/);
// Extract persona fields // Extract persona fields
const roleMatch = content.match(/<role>([^<]+)<\/role>/); const roleMatch = content.match(/<role>([^<]+)<\/role>/);
@ -342,6 +344,7 @@ class ManifestGenerator {
displayName: nameMatch ? nameMatch[1] : agentName, displayName: nameMatch ? nameMatch[1] : agentName,
title: titleMatch ? titleMatch[1] : '', title: titleMatch ? titleMatch[1] : '',
icon: iconMatch ? iconMatch[1] : '', icon: iconMatch ? iconMatch[1] : '',
capabilities: capabilitiesMatch ? this.cleanForCSV(capabilitiesMatch[1]) : '',
role: roleMatch ? this.cleanForCSV(roleMatch[1]) : '', role: roleMatch ? this.cleanForCSV(roleMatch[1]) : '',
identity: identityMatch ? this.cleanForCSV(identityMatch[1]) : '', identity: identityMatch ? this.cleanForCSV(identityMatch[1]) : '',
communicationStyle: styleMatch ? this.cleanForCSV(styleMatch[1]) : '', communicationStyle: styleMatch ? this.cleanForCSV(styleMatch[1]) : '',
@ -691,7 +694,7 @@ class ManifestGenerator {
return preservedRows; return preservedRows;
} catch (error) { } catch (error) {
console.warn(`Warning: Failed to read existing CSV ${csvPath}:`, error.message); await prompts.log.warn(`Failed to read existing CSV ${csvPath}: ${error.message}`);
return []; return [];
} }
} }
@ -784,7 +787,7 @@ class ManifestGenerator {
} }
// Create CSV header with persona fields // Create CSV header with persona fields
let csvContent = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n'; let csvContent = 'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path\n';
// Combine existing and new agents, preferring new data for duplicates // Combine existing and new agents, preferring new data for duplicates
const allAgents = new Map(); const allAgents = new Map();
@ -802,6 +805,7 @@ class ManifestGenerator {
displayName: agent.displayName, displayName: agent.displayName,
title: agent.title, title: agent.title,
icon: agent.icon, icon: agent.icon,
capabilities: agent.capabilities,
role: agent.role, role: agent.role,
identity: agent.identity, identity: agent.identity,
communicationStyle: agent.communicationStyle, communicationStyle: agent.communicationStyle,
@ -818,6 +822,7 @@ class ManifestGenerator {
escapeCsv(record.displayName), escapeCsv(record.displayName),
escapeCsv(record.title), escapeCsv(record.title),
escapeCsv(record.icon), escapeCsv(record.icon),
escapeCsv(record.capabilities),
escapeCsv(record.role), escapeCsv(record.role),
escapeCsv(record.identity), escapeCsv(record.identity),
escapeCsv(record.communicationStyle), escapeCsv(record.communicationStyle),
@ -1068,7 +1073,7 @@ class ManifestGenerator {
} }
} }
} catch (error) { } catch (error) {
console.warn(`Warning: Could not scan for installed modules: ${error.message}`); await prompts.log.warn(`Could not scan for installed modules: ${error.message}`);
} }
return modules; return modules;

View File

@ -2,6 +2,7 @@ const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const crypto = require('node:crypto'); const crypto = require('node:crypto');
const { getProjectRoot } = require('../../../lib/project-root'); const { getProjectRoot } = require('../../../lib/project-root');
const prompts = require('../../../lib/prompts');
class Manifest { class Manifest {
/** /**
@ -100,7 +101,7 @@ class Manifest {
ides: manifestData.ides || [], ides: manifestData.ides || [],
}; };
} catch (error) { } catch (error) {
console.error('Failed to read YAML manifest:', error.message); await prompts.log.error(`Failed to read YAML manifest: ${error.message}`);
} }
} }
@ -230,7 +231,7 @@ class Manifest {
const content = await fs.readFile(yamlPath, 'utf8'); const content = await fs.readFile(yamlPath, 'utf8');
return yaml.parse(content); return yaml.parse(content);
} catch (error) { } catch (error) {
console.error('Failed to read YAML manifest:', error.message); await prompts.log.error(`Failed to read YAML manifest: ${error.message}`);
} }
} }
@ -472,7 +473,7 @@ class Manifest {
} }
} }
} catch (error) { } catch (error) {
console.warn(`Warning: Could not parse ${filePath}:`, error.message); await prompts.log.warn(`Could not parse ${filePath}: ${error.message}`);
} }
} }
// Handle other file types (CSV, JSON, YAML, etc.) // Handle other file types (CSV, JSON, YAML, etc.)
@ -774,7 +775,7 @@ class Manifest {
configs[moduleName] = yaml.parse(content); configs[moduleName] = yaml.parse(content);
} }
} catch (error) { } catch (error) {
console.warn(`Could not load config for module ${moduleName}:`, error.message); await prompts.log.warn(`Could not load config for module ${moduleName}: ${error.message}`);
} }
} }
@ -876,7 +877,7 @@ class Manifest {
const pkg = require(packageJsonPath); const pkg = require(packageJsonPath);
version = pkg.version; version = pkg.version;
} catch (error) { } catch (error) {
console.warn(`Failed to read package.json for ${moduleName}: ${error.message}`); await prompts.log.warn(`Failed to read package.json for ${moduleName}: ${error.message}`);
} }
} }
} }
@ -904,7 +905,7 @@ class Manifest {
repoUrl: moduleConfig.repoUrl || null, repoUrl: moduleConfig.repoUrl || null,
}; };
} catch (error) { } catch (error) {
console.warn(`Failed to read module.yaml for ${moduleName}: ${error.message}`); await prompts.log.warn(`Failed to read module.yaml for ${moduleName}: ${error.message}`);
} }
} }

View File

@ -55,7 +55,7 @@ class CustomHandler {
// Found a custom.yaml file // Found a custom.yaml file
customPaths.push(fullPath); customPaths.push(fullPath);
} else if ( } else if (
entry.name === 'module.yaml' && // Check if this is a custom module (either in _module-installer or in root directory) entry.name === 'module.yaml' && // Check if this is a custom module (in root directory)
// Skip if it's in src/modules (those are standard modules) // Skip if it's in src/modules (those are standard modules)
!fullPath.includes(path.join('src', 'modules')) !fullPath.includes(path.join('src', 'modules'))
) { ) {

View File

@ -23,6 +23,11 @@ class CodexSetup extends BaseIdeSetup {
* @returns {Object} Collected configuration * @returns {Object} Collected configuration
*/ */
async collectConfiguration(options = {}) { async collectConfiguration(options = {}) {
// Non-interactive mode: use default (global)
if (options.skipPrompts) {
return { installLocation: 'global' };
}
let confirmed = false; let confirmed = false;
let installLocation = 'global'; let installLocation = 'global';

View File

@ -0,0 +1,655 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { BMAD_FOLDER_NAME, toDashPath } = require('./shared/path-utils');
const fs = require('fs-extra');
const csv = require('csv-parse/sync');
const yaml = require('yaml');
/**
* GitHub Copilot setup handler
* Creates agents in .github/agents/, prompts in .github/prompts/,
* copilot-instructions.md, and configures VS Code settings
*/
class GitHubCopilotSetup extends BaseIdeSetup {
constructor() {
super('github-copilot', 'GitHub Copilot', false);
// Don't set configDir to '.github' — nearly every GitHub repo has that directory,
// which would cause the base detect() to false-positive. Use detectionPaths instead.
this.configDir = null;
this.githubDir = '.github';
this.agentsDir = 'agents';
this.promptsDir = 'prompts';
this.detectionPaths = ['.github/copilot-instructions.md', '.github/agents'];
}
/**
* Setup GitHub Copilot configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Create .github/agents and .github/prompts directories
const githubDir = path.join(projectDir, this.githubDir);
const agentsDir = path.join(githubDir, this.agentsDir);
const promptsDir = path.join(githubDir, this.promptsDir);
await this.ensureDir(agentsDir);
await this.ensureDir(promptsDir);
// Preserve any customised tool permissions from existing files before cleanup
this.existingToolPermissions = await this.collectExistingToolPermissions(projectDir);
// Clean up any existing BMAD files before reinstalling
await this.cleanup(projectDir);
// Load agent manifest for enriched descriptions
const agentManifest = await this.loadAgentManifest(bmadDir);
// Generate agent launchers
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
// Create agent .agent.md files
let agentCount = 0;
for (const artifact of agentArtifacts) {
const agentMeta = agentManifest.get(artifact.name);
// Compute fileName first so we can look up any existing tool permissions
const dashName = toDashPath(artifact.relativePath);
const fileName = dashName.replace(/\.md$/, '.agent.md');
const toolsStr = this.getToolsForFile(fileName);
const agentContent = this.createAgentContent(artifact, agentMeta, toolsStr);
const targetPath = path.join(agentsDir, fileName);
await this.writeFile(targetPath, agentContent);
agentCount++;
console.log(chalk.green(` ✓ Created agent: ${fileName}`));
}
// Generate prompt files from bmad-help.csv
const promptCount = await this.generatePromptFiles(projectDir, bmadDir, agentArtifacts, agentManifest);
// Generate copilot-instructions.md
await this.generateCopilotInstructions(projectDir, bmadDir, agentManifest);
console.log(chalk.green(`\n${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents created in .github/agents/`));
console.log(chalk.dim(` - ${promptCount} prompts created in .github/prompts/`));
console.log(chalk.dim(` - copilot-instructions.md generated`));
console.log(chalk.dim(` - Destination: .github/`));
return {
success: true,
results: {
agents: agentCount,
workflows: promptCount,
tasks: 0,
tools: 0,
},
};
}
/**
* Load agent manifest CSV into a Map keyed by agent name
* @param {string} bmadDir - BMAD installation directory
* @returns {Map} Agent metadata keyed by name
*/
async loadAgentManifest(bmadDir) {
const manifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv');
const agents = new Map();
if (!(await fs.pathExists(manifestPath))) {
return agents;
}
try {
const csvContent = await fs.readFile(manifestPath, 'utf8');
const records = csv.parse(csvContent, {
columns: true,
skip_empty_lines: true,
});
for (const record of records) {
agents.set(record.name, record);
}
} catch {
// Gracefully degrade if manifest is unreadable/malformed
}
return agents;
}
/**
* Load bmad-help.csv to drive prompt generation
* @param {string} bmadDir - BMAD installation directory
* @returns {Array|null} Parsed CSV rows
*/
async loadBmadHelp(bmadDir) {
const helpPath = path.join(bmadDir, '_config', 'bmad-help.csv');
if (!(await fs.pathExists(helpPath))) {
return null;
}
try {
const csvContent = await fs.readFile(helpPath, 'utf8');
return csv.parse(csvContent, {
columns: true,
skip_empty_lines: true,
});
} catch {
// Gracefully degrade if help CSV is unreadable/malformed
return null;
}
}
/**
* Create agent .agent.md content with enriched description
* @param {Object} artifact - Agent artifact from AgentCommandGenerator
* @param {Object|undefined} manifestEntry - Agent manifest entry with metadata
* @returns {string} Agent file content
*/
createAgentContent(artifact, manifestEntry, toolsStr) {
// Build enriched description from manifest metadata
let description;
if (manifestEntry) {
const persona = manifestEntry.displayName || artifact.name;
const title = manifestEntry.title || this.formatTitle(artifact.name);
const capabilities = manifestEntry.capabilities || 'agent capabilities';
description = `${persona}${title}: ${capabilities}`;
} else {
description = `Activates the ${this.formatTitle(artifact.name)} agent persona.`;
}
// Build the agent file path for the activation block
const agentPath = artifact.agentPath || artifact.relativePath;
const agentFilePath = `{project-root}/${this.bmadFolderName}/${agentPath}`;
return `---
description: '${description.replaceAll("'", "''")}'
tools: ${toolsStr}
disable-model-invocation: true
---
You must fully embody this agent's persona and follow all activation instructions exactly as specified.
<agent-activation CRITICAL="TRUE">
1. LOAD the FULL agent file from ${agentFilePath}
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
3. FOLLOW every step in the <activation> section precisely
4. DISPLAY the welcome/greeting as instructed
5. PRESENT the numbered menu
6. WAIT for user input before proceeding
</agent-activation>
`;
}
/**
* Generate .prompt.md files for workflows, tasks, tech-writer commands, and agent activators
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Array} agentArtifacts - Agent artifacts for activator generation
* @param {Map} agentManifest - Agent manifest data
* @returns {number} Count of prompts generated
*/
async generatePromptFiles(projectDir, bmadDir, agentArtifacts, agentManifest) {
const promptsDir = path.join(projectDir, this.githubDir, this.promptsDir);
let promptCount = 0;
// Load bmad-help.csv to drive workflow/task prompt generation
const helpEntries = await this.loadBmadHelp(bmadDir);
if (helpEntries) {
for (const entry of helpEntries) {
const command = entry.command;
if (!command) continue; // Skip entries without a command (tech-writer commands have no command column)
const workflowFile = entry['workflow-file'];
if (!workflowFile) continue; // Skip entries with no workflow file path
const promptFileName = `${command}.prompt.md`;
const toolsStr = this.getToolsForFile(promptFileName);
const promptContent = this.createWorkflowPromptContent(entry, workflowFile, toolsStr);
const promptPath = path.join(promptsDir, promptFileName);
await this.writeFile(promptPath, promptContent);
promptCount++;
}
// Generate tech-writer command prompts (entries with no command column)
for (const entry of helpEntries) {
if (entry.command) continue; // Already handled above
const techWriterPrompt = this.createTechWriterPromptContent(entry);
if (techWriterPrompt) {
const promptFileName = `${techWriterPrompt.fileName}.prompt.md`;
const promptPath = path.join(promptsDir, promptFileName);
await this.writeFile(promptPath, techWriterPrompt.content);
promptCount++;
}
}
}
// Generate agent activator prompts (Pattern D)
for (const artifact of agentArtifacts) {
const agentMeta = agentManifest.get(artifact.name);
const fileName = `bmad-${artifact.name}.prompt.md`;
const toolsStr = this.getToolsForFile(fileName);
const promptContent = this.createAgentActivatorPromptContent(artifact, agentMeta, toolsStr);
const promptPath = path.join(promptsDir, fileName);
await this.writeFile(promptPath, promptContent);
promptCount++;
}
return promptCount;
}
/**
* Create prompt content for a workflow/task entry from bmad-help.csv
* Determines the pattern (A, B, or A for .xml tasks) based on file extension
* @param {Object} entry - bmad-help.csv row
* @param {string} workflowFile - Workflow file path
* @returns {string} Prompt file content
*/
createWorkflowPromptContent(entry, workflowFile, toolsStr) {
const description = this.escapeYamlSingleQuote(this.createPromptDescription(entry.name));
// bmm/config.yaml is safe to hardcode here: these prompts are only generated when
// bmad-help.csv exists (bmm module data), so bmm is guaranteed to be installed.
const configLine = `1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables`;
let body;
if (workflowFile.endsWith('.yaml')) {
// Pattern B: YAML-based workflows — use workflow engine
body = `${configLine}
2. Load the workflow engine at {project-root}/${this.bmadFolderName}/core/tasks/workflow.xml
3. Load and execute the workflow configuration at {project-root}/${workflowFile} using the engine from step 2`;
} else if (workflowFile.endsWith('.xml')) {
// Pattern A variant: XML tasks — load and execute directly
body = `${configLine}
2. Load and execute the task at {project-root}/${workflowFile}`;
} else {
// Pattern A: MD workflows — load and follow directly
body = `${configLine}
2. Load and follow the workflow at {project-root}/${workflowFile}`;
}
return `---
description: '${description}'
agent: 'agent'
tools: ${toolsStr}
---
${body}
`;
}
/**
* Create a short 2-5 word description for a prompt from the entry name
* @param {string} name - Entry name from bmad-help.csv
* @returns {string} Short description
*/
createPromptDescription(name) {
const descriptionMap = {
'Brainstorm Project': 'Brainstorm ideas',
'Market Research': 'Market research',
'Domain Research': 'Domain research',
'Technical Research': 'Technical research',
'Create Brief': 'Create product brief',
'Create PRD': 'Create PRD',
'Validate PRD': 'Validate PRD',
'Edit PRD': 'Edit PRD',
'Create UX': 'Create UX design',
'Create Architecture': 'Create architecture',
'Create Epics and Stories': 'Create epics and stories',
'Check Implementation Readiness': 'Check implementation readiness',
'Sprint Planning': 'Sprint planning',
'Sprint Status': 'Sprint status',
'Create Story': 'Create story',
'Validate Story': 'Validate story',
'Dev Story': 'Dev story',
'QA Automation Test': 'QA automation',
'Code Review': 'Code review',
Retrospective: 'Retrospective',
'Document Project': 'Document project',
'Generate Project Context': 'Generate project context',
'Quick Spec': 'Quick spec',
'Quick Dev': 'Quick dev',
'Correct Course': 'Correct course',
Brainstorming: 'Brainstorm ideas',
'Party Mode': 'Party mode',
'bmad-help': 'BMAD help',
'Index Docs': 'Index documents',
'Shard Document': 'Shard document',
'Editorial Review - Prose': 'Editorial review prose',
'Editorial Review - Structure': 'Editorial review structure',
'Adversarial Review (General)': 'Adversarial review',
};
return descriptionMap[name] || name;
}
/**
* Create prompt content for tech-writer agent-only commands (Pattern C)
* @param {Object} entry - bmad-help.csv row
* @returns {Object|null} { fileName, content } or null if not a tech-writer command
*/
createTechWriterPromptContent(entry) {
if (entry['agent-name'] !== 'tech-writer') return null;
const techWriterCommands = {
'Write Document': { code: 'WD', file: 'bmad-bmm-write-document', description: 'Write document' },
'Update Standards': { code: 'US', file: 'bmad-bmm-update-standards', description: 'Update standards' },
'Mermaid Generate': { code: 'MG', file: 'bmad-bmm-mermaid-generate', description: 'Mermaid generate' },
'Validate Document': { code: 'VD', file: 'bmad-bmm-validate-document', description: 'Validate document' },
'Explain Concept': { code: 'EC', file: 'bmad-bmm-explain-concept', description: 'Explain concept' },
};
const cmd = techWriterCommands[entry.name];
if (!cmd) return null;
const safeDescription = this.escapeYamlSingleQuote(cmd.description);
const toolsStr = this.getToolsForFile(`${cmd.file}.prompt.md`);
const content = `---
description: '${safeDescription}'
agent: 'agent'
tools: ${toolsStr}
---
1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables
2. Load the full agent file from {project-root}/${this.bmadFolderName}/bmm/agents/tech-writer/tech-writer.md and activate the Paige (Technical Writer) persona
3. Execute the ${entry.name} menu command (${cmd.code})
`;
return { fileName: cmd.file, content };
}
/**
* Create agent activator prompt content (Pattern D)
* @param {Object} artifact - Agent artifact
* @param {Object|undefined} manifestEntry - Agent manifest entry
* @returns {string} Prompt file content
*/
createAgentActivatorPromptContent(artifact, manifestEntry, toolsStr) {
let description;
if (manifestEntry) {
description = manifestEntry.title || this.formatTitle(artifact.name);
} else {
description = this.formatTitle(artifact.name);
}
const safeDescription = this.escapeYamlSingleQuote(description);
const agentPath = artifact.agentPath || artifact.relativePath;
const agentFilePath = `{project-root}/${this.bmadFolderName}/${agentPath}`;
// bmm/config.yaml is safe to hardcode: agent activators are only generated from
// bmm agent artifacts, so bmm is guaranteed to be installed.
return `---
description: '${safeDescription}'
agent: 'agent'
tools: ${toolsStr}
---
1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables
2. Load the full agent file from ${agentFilePath}
3. Follow ALL activation instructions in the agent file
4. Display the welcome/greeting as instructed
5. Present the numbered menu
6. Wait for user input before proceeding
`;
}
/**
* Generate copilot-instructions.md from module config
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Map} agentManifest - Agent manifest data
*/
async generateCopilotInstructions(projectDir, bmadDir, agentManifest) {
const configVars = await this.loadModuleConfig(bmadDir);
// Build the agents table from the manifest
let agentsTable = '| Agent | Persona | Title | Capabilities |\n|---|---|---|---|\n';
const agentOrder = [
'bmad-master',
'analyst',
'architect',
'dev',
'pm',
'qa',
'quick-flow-solo-dev',
'sm',
'tech-writer',
'ux-designer',
];
for (const agentName of agentOrder) {
const meta = agentManifest.get(agentName);
if (meta) {
const capabilities = meta.capabilities || 'agent capabilities';
const cleanTitle = (meta.title || '').replaceAll('""', '"');
agentsTable += `| ${agentName} | ${meta.displayName} | ${cleanTitle} | ${capabilities} |\n`;
}
}
const bmad = this.bmadFolderName;
const bmadSection = `# BMAD Method — Project Instructions
## Project Configuration
- **Project**: ${configVars.project_name || '{{project_name}}'}
- **User**: ${configVars.user_name || '{{user_name}}'}
- **Communication Language**: ${configVars.communication_language || '{{communication_language}}'}
- **Document Output Language**: ${configVars.document_output_language || '{{document_output_language}}'}
- **User Skill Level**: ${configVars.user_skill_level || '{{user_skill_level}}'}
- **Output Folder**: ${configVars.output_folder || '{{output_folder}}'}
- **Planning Artifacts**: ${configVars.planning_artifacts || '{{planning_artifacts}}'}
- **Implementation Artifacts**: ${configVars.implementation_artifacts || '{{implementation_artifacts}}'}
- **Project Knowledge**: ${configVars.project_knowledge || '{{project_knowledge}}'}
## BMAD Runtime Structure
- **Agent definitions**: \`${bmad}/bmm/agents/\` (BMM module) and \`${bmad}/core/agents/\` (core)
- **Workflow definitions**: \`${bmad}/bmm/workflows/\` (organized by phase)
- **Core tasks**: \`${bmad}/core/tasks/\` (help, editorial review, indexing, sharding, adversarial review)
- **Core workflows**: \`${bmad}/core/workflows/\` (brainstorming, party-mode, advanced-elicitation)
- **Workflow engine**: \`${bmad}/core/tasks/workflow.xml\` (executes YAML-based workflows)
- **Module configuration**: \`${bmad}/bmm/config.yaml\`
- **Core configuration**: \`${bmad}/core/config.yaml\`
- **Agent manifest**: \`${bmad}/_config/agent-manifest.csv\`
- **Workflow manifest**: \`${bmad}/_config/workflow-manifest.csv\`
- **Help manifest**: \`${bmad}/_config/bmad-help.csv\`
- **Agent memory**: \`${bmad}/_memory/\`
## Key Conventions
- Always load \`${bmad}/bmm/config.yaml\` before any agent activation or workflow execution
- Store all config fields as session variables: \`{user_name}\`, \`{communication_language}\`, \`{output_folder}\`, \`{planning_artifacts}\`, \`{implementation_artifacts}\`, \`{project_knowledge}\`
- MD-based workflows execute directly load and follow the \`.md\` file
- YAML-based workflows require the workflow engine load \`workflow.xml\` first, then pass the \`.yaml\` config
- Follow step-based workflow execution: load steps JIT, never multiple at once
- Save outputs after EACH step when using the workflow engine
- The \`{project-root}\` variable resolves to the workspace root at runtime
## Available Agents
${agentsTable}
## Slash Commands
Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent activators. Agents are also available in the agents dropdown.`;
const instructionsPath = path.join(projectDir, this.githubDir, 'copilot-instructions.md');
const markerStart = '<!-- BMAD:START -->';
const markerEnd = '<!-- BMAD:END -->';
const markedContent = `${markerStart}\n${bmadSection}\n${markerEnd}`;
if (await fs.pathExists(instructionsPath)) {
const existing = await fs.readFile(instructionsPath, 'utf8');
const startIdx = existing.indexOf(markerStart);
const endIdx = existing.indexOf(markerEnd);
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
// Replace only the BMAD section between markers
const before = existing.slice(0, startIdx);
const after = existing.slice(endIdx + markerEnd.length);
const merged = `${before}${markedContent}${after}`;
await this.writeFile(instructionsPath, merged);
console.log(chalk.green(' ✓ Updated BMAD section in copilot-instructions.md'));
} else {
// Existing file without markers — back it up before overwriting
const backupPath = `${instructionsPath}.bak`;
await fs.copy(instructionsPath, backupPath);
console.log(chalk.yellow(` ⚠ Backed up existing copilot-instructions.md → copilot-instructions.md.bak`));
await this.writeFile(instructionsPath, `${markedContent}\n`);
console.log(chalk.green(' ✓ Generated copilot-instructions.md (with BMAD markers)'));
}
} else {
// No existing file — create fresh with markers
await this.writeFile(instructionsPath, `${markedContent}\n`);
console.log(chalk.green(' ✓ Generated copilot-instructions.md'));
}
}
/**
* Load module config.yaml for template variables
* @param {string} bmadDir - BMAD installation directory
* @returns {Object} Config variables
*/
async loadModuleConfig(bmadDir) {
const bmmConfigPath = path.join(bmadDir, 'bmm', 'config.yaml');
const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml');
for (const configPath of [bmmConfigPath, coreConfigPath]) {
if (await fs.pathExists(configPath)) {
try {
const content = await fs.readFile(configPath, 'utf8');
return yaml.parse(content) || {};
} catch {
// Fall through to next config
}
}
}
return {};
}
/**
* Escape a string for use inside YAML single-quoted values.
* In YAML, the only escape inside single quotes is '' for a literal '.
* @param {string} value - Raw string
* @returns {string} Escaped string safe for YAML single-quoted context
*/
escapeYamlSingleQuote(value) {
return (value || '').replaceAll("'", "''");
}
/**
* Scan existing agent and prompt files for customised tool permissions before cleanup.
* Returns a Map<filename, toolsArray> so permissions can be preserved across reinstalls.
* @param {string} projectDir - Project directory
* @returns {Map} Existing tool permissions keyed by filename
*/
async collectExistingToolPermissions(projectDir) {
const permissions = new Map();
const dirs = [
[path.join(projectDir, this.githubDir, this.agentsDir), /^bmad.*\.agent\.md$/],
[path.join(projectDir, this.githubDir, this.promptsDir), /^bmad-.*\.prompt\.md$/],
];
for (const [dir, pattern] of dirs) {
if (!(await fs.pathExists(dir))) continue;
const files = await fs.readdir(dir);
for (const file of files) {
if (!pattern.test(file)) continue;
try {
const content = await fs.readFile(path.join(dir, file), 'utf8');
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (!fmMatch) continue;
const frontmatter = yaml.parse(fmMatch[1]);
if (frontmatter && Array.isArray(frontmatter.tools)) {
permissions.set(file, frontmatter.tools);
}
} catch {
// Skip unreadable files
}
}
}
return permissions;
}
/**
* Get the tools array string for a file, preserving any existing customisation.
* Falls back to the default tools if no prior customisation exists.
* @param {string} fileName - Target filename (e.g. 'bmad-agent-bmm-pm.agent.md')
* @returns {string} YAML inline array string
*/
getToolsForFile(fileName) {
const defaultTools = ['read', 'edit', 'search', 'execute'];
const tools = (this.existingToolPermissions && this.existingToolPermissions.get(fileName)) || defaultTools;
return '[' + tools.map((t) => `'${t}'`).join(', ') + ']';
}
/**
* Format name as title
*/
formatTitle(name) {
return name
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/**
* Cleanup GitHub Copilot configuration - surgically remove only BMAD files
*/
async cleanup(projectDir) {
// Clean up agents directory
const agentsDir = path.join(projectDir, this.githubDir, this.agentsDir);
if (await fs.pathExists(agentsDir)) {
const files = await fs.readdir(agentsDir);
let removed = 0;
for (const file of files) {
if (file.startsWith('bmad') && (file.endsWith('.agent.md') || file.endsWith('.md'))) {
await fs.remove(path.join(agentsDir, file));
removed++;
}
}
if (removed > 0) {
console.log(chalk.dim(` Cleaned up ${removed} existing BMAD agents`));
}
}
// Clean up prompts directory
const promptsDir = path.join(projectDir, this.githubDir, this.promptsDir);
if (await fs.pathExists(promptsDir)) {
const files = await fs.readdir(promptsDir);
let removed = 0;
for (const file of files) {
if (file.startsWith('bmad-') && file.endsWith('.prompt.md')) {
await fs.remove(path.join(promptsDir, file));
removed++;
}
}
if (removed > 0) {
console.log(chalk.dim(` Cleaned up ${removed} existing BMAD prompts`));
}
}
// Note: copilot-instructions.md is NOT cleaned up here.
// generateCopilotInstructions() handles marker-based replacement in a single
// read-modify-write pass, which correctly preserves user content outside the markers.
// Stripping markers here would cause generation to treat the file as legacy (no markers)
// and overwrite user content.
}
}
module.exports = { GitHubCopilotSetup };

View File

@ -8,7 +8,7 @@ const prompts = require('../../../lib/prompts');
* Dynamically discovers and loads IDE handlers * Dynamically discovers and loads IDE handlers
* *
* Loading strategy: * Loading strategy:
* 1. Custom installer files (codex.js, kilo.js) - for platforms with unique installation logic * 1. Custom installer files (codex.js, github-copilot.js, kilo.js) - for platforms with unique installation logic
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns * 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
*/ */
class IdeManager { class IdeManager {
@ -44,7 +44,7 @@ class IdeManager {
/** /**
* Dynamically load all IDE handlers * Dynamically load all IDE handlers
* 1. Load custom installer files first (codex.js, kilo.js) * 1. Load custom installer files first (codex.js, github-copilot.js, kilo.js)
* 2. Load config-driven handlers from platform-codes.yaml * 2. Load config-driven handlers from platform-codes.yaml
*/ */
async loadHandlers() { async loadHandlers() {
@ -61,7 +61,7 @@ class IdeManager {
*/ */
async loadCustomInstallerFiles() { async loadCustomInstallerFiles() {
const ideDir = __dirname; const ideDir = __dirname;
const customFiles = ['codex.js', 'kilo.js']; const customFiles = ['codex.js', 'github-copilot.js', 'kilo.js'];
for (const file of customFiles) { for (const file of customFiles) {
const filePath = path.join(ideDir, file); const filePath = path.join(ideDir, file);

View File

@ -89,11 +89,7 @@ platforms:
preferred: false preferred: false
category: ide category: ide
description: "GitHub's AI pair programmer" description: "GitHub's AI pair programmer"
installer: # No installer config - uses custom github-copilot.js
targets:
- target_dir: .github/agents
template_type: copilot_agents
artifact_types: [agents]
iflow: iflow:
name: "iFlow" name: "iFlow"

View File

@ -1,6 +1,7 @@
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('node:path'); const path = require('node:path');
const yaml = require('yaml'); const yaml = require('yaml');
const prompts = require('../../../lib/prompts');
/** /**
* Manages external official modules defined in external-official-modules.yaml * Manages external official modules defined in external-official-modules.yaml
@ -29,7 +30,7 @@ class ExternalModuleManager {
this.cachedModules = config; this.cachedModules = config;
return config; return config;
} catch (error) { } catch (error) {
console.warn(`Failed to load external modules config: ${error.message}`); await prompts.log.warn(`Failed to load external modules config: ${error.message}`);
return { modules: {} }; return { modules: {} };
} }
} }

View File

@ -236,17 +236,11 @@ class ModuleManager {
async getModuleInfo(modulePath, defaultName, sourceDescription) { async getModuleInfo(modulePath, defaultName, sourceDescription) {
// Check for module structure (module.yaml OR custom.yaml) // Check for module structure (module.yaml OR custom.yaml)
const moduleConfigPath = path.join(modulePath, 'module.yaml'); const moduleConfigPath = path.join(modulePath, 'module.yaml');
const installerConfigPath = path.join(modulePath, '_module-installer', 'module.yaml');
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
const rootCustomConfigPath = path.join(modulePath, 'custom.yaml'); const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
let configPath = null; let configPath = null;
if (await fs.pathExists(moduleConfigPath)) { if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath; configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else if (await fs.pathExists(customConfigPath)) {
configPath = customConfigPath;
} else if (await fs.pathExists(rootCustomConfigPath)) { } else if (await fs.pathExists(rootCustomConfigPath)) {
configPath = rootCustomConfigPath; configPath = rootCustomConfigPath;
} }
@ -268,7 +262,7 @@ class ModuleManager {
description: 'BMAD Module', description: 'BMAD Module',
version: '5.0.0', version: '5.0.0',
source: sourceDescription, source: sourceDescription,
isCustom: configPath === customConfigPath || configPath === rootCustomConfigPath || isCustomSource, isCustom: configPath === rootCustomConfigPath || isCustomSource,
}; };
// Read module config for metadata // Read module config for metadata
@ -458,7 +452,7 @@ class ModuleManager {
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`); installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
} catch (error) { } catch (error) {
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`); installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
if (!silent) await prompts.log.warn(` Warning: ${error.message}`); if (!silent) await prompts.log.warn(` ${error.message}`);
} }
} else { } else {
// Check if package.json is newer than node_modules // Check if package.json is newer than node_modules
@ -484,7 +478,7 @@ class ModuleManager {
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`); installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
} catch (error) { } catch (error) {
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`); installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
if (!silent) await prompts.log.warn(` Warning: ${error.message}`); if (!silent) await prompts.log.warn(` ${error.message}`);
} }
} }
} }
@ -541,21 +535,13 @@ class ModuleManager {
// Check if this is a custom module and read its custom.yaml values // Check if this is a custom module and read its custom.yaml values
let customConfig = null; let customConfig = null;
const rootCustomConfigPath = path.join(sourcePath, 'custom.yaml'); const rootCustomConfigPath = path.join(sourcePath, 'custom.yaml');
const moduleInstallerCustomPath = path.join(sourcePath, '_module-installer', 'custom.yaml');
if (await fs.pathExists(rootCustomConfigPath)) { if (await fs.pathExists(rootCustomConfigPath)) {
try { try {
const customContent = await fs.readFile(rootCustomConfigPath, 'utf8'); const customContent = await fs.readFile(rootCustomConfigPath, 'utf8');
customConfig = yaml.parse(customContent); customConfig = yaml.parse(customContent);
} catch (error) { } catch (error) {
await prompts.log.warn(`Warning: Failed to read custom.yaml for ${moduleName}: ${error.message}`); await prompts.log.warn(`Failed to read custom.yaml for ${moduleName}: ${error.message}`);
}
} else if (await fs.pathExists(moduleInstallerCustomPath)) {
try {
const customContent = await fs.readFile(moduleInstallerCustomPath, 'utf8');
customConfig = yaml.parse(customContent);
} catch (error) {
await prompts.log.warn(`Warning: Failed to read custom.yaml for ${moduleName}: ${error.message}`);
} }
} }
@ -563,7 +549,7 @@ class ModuleManager {
if (customConfig) { if (customConfig) {
options.moduleConfig = { ...options.moduleConfig, ...customConfig }; options.moduleConfig = { ...options.moduleConfig, ...customConfig };
if (options.logger) { if (options.logger) {
options.logger.log(` Merged custom configuration for ${moduleName}`); await options.logger.log(` Merged custom configuration for ${moduleName}`);
} }
} }
@ -585,9 +571,9 @@ class ModuleManager {
// Process agent files to inject activation block // Process agent files to inject activation block
await this.processAgentFiles(targetPath, moduleName); await this.processAgentFiles(targetPath, moduleName);
// Call module-specific installer if it exists (unless explicitly skipped) // Create directories declared in module.yaml (unless explicitly skipped)
if (!options.skipModuleInstaller) { if (!options.skipModuleInstaller) {
await this.runModuleInstaller(moduleName, bmadDir, options); await this.createModuleDirectories(moduleName, bmadDir, options);
} }
// Capture version info for manifest // Capture version info for manifest
@ -743,8 +729,8 @@ class ModuleManager {
continue; continue;
} }
// Skip _module-installer directory - it's only needed at install time // Skip module.yaml at root - it's only needed at install time
if (file.startsWith('_module-installer/') || file === 'module.yaml') { if (file === 'module.yaml') {
continue; continue;
} }
@ -871,7 +857,7 @@ class ModuleManager {
await fs.writeFile(targetFile, strippedYaml, 'utf8'); await fs.writeFile(targetFile, strippedYaml, 'utf8');
} catch { } catch {
// If anything fails, just copy the file as-is // If anything fails, just copy the file as-is
await prompts.log.warn(` Warning: Could not process ${path.basename(sourceFile)}, copying as-is`); await prompts.log.warn(` Could not process ${path.basename(sourceFile)}, copying as-is`);
await fs.copy(sourceFile, targetFile, { overwrite: true }); await fs.copy(sourceFile, targetFile, { overwrite: true });
} }
} }
@ -1026,7 +1012,7 @@ class ModuleManager {
await prompts.log.message(` Sidecar files processed: ${copiedFiles.length} files`); await prompts.log.message(` Sidecar files processed: ${copiedFiles.length} files`);
} }
} else if (process.env.BMAD_VERBOSE_INSTALL === 'true') { } else if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.warn(` Warning: Agent marked as having sidecar but ${sidecarDirName} directory not found`); await prompts.log.warn(` Agent marked as having sidecar but ${sidecarDirName} directory not found`);
} }
} }
@ -1259,64 +1245,177 @@ class ModuleManager {
} }
/** /**
* Run module-specific installer if it exists * Create directories declared in module.yaml's `directories` key
* This replaces the security-risky module installer pattern with declarative config
* During updates, if a directory path changed, moves the old directory to the new path
* @param {string} moduleName - Name of the module * @param {string} moduleName - Name of the module
* @param {string} bmadDir - Target bmad directory * @param {string} bmadDir - Target bmad directory
* @param {Object} options - Installation options * @param {Object} options - Installation options
* @param {Object} options.moduleConfig - Module configuration from config collector
* @param {Object} options.existingModuleConfig - Previous module config (for detecting path changes during updates)
* @param {Object} options.coreConfig - Core configuration
* @returns {Promise<{createdDirs: string[], movedDirs: string[], createdWdsFolders: string[]}>} Created directories info
*/ */
async runModuleInstaller(moduleName, bmadDir, options = {}) { async createModuleDirectories(moduleName, bmadDir, options = {}) {
const moduleConfig = options.moduleConfig || {};
const existingModuleConfig = options.existingModuleConfig || {};
const projectRoot = path.dirname(bmadDir);
const emptyResult = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
// Special handling for core module - it's in src/core not src/modules // Special handling for core module - it's in src/core not src/modules
let sourcePath; let sourcePath;
if (moduleName === 'core') { if (moduleName === 'core') {
sourcePath = getSourcePath('core'); sourcePath = getSourcePath('core');
} else { } else {
sourcePath = await this.findModuleSource(moduleName, { silent: options.silent }); sourcePath = await this.findModuleSource(moduleName, { silent: true });
if (!sourcePath) { if (!sourcePath) {
// No source found, skip module installer return emptyResult; // No source found, skip
return;
} }
} }
const installerPath = path.join(sourcePath, '_module-installer', 'installer.js'); // Read module.yaml to find the `directories` key
const moduleYamlPath = path.join(sourcePath, 'module.yaml');
// Check if module has a custom installer if (!(await fs.pathExists(moduleYamlPath))) {
if (!(await fs.pathExists(installerPath))) { return emptyResult; // No module.yaml, skip
return; // No custom installer
} }
let moduleYaml;
try { try {
// Load the module installer const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
const moduleInstaller = require(installerPath); moduleYaml = yaml.parse(yamlContent);
} catch {
return emptyResult; // Invalid YAML, skip
}
if (typeof moduleInstaller.install === 'function') { if (!moduleYaml || !moduleYaml.directories) {
// Get project root (parent of bmad directory) return emptyResult; // No directories declared, skip
const projectRoot = path.dirname(bmadDir); }
// Prepare logger (use console if not provided) const directories = moduleYaml.directories;
const logger = options.logger || { const wdsFolders = moduleYaml.wds_folders || [];
log: console.log, const createdDirs = [];
error: console.error, const movedDirs = [];
warn: console.warn, const createdWdsFolders = [];
};
// Call the module installer for (const dirRef of directories) {
const result = await moduleInstaller.install({ // Parse variable reference like "{design_artifacts}"
projectRoot, const varMatch = dirRef.match(/^\{([^}]+)\}$/);
config: options.moduleConfig || {}, if (!varMatch) {
coreConfig: options.coreConfig || {}, // Not a variable reference, skip
installedIDEs: options.installedIDEs || [], continue;
logger, }
});
if (!result) { const configKey = varMatch[1];
await prompts.log.warn(`Module installer for ${moduleName} returned false`); const dirValue = moduleConfig[configKey];
if (!dirValue || typeof dirValue !== 'string') {
continue; // No value or not a string, skip
}
// Strip {project-root}/ prefix if present
let dirPath = dirValue.replace(/^\{project-root\}\/?/, '');
// Handle remaining {project-root} anywhere in the path
dirPath = dirPath.replaceAll('{project-root}', '');
// Resolve to absolute path
const fullPath = path.join(projectRoot, dirPath);
// Validate path is within project root (prevent directory traversal)
const normalizedPath = path.normalize(fullPath);
const normalizedRoot = path.normalize(projectRoot);
if (!normalizedPath.startsWith(normalizedRoot + path.sep) && normalizedPath !== normalizedRoot) {
const color = await prompts.getColor();
await prompts.log.warn(color.yellow(`${configKey} path escapes project root, skipping: ${dirPath}`));
continue;
}
// Check if directory path changed from previous config (update/modify scenario)
const oldDirValue = existingModuleConfig[configKey];
let oldFullPath = null;
let oldDirPath = null;
if (oldDirValue && typeof oldDirValue === 'string') {
// F3: Normalize both values before comparing to avoid false negatives
// from trailing slashes, separator differences, or prefix format variations
let normalizedOld = oldDirValue.replace(/^\{project-root\}\/?/, '');
normalizedOld = path.normalize(normalizedOld.replaceAll('{project-root}', ''));
const normalizedNew = path.normalize(dirPath);
if (normalizedOld !== normalizedNew) {
oldDirPath = normalizedOld;
oldFullPath = path.join(projectRoot, oldDirPath);
const normalizedOldAbsolute = path.normalize(oldFullPath);
if (!normalizedOldAbsolute.startsWith(normalizedRoot + path.sep) && normalizedOldAbsolute !== normalizedRoot) {
oldFullPath = null; // Old path escapes project root, ignore it
}
// F13: Prevent parent/child move (e.g. docs/planning → docs/planning/v2)
if (oldFullPath) {
const normalizedNewAbsolute = path.normalize(fullPath);
if (
normalizedOldAbsolute.startsWith(normalizedNewAbsolute + path.sep) ||
normalizedNewAbsolute.startsWith(normalizedOldAbsolute + path.sep)
) {
const color = await prompts.getColor();
await prompts.log.warn(
color.yellow(
`${configKey}: cannot move between parent/child paths (${oldDirPath} / ${dirPath}), creating new directory instead`,
),
);
oldFullPath = null;
} }
} }
} catch (error) {
await prompts.log.error(`Error running module installer for ${moduleName}: ${error.message}`);
} }
} }
const dirName = configKey.replaceAll('_', ' ');
if (oldFullPath && (await fs.pathExists(oldFullPath)) && !(await fs.pathExists(fullPath))) {
// Path changed and old dir exists → move old to new location
// F1: Use fs.move() instead of fs.rename() for cross-device/volume support
// F2: Wrap in try/catch — fallback to creating new dir on failure
try {
await fs.ensureDir(path.dirname(fullPath));
await fs.move(oldFullPath, fullPath);
movedDirs.push(`${dirName}: ${oldDirPath}${dirPath}`);
} catch (moveError) {
const color = await prompts.getColor();
await prompts.log.warn(
color.yellow(
`Failed to move ${oldDirPath}${dirPath}: ${moveError.message}\n Creating new directory instead. Please move contents from the old directory manually.`,
),
);
await fs.ensureDir(fullPath);
createdDirs.push(`${dirName}: ${dirPath}`);
}
} else if (oldFullPath && (await fs.pathExists(oldFullPath)) && (await fs.pathExists(fullPath))) {
// F5: Both old and new directories exist — warn user about potential orphaned documents
const color = await prompts.getColor();
await prompts.log.warn(
color.yellow(
`${dirName}: path changed but both directories exist:\n Old: ${oldDirPath}\n New: ${dirPath}\n Old directory may contain orphaned documents — please review and merge manually.`,
),
);
} else if (!(await fs.pathExists(fullPath))) {
// New directory doesn't exist yet → create it
createdDirs.push(`${dirName}: ${dirPath}`);
await fs.ensureDir(fullPath);
}
// Create WDS subfolders if this is the design_artifacts directory
if (configKey === 'design_artifacts' && wdsFolders.length > 0) {
for (const subfolder of wdsFolders) {
const subPath = path.join(fullPath, subfolder);
if (!(await fs.pathExists(subPath))) {
await fs.ensureDir(subPath);
createdWdsFolders.push(subfolder);
}
}
}
}
return { createdDirs, movedDirs, createdWdsFolders };
}
/** /**
* Private: Process module configuration * Private: Process module configuration
* @param {string} modulePath - Path to installed module * @param {string} modulePath - Path to installed module
@ -1383,10 +1482,6 @@ class ModuleManager {
const fullPath = path.join(dir, entry.name); const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) { if (entry.isDirectory()) {
// Skip _module-installer directories
if (entry.name === '_module-installer') {
continue;
}
const subFiles = await this.getFileList(fullPath, baseDir); const subFiles = await this.getFileList(fullPath, baseDir);
files.push(...subFiles); files.push(...subFiles);
} else { } else {

View File

@ -279,6 +279,9 @@ async function compileToXml(agentYaml, agentName = '', targetPath = '') {
`title="${meta.title || ''}"`, `title="${meta.title || ''}"`,
`icon="${meta.icon || '🤖'}"`, `icon="${meta.icon || '🤖'}"`,
]; ];
if (meta.capabilities) {
agentAttrs.push(`capabilities="${escapeXml(meta.capabilities)}"`);
}
xml += `<agent ${agentAttrs.join(' ')}>\n`; xml += `<agent ${agentAttrs.join(' ')}>\n`;

View File

@ -189,7 +189,7 @@ class UI {
const installedVersion = existingInstall.version || 'unknown'; const installedVersion = existingInstall.version || 'unknown';
// Check if version is pre beta // Check if version is pre beta
const shouldProceed = await this.showLegacyVersionWarning(installedVersion, currentVersion, path.basename(bmadDir)); const shouldProceed = await this.showLegacyVersionWarning(installedVersion, currentVersion, path.basename(bmadDir), options);
// If user chose to cancel, exit the installer // If user chose to cancel, exit the installer
if (!shouldProceed) { if (!shouldProceed) {
@ -227,6 +227,14 @@ class UI {
} }
actionType = options.action; actionType = options.action;
await prompts.log.info(`Using action from command-line: ${actionType}`); await prompts.log.info(`Using action from command-line: ${actionType}`);
} else if (options.yes) {
// Default to quick-update if available, otherwise first available choice
if (choices.length === 0) {
throw new Error('No valid actions available for this installation');
}
const hasQuickUpdate = choices.some((c) => c.value === 'quick-update');
actionType = hasQuickUpdate ? 'quick-update' : choices[0].value;
await prompts.log.info(`Non-interactive mode (--yes): defaulting to ${actionType}`);
} else { } else {
actionType = await prompts.select({ actionType = await prompts.select({
message: 'How would you like to proceed?', message: 'How would you like to proceed?',
@ -242,6 +250,7 @@ class UI {
actionType: 'quick-update', actionType: 'quick-update',
directory: confirmedDirectory, directory: confirmedDirectory,
customContent: { hasCustomContent: false }, customContent: { hasCustomContent: false },
skipPrompts: options.yes || false,
}; };
} }
@ -252,6 +261,7 @@ class UI {
actionType: 'compile-agents', actionType: 'compile-agents',
directory: confirmedDirectory, directory: confirmedDirectory,
customContent: { hasCustomContent: false }, customContent: { hasCustomContent: false },
skipPrompts: options.yes || false,
}; };
} }
@ -272,9 +282,13 @@ class UI {
.map((m) => m.trim()) .map((m) => m.trim())
.filter(Boolean); .filter(Boolean);
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`); await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
} else if (options.yes) {
selectedModules = await this.getDefaultModules(installedModuleIds);
await prompts.log.info(
`Non-interactive mode (--yes): using default modules (installed + defaults): ${selectedModules.join(', ')}`,
);
} else { } else {
selectedModules = await this.selectAllModules(installedModuleIds); selectedModules = await this.selectAllModules(installedModuleIds);
selectedModules = selectedModules.filter((m) => m !== 'core');
} }
// After module selection, ask about custom modules // After module selection, ask about custom modules
@ -331,6 +345,22 @@ class UI {
}, },
}; };
} }
} else if (options.yes) {
// Non-interactive mode: preserve existing custom modules (matches default: false)
const cacheDir = path.join(bmadDir, '_config', 'custom');
if (await fs.pathExists(cacheDir)) {
const entries = await fs.readdir(cacheDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
customModuleResult.selectedCustomModules.push(entry.name);
}
}
await prompts.log.info(
`Non-interactive mode (--yes): preserving ${customModuleResult.selectedCustomModules.length} existing custom module(s)`,
);
} else {
await prompts.log.info('Non-interactive mode (--yes): no existing custom modules found');
}
} else { } else {
const changeCustomModules = await prompts.confirm({ const changeCustomModules = await prompts.confirm({
message: 'Modify custom modules, agents, or workflows?', message: 'Modify custom modules, agents, or workflows?',
@ -362,6 +392,9 @@ class UI {
selectedModules.push(...customModuleResult.selectedCustomModules); selectedModules.push(...customModuleResult.selectedCustomModules);
} }
// Filter out core - it's always installed via installCore flag
selectedModules = selectedModules.filter((m) => m !== 'core');
// Get tool selection // Get tool selection
const toolSelection = await this.promptToolSelection(confirmedDirectory, options); const toolSelection = await this.promptToolSelection(confirmedDirectory, options);
@ -376,6 +409,7 @@ class UI {
skipIde: toolSelection.skipIde, skipIde: toolSelection.skipIde,
coreConfig: coreConfig, coreConfig: coreConfig,
customContent: customModuleResult.customContentConfig, customContent: customModuleResult.customContentConfig,
skipPrompts: options.yes || false,
}; };
} }
} }
@ -527,6 +561,27 @@ class UI {
if (configuredIdes.length > 0) { if (configuredIdes.length > 0) {
const allTools = [...preferredIdes, ...otherIdes]; const allTools = [...preferredIdes, ...otherIdes];
// Non-interactive: handle --tools and --yes flags before interactive prompt
if (options.tools) {
if (options.tools.toLowerCase() === 'none') {
await prompts.log.info('Skipping tool configuration (--tools none)');
return { ides: [], skipIde: true };
}
const selectedIdes = options.tools
.split(',')
.map((t) => t.trim())
.filter(Boolean);
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
return { ides: selectedIdes, skipIde: false };
}
if (options.yes) {
await prompts.log.info(`Non-interactive mode (--yes): keeping configured tools: ${configuredIdes.join(', ')}`);
await this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
return { ides: configuredIdes, skipIde: false };
}
// Sort: configured tools first, then preferred, then others // Sort: configured tools first, then preferred, then others
const sortedTools = [ const sortedTools = [
...allTools.filter((ide) => configuredIdes.includes(ide.value)), ...allTools.filter((ide) => configuredIdes.includes(ide.value)),
@ -689,18 +744,6 @@ class UI {
}); });
} }
/**
* Display installation summary
* @param {Object} result - Installation result
*/
async showInstallSummary(result) {
let summary = `Installed to: ${result.path}`;
if (result.modules && result.modules.length > 0) {
summary += `\nModules: ${result.modules.join(', ')}`;
}
await prompts.note(summary, 'BMAD is ready to use!');
}
/** /**
* Get confirmed directory from user * Get confirmed directory from user
* @returns {string} Confirmed directory path * @returns {string} Confirmed directory path
@ -899,107 +942,10 @@ class UI {
} }
/** /**
* Prompt for module selection * Select all modules (official + community) using grouped multiselect.
* @param {Array} moduleChoices - Available module choices * Core is shown as locked but filtered from the result since it's always installed separately.
* @returns {Array} Selected module IDs
*/
async selectModules(moduleChoices, defaultSelections = null) {
// If defaultSelections is provided, use it to override checked state
// Otherwise preserve the checked state from moduleChoices (set by getModuleChoices)
const choicesWithDefaults = moduleChoices.map((choice) => ({
...choice,
...(defaultSelections === null ? {} : { checked: defaultSelections.includes(choice.value) }),
}));
// Add a "None" option at the end for users who changed their mind
const choicesWithSkipOption = [
...choicesWithDefaults,
{
value: '__NONE__',
label: '\u26A0 None / I changed my mind - skip module installation',
checked: false,
},
];
const selected = await prompts.multiselect({
message: 'Select modules to install (use arrow keys, space to toggle):',
choices: choicesWithSkipOption,
required: true,
});
// If user selected both "__NONE__" and other items, honor the "None" choice
if (selected && selected.includes('__NONE__') && selected.length > 1) {
await prompts.log.warn('"None / I changed my mind" was selected, so no modules will be installed.');
return [];
}
// Filter out the special '__NONE__' value
return selected ? selected.filter((m) => m !== '__NONE__') : [];
}
/**
* Get external module choices for selection
* @returns {Array} External module choices for prompt
*/
async getExternalModuleChoices() {
const externalManager = new ExternalModuleManager();
const modules = await externalManager.listAvailable();
return modules.map((mod) => ({
name: mod.name,
value: mod.code, // Use the code (e.g., 'cis') as the value
checked: mod.defaultSelected || false,
hint: mod.description || undefined, // Show description as hint
module: mod, // Store full module info for later use
}));
}
/**
* Prompt for external module selection
* @param {Array} externalModuleChoices - Available external module choices
* @param {Array} defaultSelections - Module codes to pre-select
* @returns {Array} Selected external module codes
*/
async selectExternalModules(externalModuleChoices, defaultSelections = []) {
// Build a message showing available modules
const message = 'Select official BMad modules to install (use arrow keys, space to toggle):';
// Mark choices as checked based on defaultSelections
const choicesWithDefaults = externalModuleChoices.map((choice) => ({
...choice,
checked: defaultSelections.includes(choice.value),
}));
// Add a "None" option at the end for users who changed their mind
const choicesWithSkipOption = [
...choicesWithDefaults,
{
name: '⚠ None / I changed my mind - skip external module installation',
value: '__NONE__',
checked: false,
},
];
const selected = await prompts.multiselect({
message,
choices: choicesWithSkipOption,
required: true,
});
// If user selected both "__NONE__" and other items, honor the "None" choice
if (selected && selected.includes('__NONE__') && selected.length > 1) {
await prompts.log.warn('"None / I changed my mind" was selected, so no external modules will be installed.');
return [];
}
// Filter out the special '__NONE__' value
return selected ? selected.filter((m) => m !== '__NONE__') : [];
}
/**
* Select all modules (core + official + community) using grouped multiselect
* @param {Set} installedModuleIds - Currently installed module IDs * @param {Set} installedModuleIds - Currently installed module IDs
* @returns {Array} Selected module codes * @returns {Array} Selected module codes (excluding core)
*/ */
async selectAllModules(installedModuleIds = new Set()) { async selectAllModules(installedModuleIds = new Set()) {
const { ModuleManager } = require('../installers/lib/modules/manager'); const { ModuleManager } = require('../installers/lib/modules/manager');
@ -1068,11 +1014,7 @@ class UI {
} }
} }
} }
allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })), { allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })));
// "None" option at the end
label: '\u26A0 None - Skip module installation',
value: '__NONE__',
});
const selected = await prompts.autocompleteMultiselect({ const selected = await prompts.autocompleteMultiselect({
message: 'Select modules to install:', message: 'Select modules to install:',
@ -1083,14 +1025,7 @@ class UI {
maxItems: allOptions.length, maxItems: allOptions.length,
}); });
// If user selected both "__NONE__" and other items, honor the "None" choice const result = selected ? selected.filter((m) => m !== 'core') : [];
if (selected && selected.includes('__NONE__') && selected.length > 1) {
await prompts.log.warn('"None" was selected, so no modules will be installed.');
return [];
}
// Filter out the special '__NONE__' value
const result = selected ? selected.filter((m) => m !== '__NONE__') : [];
// Display selected modules as bulleted list // Display selected modules as bulleted list
if (result.length > 0) { if (result.length > 0) {
@ -1748,7 +1683,7 @@ class UI {
* @param {string} bmadFolderName - Name of the BMAD folder * @param {string} bmadFolderName - Name of the BMAD folder
* @returns {Promise<boolean>} True if user wants to proceed, false if they cancel * @returns {Promise<boolean>} True if user wants to proceed, false if they cancel
*/ */
async showLegacyVersionWarning(installedVersion, currentVersion, bmadFolderName) { async showLegacyVersionWarning(installedVersion, currentVersion, bmadFolderName, options = {}) {
if (!this.isLegacyVersion(installedVersion)) { if (!this.isLegacyVersion(installedVersion)) {
return true; // Not legacy, proceed return true; // Not legacy, proceed
} }
@ -1774,6 +1709,11 @@ class UI {
await prompts.log.warn('VERSION WARNING'); await prompts.log.warn('VERSION WARNING');
await prompts.note(warningContent, 'Version Warning'); await prompts.note(warningContent, 'Version Warning');
if (options.yes) {
await prompts.log.warn('Non-interactive mode (--yes): auto-proceeding with legacy update');
return true;
}
const proceed = await prompts.select({ const proceed = await prompts.select({
message: 'How would you like to proceed?', message: 'How would you like to proceed?',
choices: [ choices: [

View File

@ -228,6 +228,7 @@ function buildMetadataSchema(expectedModule) {
title: createNonEmptyString('agent.metadata.title'), title: createNonEmptyString('agent.metadata.title'),
icon: createNonEmptyString('agent.metadata.icon'), icon: createNonEmptyString('agent.metadata.icon'),
module: createNonEmptyString('agent.metadata.module').optional(), module: createNonEmptyString('agent.metadata.module').optional(),
capabilities: createNonEmptyString('agent.metadata.capabilities').optional(),
hasSidecar: z.boolean(), hasSidecar: z.boolean(),
}; };

View File

@ -42,7 +42,7 @@ const STRICT = process.argv.includes('--strict');
const SCAN_EXTENSIONS = new Set(['.yaml', '.yml', '.md', '.xml', '.csv']); const SCAN_EXTENSIONS = new Set(['.yaml', '.yml', '.md', '.xml', '.csv']);
// Skip directories // Skip directories
const SKIP_DIRS = new Set(['node_modules', '_module-installer', '.git']); const SKIP_DIRS = new Set(['node_modules', '.git']);
// Pattern: {project-root}/_bmad/ references // Pattern: {project-root}/_bmad/ references
const PROJECT_ROOT_REF = /\{project-root\}\/_bmad\/([^\s'"<>})\]`]+)/g; const PROJECT_ROOT_REF = /\{project-root\}\/_bmad\/([^\s'"<>})\]`]+)/g;