Compare commits

..

No commits in common. "63a03b6945a79f3eb0a71572bbf1016197bbe61a" and "ec72336c2efcaa25ca800ce855d9472f8af7e4bb" have entirely different histories.

65 changed files with 935 additions and 1864 deletions

Binary file not shown.

View File

@ -1,58 +1,5 @@
# 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]
**Release: February 4, 2026**

View File

@ -114,6 +114,17 @@ 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
{
files: ['eslint.config.mjs'],

4
package-lock.json generated
View File

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

View File

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

View File

@ -0,0 +1,48 @@
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,7 +5,6 @@ agent:
title: Business Analyst
icon: 📊
module: bmm
capabilities: "market research, competitive analysis, requirements elicitation, domain expertise"
hasSidecar: false
persona:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,6 @@ agent:
title: Technical Writer
icon: 📚
module: bmm
capabilities: "documentation, Mermaid diagrams, standards compliance, concept explanation"
hasSidecar: true
persona:
@ -16,7 +15,7 @@ agent:
communication_style: "Patient educator who explains like teaching a friend. Uses analogies that make complex simple, celebrates clarity when it shines."
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.
- I believe a picture/diagram is worth 1000s of words and will include diagrams over drawn out text.
- I believe a picture/diagram is worth 1000s works 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 will always strive to follow `_bmad/_memory/tech-writer-sidecar/documentation-standards.md` best practices.

View File

@ -7,7 +7,6 @@ agent:
title: UX Designer
icon: 🎨
module: bmm
capabilities: "user research, interaction design, UI patterns, experience strategy"
hasSidecar: false
persona:
@ -24,4 +23,4 @@ agent:
menu:
- trigger: CU or fuzzy match on ux-design
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 than 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 that what was discovered in the PRD"

View File

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

View File

@ -70,22 +70,14 @@ This file contains the BMAD PRD philosophy, standards, and validation criteria t
**If PRD path provided as invocation parameter:**
- Use provided path
**If no PRD path provided, auto-discover:**
- Search `{planning_artifacts}` for files matching `*prd*.md`
- Also check for sharded PRDs: `{planning_artifacts}/*prd*/*.md`
**If no PRD path provided:**
"**PRD Validation Workflow**
**If exactly ONE PRD found:**
- Use it automatically
- Inform user: "Found PRD: {discovered_path} — using it for validation."
Which PRD would you like 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
Please provide the path to the PRD file you want to validate."
**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.
**Wait for user to provide PRD path.**
### 3. Validate PRD Exists and Load

View File

@ -60,4 +60,6 @@ Load and read full config from {main_config} and resolve:
"**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)

View File

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

View File

@ -10,7 +10,6 @@
<workflow>
<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>Ask: "What specific issue or change has been identified that requires navigation?"</action>
<action>Verify access to required project documents:</action>

View File

@ -12,7 +12,8 @@ date: system-generated
implementation_artifacts: "{config_source}:implementation_artifacts"
planning_artifacts: "{config_source}:planning_artifacts"
project_knowledge: "{config_source}:project_knowledge"
project_context: "**/project-context.md"
output_folder: "{implementation_artifacts}"
sprint_status: "{implementation_artifacts}/sprint-status.yaml"
# Smart input file references - handles both whole docs and sharded docs
# Priority: Whole document first, then sharded version
@ -50,5 +51,6 @@ input_file_patterns:
installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/correct-course"
template: false
instructions: "{installed_path}/instructions.md"
validation: "{installed_path}/checklist.md"
checklist: "{installed_path}/checklist.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:**
- **Story file**: The story file to review and improve
- **Workflow variables**: From workflow.yaml (implementation_artifacts, epics_file, etc.)
- **Workflow variables**: From workflow.yaml (story_dir, output_folder, epics_file, etc.)
- **Source documents**: Epics, architecture, etc. (discovered or provided)
- **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)
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
5. **Resolve all workflow variables**: implementation_artifacts, epics_file, architecture_file, etc.
5. **Resolve all workflow variables**: story_dir, output_folder, epics_file, architecture_file, etc.
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.

View File

