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) ### Phase 1: Analysis (Optional)
All workflows in this phase are optional: All workflows in this phase are optional:
- **brainstorming** — Guided ideation - **brainstorming** (`bmad-bmm-brainstorming`) — Guided ideation
- **research** — Market and technical research - **research** (`bmad-bmm-research`) — Market and technical research
- **create-product-brief** — Recommended foundation document - **create-product-brief** (`bmad-bmm-create-product-brief`) — Recommended foundation document
### Phase 2: Planning (Required) ### Phase 2: Planning (Required)
**For BMad Method and Enterprise tracks:** **For BMad Method and Enterprise tracks:**
1. Load the **PM agent** in a new chat 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` 3. Output: `PRD.md`
**For Quick Flow track:** **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** **Create Architecture**
1. Load the **Architect agent** in a new chat 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 3. Output: Architecture document with technical decisions
**Create Epics and Stories** **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 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 3. The workflow uses both PRD and Architecture to create technically-informed stories
**Implementation Readiness Check** *(Highly Recommended)* **Implementation Readiness Check** *(Highly Recommended)*
1. Load the **Architect agent** in a new chat 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 3. Validates cohesion across all planning documents
## Step 2: Build Your Project ## Step 2: Build Your Project
@ -124,7 +124,7 @@ Once planning is complete, move to implementation. **Each workflow should run in
### Initialize Sprint Planning ### 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 ### The Build Cycle
@ -132,11 +132,11 @@ For each story, repeat this cycle with fresh chats:
| Step | Agent | Workflow | Purpose | | Step | Agent | Workflow | Purpose |
| ---- | ----- | -------------- | ---------------------------------- | | ---- | ----- | -------------- | ---------------------------------- |
| 1 | SM | `create-story` | Create story file from epic | | 1 | SM | `create-story` (`bmad-bmm-create-story`) | Create story file from epic |
| 2 | DEV | `dev-story` | Implement the story | | 2 | DEV | `dev-story` (`bmad-bmm-dev-story`) | Implement the story |
| 3 | DEV | `code-review` | Quality validation *(recommended)* | | 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 ## What You've Accomplished
@ -162,17 +162,17 @@ your-project/
## Quick Reference ## Quick Reference
| Workflow | Agent | Purpose | | Workflow | Slash Command | Agent | Purpose |
| -------------------------------- | --------- | ------------------------------------ | | -------------------------------- | ------------------------------------- | --------- | ------------------------------------ |
| `help` | Any | Get guidance on what to do next | | `help` | `bmad-help` | Any | Get guidance on what to do next |
| `prd` | PM | Create Product Requirements Document | | `create-prd` | `bmad-bmm-create-prd` | PM | Create Product Requirements Document |
| `create-architecture` | Architect | Create architecture document | | `create-architecture` | `bmad-bmm-create-architecture` | Architect | Create architecture document |
| `create-epics-and-stories` | PM | Break down PRD into epics | | `create-epics-and-stories` | `bmad-bmm-create-epics-and-stories` | PM | Break down PRD into epics |
| `check-implementation-readiness` | Architect | Validate planning cohesion | | `check-implementation-readiness` | `bmad-bmm-check-implementation-readiness` | Architect | Validate planning cohesion |
| `sprint-planning` | SM | Initialize sprint tracking | | `sprint-planning` | `bmad-bmm-sprint-planning` | SM | Initialize sprint tracking |
| `create-story` | SM | Create a story file | | `create-story` | `bmad-bmm-create-story` | SM | Create a story file |
| `dev-story` | DEV | Implement a story | | `dev-story` | `bmad-bmm-dev-story` | DEV | Implement a story |
| `code-review` | DEV | Review implemented code | | `code-review` | `bmad-bmm-code-review` | DEV | Review implemented code |
## Common Questions ## 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. Yes. The SM agent has a `correct-course` workflow for handling scope changes.
**What if I want to brainstorm first?** **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?** **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. 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 - **During workflows** — Agents guide you with questions and explanations
- **Community** — [Discord](https://discord.gg/gk8jAdXWmj) (#bmad-method-help, #report-bugs-and-issues) - **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 ## 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"> <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>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>Set {{epic_num}}, {{story_num}}, {{story_key}} from user input</action>
<action>GOTO step 2a</action> <action>GOTO step 2</action>
</check> </check>
<action>Check if {{sprint_status}} file exists for auto discover</action> <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"> <check if="user provides epic-story number">
<action>Parse user input: extract epic_num, story_num, story_title</action> <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>Set {{epic_num}}, {{story_num}}, {{story_key}} from user input</action>
<action>GOTO step 2a</action> <action>GOTO step 2</action>
</check> </check>
<check if="user provides story docs path"> <check if="user provides story docs path">
<action>Use user-provided path for story documents</action> <action>Use user-provided path for story documents</action>
<action>GOTO step 2a</action> <action>GOTO step 2</action>
</check> </check>
</check> </check>
@ -152,7 +152,7 @@ input_file_patterns:
<output>📊 Epic {{epic_num}} status updated to in-progress</output> <output>📊 Epic {{epic_num}} status updated to in-progress</output>
</check> </check>
<action>GOTO step 2a</action> <action>GOTO step 2</action>
</check> </check>
</step> </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 - (As a, I want, so that) - Detailed acceptance criteria (already BDD formatted) - Technical requirements specific to this story -
Business context and value - Success criteria <!-- Previous story analysis for context continuity --> Business context and value - Success criteria <!-- Previous story analysis for context continuity -->
<check if="story_num > 1"> <check if="story_num > 1">
<action>Load previous story file: {{story_dir}}/{{epic_num}}-{{previous_story_num}}-*.md</action> **PREVIOUS STORY INTELLIGENCE:** - <action>Set {{previous_story_num}} = {{story_num}} - 1</action>
Dev notes and learnings from previous story - Review feedback and corrections needed - Files that were created/modified and their <action>Load previous story file: {{story_dir}}/{{epic_num}}-{{previous_story_num}}-*.md</action>
patterns - Testing approaches that worked/didn't work - Problems encountered and solutions found - Code patterns established <action>Extract <action>Extract previous story intelligence:
all learnings that could impact current story implementation</action> - 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> </check>
<!-- Git intelligence for previous work patterns --> <!-- Git intelligence for previous work patterns -->

View File

@ -15,9 +15,17 @@
- Set status_file_found = false - Set status_file_found = false
- Set standalone_mode = true - Set standalone_mode = true
- Set warning = "" - Set warning = ""
- Set status_load_error_reason = ""
- Set suggestion = "" - Set suggestion = ""
- Set next_workflow = "" - Set next_workflow = ""
- Set next_agent = "" - 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>
<action>Attempt to load workflow status directly from `{output_folder}/bmm-workflow-status.yaml`: <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 - Extract field_type, warning, suggestion, next_workflow, next_agent if present
- If file is missing, unreadable, or malformed: - If file is missing, unreadable, or malformed:
- Keep defaults and continue in standalone mode - 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> </action>
<check if="status_exists == false"> <check if="status_exists == false">
<check if="suggestion != ''">
<output>{{suggestion}}</output> <output>{{suggestion}}</output>
</check>
<check if="warning != ''">
<output>{{warning}}</output>
</check>
<output>Note: Documentation workflow can run standalone. Continuing without progress tracking.</output> <output>Note: Documentation workflow can run standalone. Continuing without progress tracking.</output>
<action>Set standalone_mode = true</action> <action>Set standalone_mode = true</action>
<action>Set status_file_found = false</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> <output>Note: This may be auto-invoked by prd for brownfield documentation.</output>
<ask>Continue with documentation? (y/n)</ask> <ask>Continue with documentation? (y/n)</ask>
<check if="n"> <check if="n">
<check if="suggestion != ''">
<output>{{suggestion}}</output> <output>{{suggestion}}</output>
</check>
<action>Exit workflow</action> <action>Exit workflow</action>
</check> </check>
</check> </check>
@ -78,18 +96,40 @@
<check if="project-scan-report.json exists"> <check if="project-scan-report.json exists">
<action>Read state file and extract: timestamps, mode, scan_level, current_step, completed_steps, project_classification</action> <action>Read state file and extract: timestamps, mode, scan_level, current_step, completed_steps, project_classification</action>
<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>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>Display: "Found old state file (>24 hours). Starting fresh scan."</action>
<action>Create archive directory: {output_folder}/.archive/</action> <action>Attempt to create archive directory: {output_folder}/.archive/</action>
<action>Archive old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</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 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> <action>Continue to Step 3</action>
</check> </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}}. <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"> <check if="user selects 1">
<action>Set resume_mode = true</action> <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>Set subworkflow_success = false</action>
<action>Load findings summaries from state file</action> <action>Load findings summaries from state file</action>
<action>Load cached project_type_id(s) from state file</action> <action>Load cached project_type_id(s) from state file</action>
<critical>CONDITIONAL CSV LOADING FOR RESUME:</critical> <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>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>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> <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> <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>
<check if="user selects 2"> <check if="user selects 2">
<action>Create archive directory: {output_folder}/.archive/</action> <action>Attempt to create archive directory: {output_folder}/.archive/</action>
<action>Move old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</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>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> <action>Continue to Step 3</action>
</check> </check>
@ -198,6 +260,7 @@ Your choice [1/2/3]:
<check if="user selects 1"> <check if="user selects 1">
<action>Set workflow_mode = "full_rescan"</action> <action>Set workflow_mode = "full_rescan"</action>
<action>Set scan_level = "standard"</action>
<action>Display: "Starting full project rescan..."</action> <action>Display: "Starting full project rescan..."</action>
<action>Read fully and follow: {installed_path}/workflows/full-scan-instructions.md</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> <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"> <check if="index.md does not exist">
<action>Set workflow_mode = "initial_scan"</action> <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>Display: "No existing documentation found. Starting initial project scan..."</action>
<action>Read fully and follow: {installed_path}/workflows/full-scan-instructions.md</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> <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. <output>**Status Updated:** Progress tracking updated.
**Next Steps:** **Next Steps:**
- **Next required:** {{next_workflow}} ({{next_agent}} agent)
- Run `bmad-help` if you need recommended next workflows.</output> - 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>
<check if="status_file_found == false OR status_update_success != true"> <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 - Path-like tokens in checklist items
- First matching path from glob patterns supplied by checklist/input - First matching path from glob patterns supplied by checklist/input
- Normalize all candidate paths relative to repo root and resolve `.`/`..`. - 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 multiple valid candidates remain, prefer explicit key fields over inferred tokens.
- If no valid candidate is found, prompt user with schema example: - 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` - `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: - If checklist requires edits/auto-fixes, follow safe-edit protocol:
- Ask for confirmation before editing. - Ask for confirmation before editing.
- Create backup snapshot of target file before changes. - 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. - Generate reversible diff preview and show it to user.
- Apply edits only after user approval. - Apply edits only after user approval.
- Run syntax/validation checks against edited file. - Run syntax/validation checks against edited file.
- If validation fails or user cancels, rollback from backup and report rollback status. - 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** 6. **Finalize**
- Confirm completion and provide the final validation summary. - 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`); console.log(`========================================${colors.reset}\n`);
const projectRoot = path.join(__dirname, '..'); 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) // 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, 'src', 'bmm', 'workflows', 'document-project'),
path.join(projectRoot, 'tools', 'cli', 'installers', 'lib'), 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 forbiddenRefPattern = /(^|[^a-zA-Z0-9_-])workflow\.xml\b/;
const offenders = []; const offenders = [];
@ -432,7 +438,7 @@ async function runTests() {
console.log(`${colors.yellow}Test Suite 11: Gemini Template Extension Guard${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 11: Gemini Template Extension Guard${colors.reset}\n`);
try { 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 projectDir = path.join(tmpRoot, 'project');
const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME); const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME);
await fs.ensureDir(projectDir); 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`); console.log(`${colors.yellow}Test Suite 12: Manifest Stale Entry Cleanup Guard${colors.reset}\n`);
try { 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); 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', 'core'), path.join(bmadDir, 'core'));
await fs.copy(path.join(projectRoot, 'src', 'bmm'), path.join(bmadDir, 'bmm')); 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`); console.log(`${colors.yellow}Test Suite 13: Internal Task Exposure Guard${colors.reset}\n`);
try { 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 projectDir = path.join(tmpRoot, 'project');
const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME); const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME);
const commandsDir = path.join(tmpRoot, 'commands'); 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`); console.log(`${colors.yellow}Test Suite 16: Task/Tool Standalone + CRLF Guard${colors.reset}\n`);
try { 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 coreTasksDir = path.join(tmpRoot, '_bmad', 'core', 'tasks');
const coreToolsDir = path.join(tmpRoot, '_bmad', 'core', 'tools'); const coreToolsDir = path.join(tmpRoot, '_bmad', 'core', 'tools');
await fs.ensureDir(coreTasksDir); await fs.ensureDir(coreTasksDir);
@ -717,7 +723,7 @@ internal: true
console.log(`${colors.yellow}Test Suite 18: Codex Task Visibility Guard${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 18: Codex Task Visibility Guard${colors.reset}\n`);
try { 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 projectDir = path.join(tmpRoot, 'project');
const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME); const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME);
await fs.ensureDir(projectDir); await fs.ensureDir(projectDir);
@ -751,7 +757,7 @@ internal: true
console.log(`${colors.yellow}Test Suite 19: Empty Artifact Target Guard${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 19: Empty Artifact Target Guard${colors.reset}\n`);
try { 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 projectDir = path.join(tmpRoot, 'project');
const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME); const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME);
await fs.ensureDir(projectDir); await fs.ensureDir(projectDir);
@ -856,6 +862,10 @@ internal: true
console.log(''); console.log('');
for (const tmpRoot of tmpRoots) {
await fs.remove(tmpRoot).catch(() => {});
}
// ============================================================ // ============================================================
// Summary // Summary
// ============================================================ // ============================================================

View File

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

View File

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

View File

@ -3,6 +3,7 @@ name: '{{name}}'
description: '{{description}}' description: '{{description}}'
--- ---
Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}} 1. Load the workflow runner at {project-root}/_bmad/core/tasks/workflow.md
2. Read the runner fully
Follow all instructions in the workflow file exactly as written. 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 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 = """ prompt = """
Execute the BMAD '{{name}}' workflow. 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}} WORKFLOW INSTRUCTIONS:
2. PARSE the YAML structure to understand: 1. LOAD the workflow runner from {project-root}/{{bmadFolderName}}/core/tasks/workflow.md
- Workflow phases and steps 2. READ its entire contents
- Required inputs and outputs 3. PASS this parameter to the runner: workflow-config: {{workflow_path}}
- Dependencies between steps 4. FOLLOW every runner step exactly as specified
3. EXECUTE each step in order
4. VALIDATE outputs before proceeding to next step
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{workflow_path}}
""" """

View File

@ -2,13 +2,11 @@ description = """{{description}}"""
prompt = """ prompt = """
Execute the BMAD '{{name}}' workflow. 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: 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 2. READ its entire contents
3. FOLLOW every step precisely as specified 3. PASS this parameter to the runner: workflow-config: {{workflow_path}}
4. DO NOT skip or modify any steps 4. FOLLOW every runner step exactly as specified
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{workflow_path}}
""" """

View File

@ -4,4 +4,11 @@ inclusion: manual
# {{name}} # {{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. 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: 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 2. READ its entire contents
3. FOLLOW every step precisely as specified 3. PASS this parameter to the runner: workflow-config: {{bmadFolderName}}/{{path}}
4. DO NOT skip or modify any steps 4. FOLLOW every runner step exactly as specified
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{path}}

View File

@ -4,12 +4,10 @@ description: '{{description}}'
Execute the BMAD '{{name}}' workflow. 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: 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 2. READ its entire contents
3. FOLLOW every step precisely as specified 3. PASS this parameter to the runner: workflow-config: {{bmadFolderName}}/{{path}}
4. DO NOT skip or modify any steps 4. FOLLOW every runner step exactly as specified
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{path}}

View File

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

View File

@ -4,6 +4,7 @@
## Instructions ## Instructions
Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}} 1. Load the workflow runner at {project-root}/_bmad/core/tasks/workflow.md
2. Read the runner fully
Follow all instructions in the workflow file exactly as written. 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}} # {{name}}
Read the entire workflow file at {project-root}/_bmad/{{workflow_path}} 1. Load the workflow runner at {project-root}/_bmad/core/tasks/workflow.md
2. Read the runner fully
Follow all instructions in the workflow file exactly as written. 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 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'; const newline = frontmatterMatch[0].includes('\r\n') ? '\r\n' : '\n';
try { try {
const yaml = require('yaml');
const parsed = yaml.parse(frontmatterMatch[1]); const parsed = yaml.parse(frontmatterMatch[1]);
if (!parsed || typeof parsed !== 'object' || !Object.prototype.hasOwnProperty.call(parsed, 'web_bundle')) { if (!parsed || typeof parsed !== 'object' || !Object.prototype.hasOwnProperty.call(parsed, 'web_bundle')) {
@ -897,7 +896,6 @@ class ModuleManager {
let manifestData = {}; let manifestData = {};
if (await fs.pathExists(manifestPath)) { if (await fs.pathExists(manifestPath)) {
const manifestContent = await fs.readFile(manifestPath, 'utf8'); const manifestContent = await fs.readFile(manifestPath, 'utf8');
const yaml = require('yaml');
manifestData = yaml.parse(manifestContent); manifestData = yaml.parse(manifestContent);
} }
if (!manifestData.agentCustomizations) { if (!manifestData.agentCustomizations) {
@ -906,7 +904,6 @@ class ModuleManager {
manifestData.agentCustomizations[path.relative(bmadDir, customizePath)] = originalHash; manifestData.agentCustomizations[path.relative(bmadDir, customizePath)] = originalHash;
// Write back to manifest // Write back to manifest
const yaml = require('yaml');
// Clean the manifest data to remove any non-serializable values // Clean the manifest data to remove any non-serializable values
const cleanManifestData = structuredClone(manifestData); 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 // Check if module has a custom installer
if (!(await fs.pathExists(installerPath))) { if (!hasCjs && !(await fs.pathExists(jsPath))) {
return; // No custom installer return; // No custom installer
} }
try { try {
// Load the module installer // .cjs files are always CommonJS and safe to require().
const moduleInstaller = require(installerPath); // .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') { if (typeof moduleInstaller.install === 'function') {
// Get project root (parent of bmad directory) // Get project root (parent of bmad directory)
@ -1276,8 +1288,12 @@ class ModuleManager {
await prompts.log.warn(`Module installer for ${moduleName} returned false`); await prompts.log.warn(`Module installer for ${moduleName} returned false`);
} }
} }
} catch (error) { } catch {
await prompts.log.error(`Error running module installer for ${moduleName}: ${error.message}`); // 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(', ')}`); await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
} else { } else {
selectedModules = await this.selectAllModules(installedModuleIds); selectedModules = await this.selectAllModules(installedModuleIds);
selectedModules = selectedModules.filter((m) => m !== 'core');
} }
// After module selection, ask about custom modules // After module selection, ask about custom modules
@ -362,6 +361,9 @@ class UI {
selectedModules.push(...customModuleResult.selectedCustomModules); selectedModules.push(...customModuleResult.selectedCustomModules);
} }
// Filter out core - it's always installed via installCore flag
selectedModules = selectedModules.filter((m) => m !== 'core');
// Get tool selection // Get tool selection
const toolSelection = await this.promptToolSelection(confirmedDirectory, options); const toolSelection = await this.promptToolSelection(confirmedDirectory, options);
@ -899,107 +901,10 @@ class UI {
} }
/** /**
* Prompt for module selection * Select all modules (official + community) using grouped multiselect.
* @param {Array} moduleChoices - Available module choices * Core is shown as locked but filtered from the result since it's always installed separately.
* @returns {Array} Selected module IDs
*/
async selectModules(moduleChoices, defaultSelections = null) {
// If defaultSelections is provided, use it to override checked state
// Otherwise preserve the checked state from moduleChoices (set by getModuleChoices)
const choicesWithDefaults = moduleChoices.map((choice) => ({
...choice,
...(defaultSelections === null ? {} : { checked: defaultSelections.includes(choice.value) }),
}));
// Add a "None" option at the end for users who changed their mind
const choicesWithSkipOption = [
...choicesWithDefaults,
{
value: '__NONE__',
label: '\u26A0 None / I changed my mind - skip module installation',
checked: false,
},
];
const selected = await prompts.multiselect({
message: 'Select modules to install (use arrow keys, space to toggle):',
choices: choicesWithSkipOption,
required: true,
});
// If user selected both "__NONE__" and other items, honor the "None" choice
if (selected && selected.includes('__NONE__') && selected.length > 1) {
await prompts.log.warn('"None / I changed my mind" was selected, so no modules will be installed.');
return [];
}
// Filter out the special '__NONE__' value
return selected ? selected.filter((m) => m !== '__NONE__') : [];
}
/**
* Get external module choices for selection
* @returns {Array} External module choices for prompt
*/
async getExternalModuleChoices() {
const externalManager = new ExternalModuleManager();
const modules = await externalManager.listAvailable();
return modules.map((mod) => ({
name: mod.name,
value: mod.code, // Use the code (e.g., 'cis') as the value
checked: mod.defaultSelected || false,
hint: mod.description || undefined, // Show description as hint
module: mod, // Store full module info for later use
}));
}
/**
* Prompt for external module selection
* @param {Array} externalModuleChoices - Available external module choices
* @param {Array} defaultSelections - Module codes to pre-select
* @returns {Array} Selected external module codes
*/
async selectExternalModules(externalModuleChoices, defaultSelections = []) {
// Build a message showing available modules
const message = 'Select official BMad modules to install (use arrow keys, space to toggle):';
// Mark choices as checked based on defaultSelections
const choicesWithDefaults = externalModuleChoices.map((choice) => ({
...choice,
checked: defaultSelections.includes(choice.value),
}));
// Add a "None" option at the end for users who changed their mind
const choicesWithSkipOption = [
...choicesWithDefaults,
{
name: '⚠ None / I changed my mind - skip external module installation',
value: '__NONE__',
checked: false,
},
];
const selected = await prompts.multiselect({
message,
choices: choicesWithSkipOption,
required: true,
});
// If user selected both "__NONE__" and other items, honor the "None" choice
if (selected && selected.includes('__NONE__') && selected.length > 1) {
await prompts.log.warn('"None / I changed my mind" was selected, so no external modules will be installed.');
return [];
}
// Filter out the special '__NONE__' value
return selected ? selected.filter((m) => m !== '__NONE__') : [];
}
/**
* Select all modules (core + official + community) using grouped multiselect
* @param {Set} installedModuleIds - Currently installed module IDs * @param {Set} installedModuleIds - Currently installed module IDs
* @returns {Array} Selected module codes * @returns {Array} Selected module codes (excluding core)
*/ */
async selectAllModules(installedModuleIds = new Set()) { async selectAllModules(installedModuleIds = new Set()) {
const { ModuleManager } = require('../installers/lib/modules/manager'); const { ModuleManager } = require('../installers/lib/modules/manager');
@ -1068,11 +973,7 @@ class UI {
} }
} }
} }
allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })), { allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })));
// "None" option at the end
label: '\u26A0 None - Skip module installation',
value: '__NONE__',
});
const selected = await prompts.autocompleteMultiselect({ const selected = await prompts.autocompleteMultiselect({
message: 'Select modules to install:', message: 'Select modules to install:',
@ -1083,14 +984,7 @@ class UI {
maxItems: allOptions.length, maxItems: allOptions.length,
}); });
// If user selected both "__NONE__" and other items, honor the "None" choice const result = selected ? selected.filter((m) => m !== 'core') : [];
if (selected && selected.includes('__NONE__') && selected.length > 1) {
await prompts.log.warn('"None" was selected, so no modules will be installed.');
return [];
}
// Filter out the special '__NONE__' value
const result = selected ? selected.filter((m) => m !== '__NONE__') : [];
// Display selected modules as bulleted list // Display selected modules as bulleted list
if (result.length > 0) { if (result.length > 0) {