Compare commits

..

6 Commits

Author SHA1 Message Date
Dicky Moore 3c1df006fe Merge remote-tracking branch 'upstream/main' into phase1-md-workflows-clean 2026-02-08 23:28:58 +00:00
Dicky Moore 95b437023d fix: clarify create-story context extraction and state age handling 2026-02-08 23:25:01 +00:00
Dicky Moore 5e8289fe26 test: expand workflow reference guard to scan installer JS 2026-02-08 22:52:18 +00:00
Dicky Moore 3aaa37125b fix: route IDE workflow templates through workflow runner 2026-02-08 22:37:04 +00:00
Dicky Moore 0a3f48f13f fix: address coderabbit workflow migration follow-ups 2026-02-08 22:16:06 +00:00
Davor Racic 90ea3cbed7
Minor installer fixes (#1590)
* fix: remove redundant "None" skip option from module selection

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

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

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

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

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

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

* fix: address code review findings from PR #1590

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

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

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

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

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

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

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

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

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

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

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

---------

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

View File

@ -78,15 +78,15 @@ Work through phases 1-3. **Use fresh chats for each workflow.**
### Phase 1: Analysis (Optional)
All workflows in this phase are optional:
- **brainstorming** — Guided ideation
- **research** — Market and technical research
- **create-product-brief** — Recommended foundation document
- **brainstorming** (`bmad-bmm-brainstorming`) — Guided ideation
- **research** (`bmad-bmm-research`) — Market and technical research
- **create-product-brief** (`bmad-bmm-create-product-brief`) — Recommended foundation document
### Phase 2: Planning (Required)
**For BMad Method and Enterprise tracks:**
1. Load the **PM agent** in a new chat
2. Run the `prd` workflow
2. Run the `create-prd` workflow (`bmad-bmm-create-prd`)
3. Output: `PRD.md`
**For Quick Flow track:**
@ -100,7 +100,7 @@ If your project has a user interface, load the **UX-Designer agent** and run the
**Create Architecture**
1. Load the **Architect agent** in a new chat
2. Run `create-architecture`
2. Run `create-architecture` (`bmad-bmm-create-architecture`)
3. Output: Architecture document with technical decisions
**Create Epics and Stories**
@ -110,12 +110,12 @@ Epics and stories are now created *after* architecture. This produces better qua
:::
1. Load the **PM agent** in a new chat
2. Run `create-epics-and-stories`
2. Run `create-epics-and-stories` (`bmad-bmm-create-epics-and-stories`)
3. The workflow uses both PRD and Architecture to create technically-informed stories
**Implementation Readiness Check** *(Highly Recommended)*
1. Load the **Architect agent** in a new chat
2. Run `check-implementation-readiness`
2. Run `check-implementation-readiness` (`bmad-bmm-check-implementation-readiness`)
3. Validates cohesion across all planning documents
## Step 2: Build Your Project
@ -124,7 +124,7 @@ Once planning is complete, move to implementation. **Each workflow should run in
### Initialize Sprint Planning
Load the **SM agent** and run `sprint-planning`. This creates `sprint-status.yaml` to track all epics and stories.
Load the **SM agent** and run `sprint-planning` (`bmad-bmm-sprint-planning`). This creates `sprint-status.yaml` to track all epics and stories.
### The Build Cycle
@ -132,11 +132,11 @@ For each story, repeat this cycle with fresh chats:
| Step | Agent | Workflow | Purpose |
| ---- | ----- | -------------- | ---------------------------------- |
| 1 | SM | `create-story` | Create story file from epic |
| 2 | DEV | `dev-story` | Implement the story |
| 3 | DEV | `code-review` | Quality validation *(recommended)* |
| 1 | SM | `create-story` (`bmad-bmm-create-story`) | Create story file from epic |
| 2 | DEV | `dev-story` (`bmad-bmm-dev-story`) | Implement the story |
| 3 | DEV | `code-review` (`bmad-bmm-code-review`) | Quality validation *(recommended)* |
After completing all stories in an epic, load the **SM agent** and run `retrospective`.
After completing all stories in an epic, load the **SM agent** and run `retrospective` (`bmad-bmm-retrospective`).
## What You've Accomplished
@ -162,17 +162,17 @@ your-project/
## Quick Reference
| Workflow | Agent | Purpose |
| -------------------------------- | --------- | ------------------------------------ |
| `help` | Any | Get guidance on what to do next |
| `prd` | PM | Create Product Requirements Document |
| `create-architecture` | Architect | Create architecture document |
| `create-epics-and-stories` | PM | Break down PRD into epics |
| `check-implementation-readiness` | Architect | Validate planning cohesion |
| `sprint-planning` | SM | Initialize sprint tracking |
| `create-story` | SM | Create a story file |
| `dev-story` | DEV | Implement a story |
| `code-review` | DEV | Review implemented code |
| Workflow | Slash Command | Agent | Purpose |
| -------------------------------- | ------------------------------------- | --------- | ------------------------------------ |
| `help` | `bmad-help` | Any | Get guidance on what to do next |
| `create-prd` | `bmad-bmm-create-prd` | PM | Create Product Requirements Document |
| `create-architecture` | `bmad-bmm-create-architecture` | Architect | Create architecture document |
| `create-epics-and-stories` | `bmad-bmm-create-epics-and-stories` | PM | Break down PRD into epics |
| `check-implementation-readiness` | `bmad-bmm-check-implementation-readiness` | Architect | Validate planning cohesion |
| `sprint-planning` | `bmad-bmm-sprint-planning` | SM | Initialize sprint tracking |
| `create-story` | `bmad-bmm-create-story` | SM | Create a story file |
| `dev-story` | `bmad-bmm-dev-story` | DEV | Implement a story |
| `code-review` | `bmad-bmm-code-review` | DEV | Review implemented code |
## Common Questions
@ -183,7 +183,7 @@ Only for BMad Method and Enterprise tracks. Quick Flow skips from tech-spec to i
Yes. The SM agent has a `correct-course` workflow for handling scope changes.
**What if I want to brainstorm first?**
Load the Analyst agent and run `brainstorming` before starting your PRD.
Load the Analyst agent and run `brainstorming` (`bmad-bmm-brainstorming`) before starting your PRD.
**Do I need to follow a strict order?**
Not strictly. Once you learn the flow, you can run workflows directly using the Quick Reference above.
@ -192,7 +192,7 @@ Not strictly. Once you learn the flow, you can run workflows directly using the
- **During workflows** — Agents guide you with questions and explanations
- **Community** — [Discord](https://discord.gg/gk8jAdXWmj) (#bmad-method-help, #report-bugs-and-issues)
- **Stuck?** — Run `help` to see what to do next
- **Stuck?** — Run `help` (`bmad-help` on most platforms) to see what to do next
## Key Takeaways

View File

@ -59,7 +59,7 @@ input_file_patterns:
<check if="{{story_path}} is provided by user or user provided the epic and story number such as 2-4 or 1.6 or epic 1 story 5">
<action>Parse user-provided story path: extract epic_num, story_num, story_title from format like "1-2-user-auth"</action>
<action>Set {{epic_num}}, {{story_num}}, {{story_key}} from user input</action>
<action>GOTO step 2a</action>
<action>GOTO step 2</action>
</check>
<action>Check if {{sprint_status}} file exists for auto discover</action>
@ -85,12 +85,12 @@ input_file_patterns:
<check if="user provides epic-story number">
<action>Parse user input: extract epic_num, story_num, story_title</action>
<action>Set {{epic_num}}, {{story_num}}, {{story_key}} from user input</action>
<action>GOTO step 2a</action>
<action>GOTO step 2</action>
</check>
<check if="user provides story docs path">
<action>Use user-provided path for story documents</action>
<action>GOTO step 2a</action>
<action>GOTO step 2</action>
</check>
</check>
@ -152,7 +152,7 @@ input_file_patterns:
<output>📊 Epic {{epic_num}} status updated to in-progress</output>
</check>
<action>GOTO step 2a</action>
<action>GOTO step 2</action>
</check>
</step>
@ -174,10 +174,17 @@ input_file_patterns:
(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>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>
<action>Set {{previous_story_num}} = {{story_num}} - 1</action>
<action>Load previous story file: {{story_dir}}/{{epic_num}}-{{previous_story_num}}-*.md</action>
<action>Extract 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>
<action>Extract all learnings that could impact current story implementation</action>
</check>
<!-- Git intelligence for previous work patterns -->

View File

@ -15,9 +15,17 @@
- Set status_file_found = false
- Set standalone_mode = true
- Set warning = ""
- Set status_load_error_reason = ""
- Set suggestion = ""
- Set next_workflow = ""
- Set next_agent = ""
- Set status_file_path = ""
- Set field_type = ""
- Set workflow_mode = ""
- Set scan_level = ""
- Set subworkflow_success = false
- Set status_update_success = false
- Set cached_project_types = ""
</action>
<action>Attempt to load workflow status directly from `{output_folder}/bmm-workflow-status.yaml`:
@ -29,10 +37,18 @@
- Extract field_type, warning, suggestion, next_workflow, next_agent if present
- If file is missing, unreadable, or malformed:
- Keep defaults and continue in standalone mode
- Set status_load_error_reason from the caught file/parse error (e.g., missing file, permission denied, YAML parse error)
- Set warning = "Unable to load workflow status from {output_folder}/bmm-workflow-status.yaml: {{status_load_error_reason}}"
- Output warning and continue in standalone mode
</action>
<check if="status_exists == false">
<output>{{suggestion}}</output>
<check if="suggestion != ''">
<output>{{suggestion}}</output>
</check>
<check if="warning != ''">
<output>{{warning}}</output>
</check>
<output>Note: Documentation workflow can run standalone. Continuing without progress tracking.</output>
<action>Set standalone_mode = true</action>
<action>Set status_file_found = false</action>
@ -62,7 +78,9 @@
<output>Note: This may be auto-invoked by prd for brownfield documentation.</output>
<ask>Continue with documentation? (y/n)</ask>
<check if="n">
<output>{{suggestion}}</output>
<check if="suggestion != ''">
<output>{{suggestion}}</output>
</check>
<action>Exit workflow</action>
</check>
</check>
@ -78,18 +96,40 @@
<check if="project-scan-report.json exists">
<action>Read state file and extract: timestamps, mode, scan_level, current_step, completed_steps, project_classification</action>
<action>Validate last_updated from state file:
- If last_updated is missing or invalid, set state_age_hours = 999 and mark state as stale
- Otherwise parse last_updated into validated_last_updated
</action>
<action>Extract cached project_type_id(s) from state file if present</action>
<action>Calculate age of state file (current time - last_updated)</action>
<action>Calculate state_age_hours:
- If validated_last_updated exists, compute current time - validated_last_updated
- Otherwise keep state_age_hours = 999
</action>
<check if="state file age >= 24 hours">
<check if="state_age_hours >= 24">
<action>Display: "Found old state file (>24 hours). Starting fresh scan."</action>
<action>Create archive directory: {output_folder}/.archive/</action>
<action>Archive old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action>
<action>Attempt to create archive directory: {output_folder}/.archive/</action>
<check if="archive directory creation failed">
<output>Failed to create archive directory at {output_folder}/.archive/. Keeping existing state and exiting to avoid data loss.</output>
<action>Set resume_mode = true</action>
<action>Exit workflow</action>
</check>
<action>Attempt to archive old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action>
<check if="archive move failed">
<output>Failed to archive old state file. Keeping existing state and exiting to avoid data loss.</output>
<action>Set resume_mode = true</action>
<action>Exit workflow</action>
</check>
<action>Set resume_mode = false</action>
<action>Set workflow_mode = ""</action>
<action>Set scan_level = ""</action>
<action>Set cached_project_types = ""</action>
<action>Set current_step = ""</action>
<action>Set subworkflow_success = false</action>
<action>Continue to Step 3</action>
</check>
<check if="state file age < 24 hours">
<check if="state_age_hours < 24">
<ask>I found an in-progress workflow state from {{last_updated}}.
@ -112,15 +152,25 @@ Your choice [1/2/3]:
<check if="user selects 1">
<action>Set resume_mode = true</action>
<action>Set workflow_mode = {{mode}}</action>
<action>Validate persisted mode before assigning workflow_mode:
- If mode is one of [deep_dive, initial_scan, full_rescan], set workflow_mode = {{mode}}
- Otherwise set workflow_mode = "full_rescan", set resume_mode = false, and continue as fresh scan
</action>
<action>Set subworkflow_success = false</action>
<action>Load findings summaries from state file</action>
<action>Load cached project_type_id(s) from state file</action>
<critical>CONDITIONAL CSV LOADING FOR RESUME:</critical>
<check if="cached_project_types == ''">
<output>No cached project types found. Falling back to full CSV load.</output>
<action>Load project-types.csv and architecture_registry.csv</action>
<action>Load documentation_requirements_csv for active project classification</action>
</check>
<check if="cached_project_types != ''">
<action>For each cached project_type_id, load ONLY the corresponding row from: {documentation_requirements_csv}</action>
<action>Skip loading project-types.csv and architecture_registry.csv (not needed on resume)</action>
<action>Store loaded doc requirements for use in remaining steps</action>
</check>
<action>Display: "Resuming {{workflow_mode}} from {{current_step}} with cached project type(s): {{cached_project_types}}"</action>
@ -152,9 +202,21 @@ Your choice [1/2/3]:
</check>
<check if="user selects 2">
<action>Create archive directory: {output_folder}/.archive/</action>
<action>Move old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action>
<action>Attempt to create archive directory: {output_folder}/.archive/</action>
<check if="archive directory creation failed">
<output>Failed to create archive directory. Keeping existing state and exiting to avoid data loss.</output>
<action>Set resume_mode = true</action>
<action>Exit workflow</action>
</check>
<action>Attempt to move old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action>
<check if="archive move failed">
<output>Failed to archive old state file. Keeping existing state and exiting to avoid data loss.</output>
<action>Set resume_mode = true</action>
<action>Exit workflow</action>
</check>
<action>Set resume_mode = false</action>
<action>Reset workflow_mode, scan_level, cached_project_types, current_step to defaults</action>
<action>Set subworkflow_success = false</action>
<action>Continue to Step 3</action>
</check>
@ -198,6 +260,7 @@ Your choice [1/2/3]:
<check if="user selects 1">
<action>Set workflow_mode = "full_rescan"</action>
<action>Set scan_level = "standard"</action>
<action>Display: "Starting full project rescan..."</action>
<action>Read fully and follow: {installed_path}/workflows/full-scan-instructions.md</action>
<action>Set subworkflow_success = true only if delegated workflow completed without HALT/error</action>
@ -234,6 +297,7 @@ Your choice [1/2/3]:
<check if="index.md does not exist">
<action>Set workflow_mode = "initial_scan"</action>
<action>Set scan_level = "initial"</action>
<action>Display: "No existing documentation found. Starting initial project scan..."</action>
<action>Read fully and follow: {installed_path}/workflows/full-scan-instructions.md</action>
<action>Set subworkflow_success = true only if delegated workflow completed without HALT/error</action>
@ -282,8 +346,13 @@ Your choice [1/2/3]:
<output>**Status Updated:** Progress tracking updated.
**Next Steps:**
- **Next required:** {{next_workflow}} ({{next_agent}} agent)
- Run `bmad-help` if you need recommended next workflows.</output>
<check if="next_workflow != '' AND next_agent != ''">
<output>- **Next required:** {{next_workflow}} ({{next_agent}} agent)</output>
</check>
<check if="next_workflow == '' OR next_agent == ''">
<output>- **Next required:** not specified</output>
</check>
</check>
<check if="status_file_found == false OR status_update_success != true">

View File

@ -31,7 +31,7 @@ Execute a validation checklist against a target file and report findings clearly
- Path-like tokens in checklist items
- First matching path from glob patterns supplied by checklist/input
- Normalize all candidate paths relative to repo root and resolve `.`/`..`.
- Validate candidate existence and expected file type (`.yaml`, `.yml`, `.json`, or checklist-defined extension).
- Validate candidate existence and expected file type (`.md`, `.yaml`, `.yml`, `.json`, or checklist-defined extension).
- If multiple valid candidates remain, prefer explicit key fields over inferred tokens.
- If no valid candidate is found, prompt user with schema example:
- `Please provide the exact file path (relative to repo root), e.g. ./workflows/ci.yml`
@ -50,11 +50,17 @@ Execute a validation checklist against a target file and report findings clearly
- If checklist requires edits/auto-fixes, follow safe-edit protocol:
- Ask for confirmation before editing.
- Create backup snapshot of target file before changes.
- Use deterministic backup location: `{project-root}/.bmad-tmp/validate-workflow/`.
- Name backup as `{target-file-name}.{timestamp}.bak` and diff as `{target-file-name}.{timestamp}.diff`.
- If temp backup directory cannot be created, fall back to adjacent backup file `{target-file}.bak`.
- Generate reversible diff preview and show it to user.
- Apply edits only after user approval.
- Run syntax/validation checks against edited file.
- If validation fails or user cancels, rollback from backup and report rollback status.
- Record backup/diff locations in task output.
- Record full backup and diff paths in task output.
- Support `retain_artifacts` flag (default `false`) to keep backup/diff artifacts when requested.
6. **Finalize**
- Confirm completion and provide the final validation summary.
- If edits succeeded and `retain_artifacts` is `false`, delete backup/diff artifacts and report cleanup status.
- If edits failed or rollback occurred, preserve backup/diff artifacts and report rollback path explicitly.

View File

@ -114,6 +114,12 @@ async function runTests() {
console.log(`========================================${colors.reset}\n`);
const projectRoot = path.join(__dirname, '..');
const tmpRoots = [];
const trackTmp = async (prefix) => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
tmpRoots.push(dir);
return dir;
};
// ============================================================
// Test 1: YAML → XML Agent Compilation (In-Memory)
@ -386,7 +392,7 @@ async function runTests() {
path.join(projectRoot, 'src', 'bmm', 'workflows', 'document-project'),
path.join(projectRoot, 'tools', 'cli', 'installers', 'lib'),
];
const allowedExtensions = new Set(['.md', '.yaml', '.yml', '.xml']);
const allowedExtensions = new Set(['.md', '.yaml', '.yml', '.xml', '.js', '.cjs', '.mjs']);
const forbiddenRefPattern = /(^|[^a-zA-Z0-9_-])workflow\.xml\b/;
const offenders = [];
@ -432,7 +438,7 @@ async function runTests() {
console.log(`${colors.yellow}Test Suite 11: Gemini Template Extension Guard${colors.reset}\n`);
try {
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-gemini-install-'));
const tmpRoot = await trackTmp('bmad-gemini-install-');
const projectDir = path.join(tmpRoot, 'project');
const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME);
await fs.ensureDir(projectDir);
@ -470,7 +476,7 @@ async function runTests() {
console.log(`${colors.yellow}Test Suite 12: Manifest Stale Entry Cleanup Guard${colors.reset}\n`);
try {
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-manifest-clean-'));
const tmpRoot = await trackTmp('bmad-manifest-clean-');
const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME);
await fs.copy(path.join(projectRoot, 'src', 'core'), path.join(bmadDir, 'core'));
await fs.copy(path.join(projectRoot, 'src', 'bmm'), path.join(bmadDir, 'bmm'));
@ -505,7 +511,7 @@ async function runTests() {
console.log(`${colors.yellow}Test Suite 13: Internal Task Exposure Guard${colors.reset}\n`);
try {
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-task-filter-'));
const tmpRoot = await trackTmp('bmad-task-filter-');
const projectDir = path.join(tmpRoot, 'project');
const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME);
const commandsDir = path.join(tmpRoot, 'commands');
@ -592,7 +598,7 @@ web_bundle:
console.log(`${colors.yellow}Test Suite 16: Task/Tool Standalone + CRLF Guard${colors.reset}\n`);
try {
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-standalone-crlf-'));
const tmpRoot = await trackTmp('bmad-standalone-crlf-');
const coreTasksDir = path.join(tmpRoot, '_bmad', 'core', 'tasks');
const coreToolsDir = path.join(tmpRoot, '_bmad', 'core', 'tools');
await fs.ensureDir(coreTasksDir);
@ -717,7 +723,7 @@ internal: true
console.log(`${colors.yellow}Test Suite 18: Codex Task Visibility Guard${colors.reset}\n`);
try {
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-visibility-'));
const tmpRoot = await trackTmp('bmad-codex-visibility-');
const projectDir = path.join(tmpRoot, 'project');
const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME);
await fs.ensureDir(projectDir);
@ -751,7 +757,7 @@ internal: true
console.log(`${colors.yellow}Test Suite 19: Empty Artifact Target Guard${colors.reset}\n`);
try {
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-empty-target-'));
const tmpRoot = await trackTmp('bmad-empty-target-');
const projectDir = path.join(tmpRoot, 'project');
const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME);
await fs.ensureDir(projectDir);
@ -856,6 +862,10 @@ internal: true
console.log('');
for (const tmpRoot of tmpRoots) {
await fs.remove(tmpRoot).catch(() => {});
}
// ============================================================
// Summary
// ============================================================

View File

@ -1201,19 +1201,11 @@ class Installer {
lines.push(` ${icon} ${r.step}${detail}`);
}
// Add context info
// Context and warnings
lines.push('');
if (context.bmadDir) {
lines.push(` Installed to: ${color.dim(context.bmadDir)}`);
}
if (context.modules && context.modules.length > 0) {
lines.push(` Modules: ${color.dim(context.modules.join(', '))}`);
}
if (context.ides && context.ides.length > 0) {
lines.push(` Tools: ${color.dim(context.ides.join(', '))}`);
}
// Custom/modified file warnings
if (context.customFiles && context.customFiles.length > 0) {
lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`);
}

View File

@ -75,10 +75,10 @@ class WorkflowCommandGenerator {
if (workflowRelPath.includes('_bmad/')) {
const parts = workflowRelPath.split(/_bmad\//);
if (parts.length > 1) {
workflowRelPath = parts.slice(1).join('/');
workflowRelPath = parts.at(-1);
}
} else if (workflowRelPath.includes('/src/')) {
const match = workflowRelPath.match(/\/src\/([^/]+)\/(.+)/);
} else if (workflowRelPath.includes('/src/') || workflowRelPath.startsWith('src/')) {
const match = workflowRelPath.match(/(?:^|\/)src\/([^/]+)\/(.+)/);
if (match) {
workflowRelPath = `${match[1]}/${match[2]}`;
}
@ -119,30 +119,9 @@ class WorkflowCommandGenerator {
* Generate command content for a workflow
*/
async generateCommandContent(workflow, bmadDir) {
// Determine template based on workflow file type
const templatePath = path.join(path.dirname(this.templatePath), 'workflow-commander.md');
// Load the appropriate template
const template = await fs.readFile(templatePath, 'utf8');
// Convert source path to installed path
// From: /Users/.../src/bmm/workflows/.../workflow.md
// To: {project-root}/_bmad/bmm/workflows/.../workflow.md
let workflowPath = workflow.path;
// Extract the relative path from source
if (workflowPath.includes('/src/bmm/')) {
// bmm is directly under src/
const match = workflowPath.match(/\/src\/bmm\/(.+)/);
if (match) {
workflowPath = `${this.bmadFolderName}/bmm/${match[1]}`;
}
} else if (workflowPath.includes('/src/core/')) {
const match = workflowPath.match(/\/src\/core\/(.+)/);
if (match) {
workflowPath = `${this.bmadFolderName}/core/${match[1]}`;
}
}
// Load the workflow command template
const template = await fs.readFile(this.templatePath, 'utf8');
const workflowPath = this.mapSourcePathToInstalled(workflow.path);
// Replace template variables
return template
@ -212,14 +191,15 @@ class WorkflowCommandGenerator {
When running any workflow:
1. Resolve loader paths:
- Primary: {project-root}/${this.bmadFolderName}/core/tasks/workflow.md
- Fallback: {project-root}/src/core/tasks/workflow.md
- Optional dev fallback: {project-root}/src/core/tasks/workflow.md (only if it exists and is readable)
2. Check the primary path exists and is readable before loading
3. If primary is missing/unreadable, log a warning with the path and error, then try fallback
4. If fallback is also missing/unreadable, log an error with both attempted paths and stop
5. LOAD the resolved workflow loader file
6. Pass the workflow path as 'workflow-config' parameter
7. Follow workflow.md instructions EXACTLY
8. Save outputs after EACH section
3. If primary is missing/unreadable, log a warning with the primary path and error
4. Only if the dev fallback exists and is readable, try the fallback path; otherwise skip it
5. If no readable loader is found, log an error with all attempted readable paths and stop
6. LOAD the resolved workflow loader file
7. Pass the workflow path as 'workflow-config' parameter
8. Follow workflow.md instructions EXACTLY
9. Save outputs after EACH section
## Modes
- Normal: Full interaction
@ -230,21 +210,33 @@ When running any workflow:
}
transformWorkflowPath(workflowPath) {
let transformed = workflowPath;
return this.mapSourcePathToInstalled(workflowPath, true);
}
if (workflowPath.includes('/src/bmm/')) {
const match = workflowPath.match(/\/src\/bmm\/(.+)/);
if (match) {
transformed = `{project-root}/${this.bmadFolderName}/bmm/${match[1]}`;
}
} else if (workflowPath.includes('/src/core/')) {
const match = workflowPath.match(/\/src\/core\/(.+)/);
if (match) {
transformed = `{project-root}/${this.bmadFolderName}/core/${match[1]}`;
}
mapSourcePathToInstalled(sourcePath, includeProjectRootPrefix = false) {
if (!sourcePath) {
return sourcePath;
}
return transformed;
const normalized = sourcePath.replaceAll('\\', '/');
const srcMatch = normalized.match(/(?:^|\/)src\/([^/]+)\/(.+)/);
if (srcMatch) {
const mapped = `${this.bmadFolderName}/${srcMatch[1]}/${srcMatch[2]}`;
return includeProjectRootPrefix ? `{project-root}/${mapped}` : mapped;
}
if (normalized.includes('_bmad/')) {
const parts = normalized.split(/_bmad\//);
const relative = parts.at(-1);
const mapped = `${this.bmadFolderName}/${relative}`;
return includeProjectRootPrefix ? `{project-root}/${mapped}` : mapped;
}
if (normalized.startsWith(`${this.bmadFolderName}/`)) {
return includeProjectRootPrefix ? `{project-root}/${normalized}` : normalized;
}
return sourcePath;
}
async loadWorkflowManifest(bmadDir) {

View File

@ -3,6 +3,7 @@ name: '{{name}}'
description: '{{description}}'
---
Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}}
Follow all instructions in the workflow file exactly as written.
1. Load the workflow runner at {project-root}/_bmad/core/tasks/workflow.md
2. Read the runner fully
3. Run it with workflow-config: {{workflow_path}}
4. Follow all runner instructions exactly

View File

@ -4,4 +4,8 @@ description: '{{description}}'
disable-model-invocation: true
---
IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL @{project-root}/{{bmadFolderName}}/{{path}}, READ its entire contents and follow its directions exactly!
IT IS CRITICAL THAT YOU FOLLOW THESE STEPS:
1. LOAD the FULL @{project-root}/{{bmadFolderName}}/core/tasks/workflow.md
2. READ its entire contents
3. Execute workflow runner with parameter: workflow-config: {{bmadFolderName}}/{{path}}
4. FOLLOW the runner instructions exactly as written

View File

@ -2,15 +2,11 @@ description = """{{description}}"""
prompt = """
Execute the BMAD '{{name}}' workflow.
CRITICAL: This is a structured YAML workflow. Follow these steps precisely:
CRITICAL: Use the workflow runner task, not direct workflow-file execution.
1. LOAD the workflow definition from {project-root}/{{bmadFolderName}}/{{workflow_path}}
2. PARSE the YAML structure to understand:
- Workflow phases and steps
- Required inputs and outputs
- Dependencies between steps
3. EXECUTE each step in order
4. VALIDATE outputs before proceeding to next step
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{workflow_path}}
WORKFLOW INSTRUCTIONS:
1. LOAD the workflow runner from {project-root}/{{bmadFolderName}}/core/tasks/workflow.md
2. READ its entire contents
3. PASS this parameter to the runner: workflow-config: {{workflow_path}}
4. FOLLOW every runner step exactly as specified
"""

View File

@ -2,13 +2,11 @@ description = """{{description}}"""
prompt = """
Execute the BMAD '{{name}}' workflow.
CRITICAL: You must load and follow the workflow definition exactly.
CRITICAL: Use the workflow runner task, not direct workflow-file execution.
WORKFLOW INSTRUCTIONS:
1. LOAD the workflow file from {project-root}/{{bmadFolderName}}/{{workflow_path}}
1. LOAD the workflow runner from {project-root}/{{bmadFolderName}}/core/tasks/workflow.md
2. READ its entire contents
3. FOLLOW every step precisely as specified
4. DO NOT skip or modify any steps
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{workflow_path}}
3. PASS this parameter to the runner: workflow-config: {{workflow_path}}
4. FOLLOW every runner step exactly as specified
"""

View File

@ -4,4 +4,11 @@ inclusion: manual
# {{name}}
IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL #[[file:{{bmadFolderName}}/{{path}}]], READ its entire contents and follow its directions exactly!
IT IS CRITICAL THAT YOU FOLLOW THESE STEPS:
1. Always LOAD the FULL #[[file:{{bmadFolderName}}/core/tasks/workflow.md]]
2. READ its entire contents
3. Execute workflow runner with YAML parameter:
```yaml
workflow-config: {{bmadFolderName}}/{{path}}
```
4. FOLLOW the runner instructions exactly as written

View File

@ -4,12 +4,10 @@ description: '{{description}}'
Execute the BMAD '{{name}}' workflow.
CRITICAL: You must load and follow the workflow definition exactly.
CRITICAL: Use the workflow runner task, not direct workflow-file execution.
WORKFLOW INSTRUCTIONS:
1. LOAD the workflow file from {project-root}/{{bmadFolderName}}/{{path}}
1. LOAD the workflow runner from {project-root}/{{bmadFolderName}}/core/tasks/workflow.md
2. READ its entire contents
3. FOLLOW every step precisely as specified
4. DO NOT skip or modify any steps
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{path}}
3. PASS this parameter to the runner: workflow-config: {{bmadFolderName}}/{{path}}
4. FOLLOW every runner step exactly as specified

View File

@ -4,12 +4,10 @@ description: '{{description}}'
Execute the BMAD '{{name}}' workflow.
CRITICAL: You must load and follow the workflow definition exactly.
CRITICAL: Use the workflow runner task, not direct workflow-file execution.
WORKFLOW INSTRUCTIONS:
1. LOAD the workflow file from {project-root}/{{bmadFolderName}}/{{path}}
1. LOAD the workflow runner from {project-root}/{{bmadFolderName}}/core/tasks/workflow.md
2. READ its entire contents
3. FOLLOW every step precisely as specified
4. DO NOT skip or modify any steps
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{path}}
3. PASS this parameter to the runner: workflow-config: {{bmadFolderName}}/{{path}}
4. FOLLOW every runner step exactly as specified

View File

@ -4,6 +4,7 @@
---
Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}}
Follow all instructions in the workflow file exactly as written.
1. Load the workflow runner at {project-root}/_bmad/core/tasks/workflow.md
2. Read the runner fully
3. Run it with workflow-config: {{workflow_path}}
4. Follow all runner instructions exactly

View File

@ -4,6 +4,7 @@
## Instructions
Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}}
Follow all instructions in the workflow file exactly as written.
1. Load the workflow runner at {project-root}/_bmad/core/tasks/workflow.md
2. Read the runner fully
3. Run it with workflow-config: {{workflow_path}}
4. Follow all runner instructions exactly

View File

@ -5,6 +5,7 @@ auto_execution_mode: "iterate"
# {{name}}
Read the entire workflow file at {project-root}/_bmad/{{workflow_path}}
Follow all instructions in the workflow file exactly as written.
1. Load the workflow runner at {project-root}/_bmad/core/tasks/workflow.md
2. Read the runner fully
3. Run it with workflow-config: {{workflow_path}}
4. Follow all runner instructions exactly

View File

@ -3,4 +3,8 @@ description: '{{description}}'
disable-model-invocation: true
---
IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL @{{workflow_path}}, READ its entire contents and follow its directions exactly!
IT IS CRITICAL THAT YOU FOLLOW THESE STEPS:
1. LOAD the FULL @{project-root}/_bmad/core/tasks/workflow.md
2. READ its entire contents
3. Execute workflow runner with parameter: workflow-config: {{workflow_path}}
4. FOLLOW the runner instructions exactly as written

View File

@ -813,7 +813,6 @@ class ModuleManager {
const newline = frontmatterMatch[0].includes('\r\n') ? '\r\n' : '\n';
try {
const yaml = require('yaml');
const parsed = yaml.parse(frontmatterMatch[1]);
if (!parsed || typeof parsed !== 'object' || !Object.prototype.hasOwnProperty.call(parsed, 'web_bundle')) {
@ -897,7 +896,6 @@ class ModuleManager {
let manifestData = {};
if (await fs.pathExists(manifestPath)) {
const manifestContent = await fs.readFile(manifestPath, 'utf8');
const yaml = require('yaml');
manifestData = yaml.parse(manifestContent);
}
if (!manifestData.agentCustomizations) {
@ -906,7 +904,6 @@ class ModuleManager {
manifestData.agentCustomizations[path.relative(bmadDir, customizePath)] = originalHash;
// Write back to manifest
const yaml = require('yaml');
// Clean the manifest data to remove any non-serializable values
const cleanManifestData = structuredClone(manifestData);
@ -1241,16 +1238,31 @@ class ModuleManager {
}
}
const installerPath = path.join(sourcePath, '_module-installer', 'installer.js');
const installerDir = path.join(sourcePath, '_module-installer');
// Prefer .cjs (always CommonJS) then fall back to .js
const cjsPath = path.join(installerDir, 'installer.cjs');
const jsPath = path.join(installerDir, 'installer.js');
const hasCjs = await fs.pathExists(cjsPath);
const installerPath = hasCjs ? cjsPath : jsPath;
// Check if module has a custom installer
if (!(await fs.pathExists(installerPath))) {
if (!hasCjs && !(await fs.pathExists(jsPath))) {
return; // No custom installer
}
try {
// Load the module installer
const moduleInstaller = require(installerPath);
// .cjs files are always CommonJS and safe to require().
// .js files may be ESM (when the package sets "type":"module"),
// so use dynamic import() which handles both CJS and ESM.
let moduleInstaller;
if (hasCjs) {
moduleInstaller = require(installerPath);
} else {
const { pathToFileURL } = require('node:url');
const imported = await import(pathToFileURL(installerPath).href);
// CJS module.exports lands on .default; ESM default can be object, function, or class
moduleInstaller = imported.default == null ? imported : imported.default;
}
if (typeof moduleInstaller.install === 'function') {
// Get project root (parent of bmad directory)
@ -1276,8 +1288,12 @@ class ModuleManager {
await prompts.log.warn(`Module installer for ${moduleName} returned false`);
}
}
} catch (error) {
await prompts.log.error(`Error running module installer for ${moduleName}: ${error.message}`);
} catch {
// Post-install scripts are optional; module files are already installed.
// TODO: Eliminate post-install scripts entirely by adding a `directories` key
// to module.yaml that declares which config keys are paths to auto-create.
// The main installer can then handle directory creation centrally, removing
// the need for per-module installer.js scripts and their CJS/ESM issues.
}
}

View File

@ -274,7 +274,6 @@ class UI {
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
} else {
selectedModules = await this.selectAllModules(installedModuleIds);
selectedModules = selectedModules.filter((m) => m !== 'core');
}
// After module selection, ask about custom modules
@ -362,6 +361,9 @@ 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);
@ -899,107 +901,10 @@ class UI {
}
/**
* 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
* Select all modules (official + community) using grouped multiselect.
* Core is shown as locked but filtered from the result since it's always installed separately.
* @param {Set} installedModuleIds - Currently installed module IDs
* @returns {Array} Selected module codes
* @returns {Array} Selected module codes (excluding core)
*/
async selectAllModules(installedModuleIds = new Set()) {
const { ModuleManager } = require('../installers/lib/modules/manager');
@ -1068,11 +973,7 @@ class UI {
}
}
}
allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })), {
// "None" option at the end
label: '\u26A0 None - Skip module installation',
value: '__NONE__',
});
allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })));
const selected = await prompts.autocompleteMultiselect({
message: 'Select modules to install:',
@ -1083,14 +984,7 @@ class UI {
maxItems: allOptions.length,
});
// 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__') : [];
const result = selected ? selected.filter((m) => m !== 'core') : [];
// Display selected modules as bulleted list
if (result.length > 0) {