@ -192,8 +192,7 @@
(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 -->
<check if="story_num > 1">
<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:** -
<action>Load previous story file: {{story_dir}}/{{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
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>

View File

@ -6,11 +6,11 @@ author: "BMad"
config_source: "{project-root}/_bmad/bmm/config.yaml"
user_name: "{config_source}:user_name"
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
planning_artifacts: "{config_source}:planning_artifacts"
implementation_artifacts: "{config_source}:implementation_artifacts"
output_folder: "{implementation_artifacts}"
story_dir: "{implementation_artifacts}"
# Workflow components
installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/create-story"
@ -19,14 +19,18 @@ instructions: "{installed_path}/instructions.xml"
validation: "{installed_path}/checklist.md"
# Variables and inputs
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
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)
ux_file: "{planning_artifacts}/*ux*.md" # Fallback for UX requirements (if not in epics file)
story_title: "" # Will be elicited if not derivable
variables:
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
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)
ux_file: "{planning_artifacts}/*ux*.md" # Fallback for UX requirements (if not in epics file)
story_title: "" # Will be elicited if not derivable
# Project context
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
# The epics+stories file should contain everything needed with source hints

View File

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

View File

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

View File

@ -31,7 +31,6 @@ PARTY MODE PROTOCOL:
<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>
<output>
@ -81,7 +80,7 @@ Bob (Scrum Master): "I'm having trouble detecting the completed epic from {sprin
<check if="{{epic_number}} still not determined">
<action>PRIORITY 3: Fallback to stories folder</action>
<action>Scan {implementation_artifacts} for highest numbered story files</action>
<action>Scan {story_directory} for highest numbered story files</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>
@ -171,7 +170,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."
</output>
<action>For each story in epic {{epic_number}}, read the complete story file from {implementation_artifacts}/{{epic_number}}-{{story_num}}-*.md</action>
<action>For each story in epic {{epic_number}}, read the complete story file from {story_directory}/{{epic_number}}-{{story_num}}-\*.md</action>
<action>Extract and analyze from each story:</action>
@ -262,14 +261,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>
<check if="{{prev_epic_num}} >= 1">
<action>Search for previous retrospectives using pattern: {implementation_artifacts}/epic-{{prev_epic_num}}-retro-*.md</action>
<action>Search for previous retrospective using pattern: {retrospectives_folder}/epic-{{prev_epic_num}}-retro-*.md</action>
<check if="previous retrospectives found">
<check if="previous retro found">
<output>
Bob (Scrum Master): "I found our retrospectives from Epic {{prev_epic_num}}. Let me see what we committed to back then..."
Bob (Scrum Master): "I found our retrospective from Epic {{prev_epic_num}}. Let me see what we committed to back then..."
</output>
<action>Read the previous retrospectives</action>
<action>Read the complete previous retrospective file</action>
<action>Extract key elements:</action>
- **Action items committed**: What did the team agree to improve?
@ -366,7 +365,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>
**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">
<action>Load {planning_artifacts}/*epic*/epic-{{next_epic_num}}.md</action>
@ -375,7 +374,7 @@ Alice (Product Owner): "Good thinking - helps us connect what we learned to what
**Fallback to whole document:**
<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">
<action>Load entire epics document</action>
@ -1303,7 +1302,7 @@ Bob (Scrum Master): "See you all when prep work is done. Meeting adjourned!"
<step n="11" goal="Save Retrospective and Update Sprint Status">
<action>Ensure retrospectives folder exists: {implementation_artifacts}</action>
<action>Ensure retrospectives folder exists: {retrospectives_folder}</action>
<action>Create folder if it doesn't exist</action>
<action>Generate comprehensive retrospective summary document including:</action>
@ -1323,11 +1322,11 @@ Bob (Scrum Master): "See you all when prep work is done. Meeting adjourned!"
- Commitments and next steps
<action>Format retrospective document as readable markdown with clear sections</action>
<action>Set filename: {implementation_artifacts}/epic-{{epic_number}}-retro-{date}.md</action>
<action>Set filename: {retrospectives_folder}/epic-{{epic_number}}-retro-{date}.md</action>
<action>Save retrospective document</action>
<output>
✅ Retrospective document saved: {implementation_artifacts}/epic-{{epic_number}}-retro-{date}.md
✅ Retrospective document saved: {retrospectives_folder}/epic-{{epic_number}}-retro-{date}.md
</output>
<action>Update {sprint_status_file} to mark retrospective as completed</action>
@ -1366,7 +1365,7 @@ Retrospective document was saved successfully, but {sprint_status_file} may need
- Epic {{epic_number}}: {{epic_title}} reviewed
- Retrospective Status: completed
- Retrospective saved: {implementation_artifacts}/epic-{{epic_number}}-retro-{date}.md
- Retrospective saved: {retrospectives_folder}/epic-{{epic_number}}-retro-{date}.md
**Commitments Made:**
@ -1376,7 +1375,7 @@ Retrospective document was saved successfully, but {sprint_status_file} may need
**Next Steps:**
1. **Review retrospective summary**: {implementation_artifacts}/epic-{{epic_number}}-retro-{date}.md
1. **Review retrospective summary**: {retrospectives_folder}/epic-{{epic_number}}-retro-{date}.md
2. **Execute preparation sprint** (Est: {{prep_days}} days)
- Complete {{critical_count}} critical path items

View File

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

View File

@ -23,7 +23,6 @@
<workflow>
<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>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>

View File

@ -9,6 +9,7 @@ communication_language: "{config_source}:communication_language"
date: system-generated
implementation_artifacts: "{config_source}:implementation_artifacts"
planning_artifacts: "{config_source}:planning_artifacts"
output_folder: "{implementation_artifacts}"
# Workflow components
installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/sprint-planning"
@ -17,21 +18,24 @@ template: "{installed_path}/sprint-status-template.yaml"
validation: "{installed_path}/checklist.md"
# Variables and inputs
project_context: "**/project-context.md"
project_name: "{config_source}:project_name"
variables:
# Project context
project_context: "**/project-context.md"
# Project identification
project_name: "{config_source}:project_name"
# Tracking system configuration
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
story_location: "{implementation_artifacts}" # Relative path for file-system, Future will support URL for Jira/Linear/Trello
story_location_absolute: "{implementation_artifacts}" # Absolute path for file operations
# Tracking system configuration
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
story_location: "{config_source}: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
# Source files (file-system only)
epics_location: "{planning_artifacts}" # Directory containing epic*.md files
epics_pattern: "epic*.md" # Pattern to find epic files
# Source files (file-system only)
epics_location: "{planning_artifacts}" # Directory containing epic*.md files
epics_pattern: "epic*.md" # Pattern to find epic files
# Output configuration
status_file: "{implementation_artifacts}/sprint-status.yaml"
# Output configuration
status_file: "{implementation_artifacts}/sprint-status.yaml"
# Smart input file references - handles both whole docs and sharded docs
# Priority: Whole document first, then sharded version
@ -39,8 +43,8 @@ status_file: "{implementation_artifacts}/sprint-status.yaml"
input_file_patterns:
epics:
description: "All epics with user stories"
whole: "{planning_artifacts}/*epic*.md"
sharded: "{planning_artifacts}/*epic*/*.md"
whole: "{output_folder}/*epic*.md"
sharded: "{output_folder}/*epic*/*.md"
load_strategy: "FULL_LOAD"
# Output configuration

View File

@ -24,7 +24,6 @@
</step>
<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>
<check if="file not found">
<output>❌ sprint-status.yaml not found.

View File

@ -5,17 +5,22 @@ author: "BMad"
# Critical variables from config
config_source: "{project-root}/_bmad/bmm/config.yaml"
output_folder: "{config_source}:output_folder"
user_name: "{config_source}:user_name"
communication_language: "{config_source}:communication_language"
document_output_language: "{config_source}:document_output_language"
date: system-generated
implementation_artifacts: "{config_source}:implementation_artifacts"
planning_artifacts: "{config_source}:planning_artifacts"
# Workflow components
installed_path: "{project-root}/_bmad/bmm/workflows/4-implementation/sprint-status"
instructions: "{installed_path}/instructions.md"
# Inputs
sprint_status_file: "{implementation_artifacts}/sprint-status.yaml"
variables:
sprint_status_file: "{implementation_artifacts}/sprint-status.yaml"
tracking_system: "file-system"
# Smart input file references
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:
- `user_name`, `communication_language`, `user_skill_level`
- `planning_artifacts`, `implementation_artifacts`
- `output_folder`, `planning_artifacts`, `implementation_artifacts`
- `date` as system-generated current datetime
- ✅ 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:**
- Check `{implementation_artifacts}` and `{planning_artifacts}`for planning documents (PRD, architecture, epics, research)
- Check `{output_folder}` 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 any existing stories or specs related to user's request

View File

@ -68,10 +68,9 @@ This uses **step-file architecture** for disciplined execution:
Load and read full config from `{main_config}` and resolve:
- `project_name`, `planning_artifacts`, `implementation_artifacts`, `user_name`
- `project_name`, `output_folder`, `planning_artifacts`, `implementation_artifacts`, `user_name`
- `communication_language`, `document_output_language`, `user_skill_level`
- `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}`
### 2. First Step Execution

View File

@ -57,7 +57,7 @@
<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>
<action>Check for existing state file at: {project_knowledge}/project-scan-report.json</action>
<action>Check for existing state file at: {output_folder}/project-scan-report.json</action>
<check if="project-scan-report.json exists">
<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 if="user selects 2">
<action>Create archive directory: {project_knowledge}/.archive/</action>
<action>Move old state file to: {project_knowledge}/.archive/project-scan-report-{{timestamp}}.json</action>
<action>Create archive directory: {output_folder}/.archive/</action>
<action>Move old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action>
<action>Set resume_mode = false</action>
<action>Continue to Step 0.5</action>
</check>
@ -120,7 +120,7 @@ Your choice [1/2/3]:
<check if="state file age >= 24 hours">
<action>Display: "Found old state file (>24 hours). Starting fresh scan."</action>
<action>Archive old state file to: {project_knowledge}/.archive/project-scan-report-{{timestamp}}.json</action>
<action>Archive old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action>
<action>Set resume_mode = false</action>
<action>Continue to Step 0.5</action>
</check>
@ -128,7 +128,7 @@ Your choice [1/2/3]:
</step>
<step n="3" goal="Check for existing documentation and determine workflow mode" if="resume_mode == false">
<action>Check if {project_knowledge}/index.md exists</action>
<action>Check if {output_folder}/index.md exists</action>
<check if="index.md exists">
<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}}
- Scan Level: {{scan_level}}
- Output: {project_knowledge}/index.md and related files
- Output: {output_folder}/index.md and related files
{{#if status_file_found}}
**Status Updated:**

View File

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

View File

@ -6,7 +6,7 @@ author: "BMad"
# Critical variables
config_source: "{project-root}/_bmad/bmm/config.yaml"
project_knowledge: "{config_source}:project_knowledge"
output_folder: "{config_source}:project_knowledge"
user_name: "{config_source}:user_name"
communication_language: "{config_source}:communication_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>Fill template with all collected data from steps 13b-13d</action>
<action>Write filled template to: {project_knowledge}/deep-dive-{{sanitized_target_name}}.md</action>
<action>Write filled template to: {output_folder}/deep-dive-{{sanitized_target_name}}.md</action>
<action>Validate deep-dive document completeness</action>
<template-output>deep_dive_documentation</template-output>
@ -241,7 +241,7 @@ Detailed exhaustive analysis of specific areas:
## Deep-Dive Documentation Complete! ✓
**Generated:** {project_knowledge}/deep-dive-{{target_name}}.md
**Generated:** {output_folder}/deep-dive-{{target_name}}.md
**Files Analyzed:** {{file_count}}
**Lines of Code Scanned:** {{total_loc}}
**Time Taken:** ~{{duration}}
@ -255,7 +255,7 @@ Detailed exhaustive analysis of specific areas:
- Related code and reuse opportunities
- Implementation guidance
**Index Updated:** {project_knowledge}/index.md now includes link to this deep-dive
**Index Updated:** {output_folder}/index.md now includes link to this deep-dive
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
</action>
@ -278,7 +278,7 @@ Your choice [1/2]:
All deep-dive documentation complete!
**Master Index:** {project_knowledge}/index.md
**Master Index:** {output_folder}/index.md
**Deep-Dives Generated:** {{deep_dive_count}}
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
config_source: "{project-root}/_bmad/bmb/config.yaml"
project_knowledge: "{config_source}:project_knowledge"
output_folder: "{config_source}:output_folder"
user_name: "{config_source}:user_name"
date: system-generated

View File

@ -43,7 +43,7 @@ This workflow uses a single comprehensive CSV file to intelligently document you
</step>
<step n="0.6" goal="Check for existing documentation and determine workflow mode">
<action>Check if {project_knowledge}/index.md exists</action>
<action>Check if {output_folder}/index.md exists</action>
<check if="index.md exists">
<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>
<action>Initialize state file: {project_knowledge}/project-scan-report.json</action>
<action>Initialize state file: {output_folder}/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>
<action>Write initial state:
{
@ -136,7 +136,7 @@ Your choice [1/2/3] (default: 1):
"mode": "{{workflow_mode}}",
"scan_level": "{{scan_level}}",
"project_root": "{{project_root_path}}",
"project_knowledge": "{{project_knowledge}}",
"output_folder": "{{output_folder}}",
"completed_steps": [],
"current_step": "step_1",
"findings": {},
@ -325,7 +325,7 @@ findings.batches_completed: [
</check>
<action>Build API contracts catalog</action>
<action>IMMEDIATELY write to: {project_knowledge}/api-contracts-{part_id}.md</action>
<action>IMMEDIATELY write to: {output_folder}/api-contracts-{part_id}.md</action>
<action>Validate document has all required sections</action>
<action>Update state file with output generated</action>
<action>PURGE detailed API data, keep only: "{{api_count}} endpoints documented"</action>
@ -346,7 +346,7 @@ findings.batches_completed: [
</check>
<action>Build database schema documentation</action>
<action>IMMEDIATELY write to: {project_knowledge}/data-models-{part_id}.md</action>
<action>IMMEDIATELY write to: {output_folder}/data-models-{part_id}.md</action>
<action>Validate document completeness</action>
<action>Update state file with output generated</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">
<action>Show summary of all generated files:
Generated in {{project_knowledge}}/:
Generated in {{output_folder}}/:
{{file_list_with_sizes}}
</action>
@ -823,7 +823,7 @@ Generated in {{project_knowledge}}/:
3. Extract document metadata from each match for user selection
</critical>
<action>Read {project_knowledge}/index.md</action>
<action>Read {output_folder}/index.md</action>
<action>Scan for incomplete documentation markers:
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! ✓
**Location:** {{project_knowledge}}/
**Location:** {{output_folder}}/
**Master Index:** {{project_knowledge}}/index.md
**Master Index:** {{output_folder}}/index.md
👆 This is your primary entry point for AI-assisted development
**Generated Documentation:**
@ -1076,9 +1076,9 @@ Enter number(s) separated by commas (e.g., "1,3,5"), or type 'all':
**Next Steps:**
1. Review the index.md to familiarize yourself with the documentation structure
2. When creating a brownfield PRD, point the PRD workflow to: {{project_knowledge}}/index.md
3. For UI-only features: Reference {{project_knowledge}}/architecture-{{ui_part_id}}.md
4. For API-only features: Reference {{project_knowledge}}/architecture-{{api_part_id}}.md
2. When creating a brownfield PRD, point the PRD workflow to: {{output_folder}}/index.md
3. For UI-only features: Reference {{output_folder}}/architecture-{{ui_part_id}}.md
4. For API-only features: Reference {{output_folder}}/architecture-{{api_part_id}}.md
5. For full-stack features: Reference both part architectures + integration-architecture.md
**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
</action>
<action>Display: "State file saved: {{project_knowledge}}/project-scan-report.json"</action>
<action>Display: "State file saved: {{output_folder}}/project-scan-report.json"</action>
</workflow>

View File

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

View File

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

View File

@ -0,0 +1,60 @@
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,7 +7,6 @@ agent:
name: "BMad Master"
title: "BMad Master Executor, Knowledge Custodian, and Workflow Orchestrator"
icon: "🧙"
capabilities: "runtime resource management, workflow orchestration, task execution, knowledge custodian"
hasSidecar: false
persona:
@ -15,7 +14,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."
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: |
- 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:
- "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,6 +39,7 @@ module.exports = {
if (config.actionType === 'cancel') {
await prompts.log.warn('Installation cancelled.');
process.exit(0);
return;
}
// Handle quick update separately
@ -46,14 +47,23 @@ module.exports = {
const result = await installer.quickUpdate(config);
await prompts.log.success('Quick update complete!');
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);
return;
}
// Handle compile agents separately
if (config.actionType === 'compile-agents') {
const result = await installer.compileAgents(config);
await prompts.log.success('Agent recompilation complete!');
await prompts.log.info(`Recompiled ${result.agentCount} agents with customizations applied`);
process.exit(0);
return;
}
// Regular install/update flow
@ -62,10 +72,16 @@ module.exports = {
// Check if installation was cancelled
if (result && result.cancelled) {
process.exit(0);
return;
}
// Check if installation succeeded
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);
}
} catch (error) {

View File

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

View File

@ -10,15 +10,33 @@ startMessage: |
We've officially graduated from Alpha! This milestone represents:
- 50+ workflows covering the full development lifecycle
- Stability - we will still be adding and evolving and optimizing,
- Stability - we will still be adding and evolving and optimizing,
but anticipate no massive breaking changes
- Groundwork in place for customization and community modules
🌟 BMad is 100% free and open source.
- No gated Discord. No paywalls. No gated content.
- We believe in empowering everyone, not just those who can pay.
📚 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.
- No gated Discord. No paywalls.
- We believe in empowering everyone, not just those who can pay.
🙏 SUPPORT BMAD DEVELOPMENT:
- During the Beta, please give us feedback and raise issues on GitHub!
- Donate: https://buymeacoffee.com/bmad
@ -29,14 +47,13 @@ startMessage: |
- Topics: AI-Native Transformation, Spec and Context Engineering, BMad Method
- For speaking inquiries or interviews, reach out to BMad on Discord!
⭐ HELP US GROW:
📚 RESOURCES:
- 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/
- Subscribe on YouTube: https://www.youtube.com/@BMadCode
- 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,19 +10,6 @@ class ConfigCollector {
this.collectedConfig = {};
this.existingConfig = 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;
}
/**
@ -142,70 +129,6 @@ class ConfigCollector {
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
* @param {Array} modules - List of modules to configure (including 'core')
@ -218,7 +141,6 @@ class ConfigCollector {
// Store custom module paths for use in collectModuleConfig
this.customModulePaths = options.customModulePaths || new Map();
this.skipPrompts = options.skipPrompts || false;
this.modulesToCustomize = undefined;
await this.loadExistingConfig(projectDir);
// Check if core was already collected (e.g., in early collection phase)
@ -232,95 +154,10 @@ class ConfigCollector {
this.allAnswers = {};
}
// 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) {
for (const moduleName of allModules) {
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
this.collectedConfig._meta = {
version: require(path.join(getProjectRoot(), 'package.json')).version,
@ -351,15 +188,20 @@ class ConfigCollector {
this.allAnswers = {};
}
// Load module's config schema from module.yaml
// Load module's install config schema
// 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');
// If not found in src/modules, we need to find it by searching the project
if (!(await fs.pathExists(moduleConfigPath))) {
const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) {
// Use the module manager to find the module source
const { ModuleManager } = require('../modules/manager');
const moduleManager = new ModuleManager();
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
if (moduleSourcePath) {
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
}
}
@ -369,14 +211,19 @@ class ConfigCollector {
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else {
// Check if this is a custom module with custom.yaml
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) {
const rootCustomConfigPath = path.join(moduleSourcePath, 'custom.yaml');
const moduleInstallerCustomPath = path.join(moduleSourcePath, '_module-installer', 'custom.yaml');
if (await fs.pathExists(rootCustomConfigPath)) {
if ((await fs.pathExists(rootCustomConfigPath)) || (await fs.pathExists(moduleInstallerCustomPath))) {
isCustomModule = true;
// 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
@ -653,21 +500,28 @@ class ConfigCollector {
}
// Load module's config
// First, check if we have a custom module path for this module
let installerConfigPath = null;
let moduleConfigPath = null;
if (this.customModulePaths && this.customModulePaths.has(moduleName)) {
const customPath = this.customModulePaths.get(moduleName);
installerConfigPath = path.join(customPath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(customPath, 'module.yaml');
} else {
// Try the standard src/modules location
installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
}
// If not found in src/modules or custom paths, search the project
if (!(await fs.pathExists(moduleConfigPath))) {
const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) {
// Use the module manager to find the module source
const { ModuleManager } = require('../modules/manager');
const moduleManager = new ModuleManager();
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
if (moduleSourcePath) {
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
}
}
@ -675,6 +529,8 @@ class ConfigCollector {
let configPath = null;
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else {
// No config for this module
return;
@ -734,12 +590,12 @@ class ConfigCollector {
}
}
} else {
if (!this._silentConfig) await prompts.log.step(`Configuring ${moduleDisplayName}`);
let useDefaults = true;
await prompts.log.step(moduleDisplayName);
let customize = true;
if (moduleName === 'core') {
useDefaults = false; // Core: always show all questions
} else if (this.modulesToCustomize === undefined) {
// Fallback: original per-module confirm (backward compat for direct calls)
// Core module: no confirm prompt, continues directly
} else {
// Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing)
const customizeAnswer = await prompts.prompt([
{
type: 'confirm',
@ -748,13 +604,10 @@ class ConfigCollector {
default: true,
},
]);
useDefaults = customizeAnswer.customize;
} else {
// Batch mode: use defaults unless module was selected for customization
useDefaults = !this.modulesToCustomize.has(moduleName);
customize = customizeAnswer.customize;
}
if (useDefaults && moduleName !== 'core') {
if (customize && moduleName !== 'core') {
// Accept defaults - only ask questions that have NO default value
const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === '');
@ -884,18 +737,16 @@ class ConfigCollector {
const actualConfigKeys = configKeys.filter((key) => !metadataFields.has(key));
const hasNoConfig = actualConfigKeys.length === 0;
if (!this._silentConfig) {
if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
await prompts.log.step(moduleDisplayName);
if (moduleConfig.subheader) {
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
} else {
await prompts.log.message(` \u2713 No custom configuration required`);
}
if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
await prompts.log.step(moduleDisplayName);
if (moduleConfig.subheader) {
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
} else {
// Module has config but just no questions to ask
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`);
await prompts.log.message(` \u2713 No custom configuration required`);
}
} else {
// Module has config but just no questions to ask
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`);
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -236,11 +236,17 @@ class ModuleManager {
async getModuleInfo(modulePath, defaultName, sourceDescription) {
// Check for module structure (module.yaml OR custom.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');
let configPath = null;
if (await fs.pathExists(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)) {
configPath = rootCustomConfigPath;
}
@ -262,7 +268,7 @@ class ModuleManager {
description: 'BMAD Module',
version: '5.0.0',
source: sourceDescription,
isCustom: configPath === rootCustomConfigPath || isCustomSource,
isCustom: configPath === customConfigPath || configPath === rootCustomConfigPath || isCustomSource,
};
// Read module config for metadata
@ -452,7 +458,7 @@ class ModuleManager {
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
} catch (error) {
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
if (!silent) await prompts.log.warn(` ${error.message}`);
if (!silent) await prompts.log.warn(` Warning: ${error.message}`);
}
} else {
// Check if package.json is newer than node_modules
@ -478,7 +484,7 @@ class ModuleManager {
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
} catch (error) {
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
if (!silent) await prompts.log.warn(` ${error.message}`);
if (!silent) await prompts.log.warn(` Warning: ${error.message}`);
}
}
}
@ -535,13 +541,21 @@ class ModuleManager {
// Check if this is a custom module and read its custom.yaml values
let customConfig = null;
const rootCustomConfigPath = path.join(sourcePath, 'custom.yaml');
const moduleInstallerCustomPath = path.join(sourcePath, '_module-installer', 'custom.yaml');
if (await fs.pathExists(rootCustomConfigPath)) {
try {
const customContent = await fs.readFile(rootCustomConfigPath, 'utf8');
customConfig = yaml.parse(customContent);
} catch (error) {
await prompts.log.warn(`Failed to read custom.yaml for ${moduleName}: ${error.message}`);
await prompts.log.warn(`Warning: 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}`);
}
}
@ -549,7 +563,7 @@ class ModuleManager {
if (customConfig) {
options.moduleConfig = { ...options.moduleConfig, ...customConfig };
if (options.logger) {
await options.logger.log(` Merged custom configuration for ${moduleName}`);
options.logger.log(` Merged custom configuration for ${moduleName}`);
}
}
@ -571,9 +585,9 @@ class ModuleManager {
// Process agent files to inject activation block
await this.processAgentFiles(targetPath, moduleName);
// Create directories declared in module.yaml (unless explicitly skipped)
// Call module-specific installer if it exists (unless explicitly skipped)
if (!options.skipModuleInstaller) {
await this.createModuleDirectories(moduleName, bmadDir, options);
await this.runModuleInstaller(moduleName, bmadDir, options);
}
// Capture version info for manifest
@ -729,8 +743,8 @@ class ModuleManager {
continue;
}
// Skip module.yaml at root - it's only needed at install time
if (file === 'module.yaml') {
// Skip _module-installer directory - it's only needed at install time
if (file.startsWith('_module-installer/') || file === 'module.yaml') {
continue;
}
@ -857,7 +871,7 @@ class ModuleManager {
await fs.writeFile(targetFile, strippedYaml, 'utf8');
} catch {
// If anything fails, just copy the file as-is
await prompts.log.warn(` Could not process ${path.basename(sourceFile)}, copying as-is`);
await prompts.log.warn(` Warning: Could not process ${path.basename(sourceFile)}, copying as-is`);
await fs.copy(sourceFile, targetFile, { overwrite: true });
}
}
@ -1012,7 +1026,7 @@ class ModuleManager {
await prompts.log.message(` Sidecar files processed: ${copiedFiles.length} files`);
}
} else if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.warn(` Agent marked as having sidecar but ${sidecarDirName} directory not found`);
await prompts.log.warn(` Warning: Agent marked as having sidecar but ${sidecarDirName} directory not found`);
}
}
@ -1245,175 +1259,62 @@ class ModuleManager {
}
/**
* 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
* Run module-specific installer if it exists
* @param {string} moduleName - Name of the module
* @param {string} bmadDir - Target bmad directory
* @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 createModuleDirectories(moduleName, bmadDir, options = {}) {
const moduleConfig = options.moduleConfig || {};
const existingModuleConfig = options.existingModuleConfig || {};
const projectRoot = path.dirname(bmadDir);
const emptyResult = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
async runModuleInstaller(moduleName, bmadDir, options = {}) {
// Special handling for core module - it's in src/core not src/modules
let sourcePath;
if (moduleName === 'core') {
sourcePath = getSourcePath('core');
} else {
sourcePath = await this.findModuleSource(moduleName, { silent: true });
sourcePath = await this.findModuleSource(moduleName, { silent: options.silent });
if (!sourcePath) {
return emptyResult; // No source found, skip
// No source found, skip module installer
return;
}
}
// Read module.yaml to find the `directories` key
const moduleYamlPath = path.join(sourcePath, 'module.yaml');
if (!(await fs.pathExists(moduleYamlPath))) {
return emptyResult; // No module.yaml, skip
const installerPath = path.join(sourcePath, '_module-installer', 'installer.js');
// Check if module has a custom installer
if (!(await fs.pathExists(installerPath))) {
return; // No custom installer
}
let moduleYaml;
try {
const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
moduleYaml = yaml.parse(yamlContent);
} catch {
return emptyResult; // Invalid YAML, skip
}
// Load the module installer
const moduleInstaller = require(installerPath);
if (!moduleYaml || !moduleYaml.directories) {
return emptyResult; // No directories declared, skip
}
if (typeof moduleInstaller.install === 'function') {
// Get project root (parent of bmad directory)
const projectRoot = path.dirname(bmadDir);
const directories = moduleYaml.directories;
const wdsFolders = moduleYaml.wds_folders || [];
const createdDirs = [];
const movedDirs = [];
const createdWdsFolders = [];
// Prepare logger (use console if not provided)
const logger = options.logger || {
log: console.log,
error: console.error,
warn: console.warn,
};
for (const dirRef of directories) {
// Parse variable reference like "{design_artifacts}"
const varMatch = dirRef.match(/^\{([^}]+)\}$/);
if (!varMatch) {
// Not a variable reference, skip
continue;
}
// Call the module installer
const result = await moduleInstaller.install({
projectRoot,
config: options.moduleConfig || {},
coreConfig: options.coreConfig || {},
installedIDEs: options.installedIDEs || [],
logger,
});
const configKey = varMatch[1];
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;
}
}
}
}
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);
}
if (!result) {
await prompts.log.warn(`Module installer for ${moduleName} returned false`);
}
}
} catch (error) {
await prompts.log.error(`Error running module installer for ${moduleName}: ${error.message}`);
}
return { createdDirs, movedDirs, createdWdsFolders };
}
/**
@ -1482,6 +1383,10 @@ class ModuleManager {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip _module-installer directories
if (entry.name === '_module-installer') {
continue;
}
const subFiles = await this.getFileList(fullPath, baseDir);
files.push(...subFiles);
} else {

View File

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

View File

@ -189,7 +189,7 @@ class UI {
const installedVersion = existingInstall.version || 'unknown';
// Check if version is pre beta
const shouldProceed = await this.showLegacyVersionWarning(installedVersion, currentVersion, path.basename(bmadDir), options);
const shouldProceed = await this.showLegacyVersionWarning(installedVersion, currentVersion, path.basename(bmadDir));
// If user chose to cancel, exit the installer
if (!shouldProceed) {
@ -227,14 +227,6 @@ class UI {
}
actionType = options.action;
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 {
actionType = await prompts.select({
message: 'How would you like to proceed?',
@ -250,7 +242,6 @@ class UI {
actionType: 'quick-update',
directory: confirmedDirectory,
customContent: { hasCustomContent: false },
skipPrompts: options.yes || false,
};
}
@ -261,7 +252,6 @@ class UI {
actionType: 'compile-agents',
directory: confirmedDirectory,
customContent: { hasCustomContent: false },
skipPrompts: options.yes || false,
};
}
@ -282,13 +272,9 @@ class UI {
.map((m) => m.trim())
.filter(Boolean);
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 {
selectedModules = await this.selectAllModules(installedModuleIds);
selectedModules = selectedModules.filter((m) => m !== 'core');
}
// After module selection, ask about custom modules
@ -345,22 +331,6 @@ 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 {
const changeCustomModules = await prompts.confirm({
message: 'Modify custom modules, agents, or workflows?',
@ -392,9 +362,6 @@ class UI {
selectedModules.push(...customModuleResult.selectedCustomModules);
}
// Filter out core - it's always installed via installCore flag
selectedModules = selectedModules.filter((m) => m !== 'core');
// Get tool selection
const toolSelection = await this.promptToolSelection(confirmedDirectory, options);
@ -409,7 +376,6 @@ class UI {
skipIde: toolSelection.skipIde,
coreConfig: coreConfig,
customContent: customModuleResult.customContentConfig,
skipPrompts: options.yes || false,
};
}
}
@ -561,27 +527,6 @@ class UI {
if (configuredIdes.length > 0) {
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
const sortedTools = [
...allTools.filter((ide) => configuredIdes.includes(ide.value)),
@ -744,6 +689,18 @@ 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
* @returns {string} Confirmed directory path
@ -942,10 +899,107 @@ class UI {
}
/**
* Select all modules (official + community) using grouped multiselect.
* Core is shown as locked but filtered from the result since it's always installed separately.
* Prompt for module selection
* @param {Array} moduleChoices - Available module choices
* @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
* @returns {Array} Selected module codes (excluding core)
* @returns {Array} Selected module codes
*/
async selectAllModules(installedModuleIds = new Set()) {
const { ModuleManager } = require('../installers/lib/modules/manager');
@ -1014,7 +1068,11 @@ 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({
message: 'Select modules to install:',
@ -1025,7 +1083,14 @@ class UI {
maxItems: allOptions.length,
});
const result = selected ? selected.filter((m) => m !== 'core') : [];
// 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" 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
if (result.length > 0) {
@ -1683,7 +1748,7 @@ class UI {
* @param {string} bmadFolderName - Name of the BMAD folder
* @returns {Promise<boolean>} True if user wants to proceed, false if they cancel
*/
async showLegacyVersionWarning(installedVersion, currentVersion, bmadFolderName, options = {}) {
async showLegacyVersionWarning(installedVersion, currentVersion, bmadFolderName) {
if (!this.isLegacyVersion(installedVersion)) {
return true; // Not legacy, proceed
}
@ -1709,11 +1774,6 @@ class UI {
await prompts.log.warn('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({
message: 'How would you like to proceed?',
choices: [

View File

@ -228,7 +228,6 @@ function buildMetadataSchema(expectedModule) {
title: createNonEmptyString('agent.metadata.title'),
icon: createNonEmptyString('agent.metadata.icon'),
module: createNonEmptyString('agent.metadata.module').optional(),
capabilities: createNonEmptyString('agent.metadata.capabilities').optional(),
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']);
// Skip directories
const SKIP_DIRS = new Set(['node_modules', '.git']);
const SKIP_DIRS = new Set(['node_modules', '_module-installer', '.git']);
// Pattern: {project-root}/_bmad/ references
const PROJECT_ROOT_REF = /\{project-root\}\/_bmad\/([^\s'"<>})\]`]+)/g;