Compare commits

..

8 Commits

Author SHA1 Message Date
Brian Madison df176d4206 installer remove double tool questioning 2026-02-03 21:36:21 -06:00
Davor Racic 2d9ebcaf2f
feat: Update @clack/prompts to v1.0.0 and Add autocompleteMultiselect prompt (#1514)
* feat: Update @clack/prompts to v1.0.0 and Add autocompleteMultiselect prompt

* fix(cli): flexible tool selection (skip recommended or additional) + fix spacing

* feat(cli): improve tool selection UX with autocomplete and upgrade path

* feat(cli): display selected tools after IDE selection with preferred markers

* fix: formatting

* fix: make selection message more clear

* fix: formatting

* fix: Remove redundant colon

---------

Co-authored-by: Brian <bmadcode@gmail.com>
2026-02-03 17:39:05 -06:00
Davor Racic 5b80649d3a
fix(installer): Multiple installer fixes (#1492)
* fix: support CRLF line endings and add task/tool templates for all IDEs

* fix: preserve file extensions in IDE task/tool paths and update BMAD branding

* fix: double extension issue in wrapper filename generation

* fix: correct path handling and variable reference in task/tool command generator

* fix: change default BMAD folder name from 'bmad' to '_bmad' across all IDE components

* refactor: centralize BMAD_FOLDER_NAME constant in path-utils

* fix: Replace the rest of BMAD_FOLDER magic values

* fix: add safety checks for setBmadFolderName method calls in IdeManager

* fix: convert absolute paths to relative in task-tool-command-generator

* fix: support .xml task files in bmad-artifacts task discovery

* fix: skip internal tasks in manifest generation and IDE command discovery

* fix: skip empty artifact_types targets and remove unused vscode_settings target

* fix: skip internal tools in manifest generation and improve Windows path handling in command generator

* fix: use csv-parse library for proper CSV handling in manifest generation

* refactor: extract CSV text cleaning to reusable method in manifest generator

* fix: normalize path separators to forward slashes in agent file copying for cross-platform compatibility

---------

Co-authored-by: Alex Verkhovsky <alexey.verkhovsky@gmail.com>
Co-authored-by: Brian <bmadcode@gmail.com>
2026-02-03 17:36:54 -06:00
Michael Pursifull 594235522c
fix: add process control and building automation domains (#1510)
Adds two operational technology domains to domain-complexity.csv
in both PRD and architecture workflows. Addresses the gap in OT
domain coverage for physical process control and building systems.

process_control: industrial automation, SCADA, PLC, DCS, I&C,
P&ID — covers power/utilities, water treatment, oil & gas,
manufacturing, chemical, pharmaceutical, food & beverage, mining,
and other sectors where software controls physical processes.
Key concerns include functional safety, process safety and hazard
analysis, environmental compliance, OT cybersecurity, and plant
reliability/maintainability. Requires engineering_authority PRD
section for PE/EOR credential requirements.

building_automation: BAS/BMS, HVAC, fire alarm, fire protection,
life safety, elevators, lighting, access control, commissioning —
covers commercial and institutional building systems. Key concerns
include life safety codes, multi-trade coordination, commissioning,
and indoor environmental quality.

Both domains are high complexity, include engineering_authority
as a required PRD section, and follow established entry patterns.

Fixes #1240

Co-authored-by: Brian <bmadcode@gmail.com>
2026-02-03 13:24:33 -06:00
Murat K Ozcan 7ecae1d000
test: quinn to qa (#1508)
* test: quinn to qa

* Removed the TEA sidebar section from the main docs nav

---------

Co-authored-by: Brian <bmadcode@gmail.com>
2026-02-03 13:23:37 -06:00
Michael Pursifull ba890779a2
feat: cross-file reference validator for BMAD source files (#1494)
* feat: add cross-file reference validator for CI

Add tools/validate-file-refs.js that validates cross-file references
in BMAD source files (agents, workflows, tasks, steps). Catches broken
file paths, missing referenced files, wrong extensions, and absolute
path leaks before they reach users.

Addresses broken-file-ref and path-handling bug classes which account
for 25% of all historical bugs (59 closed issues, 129+ comments).

- Scans src/ for YAML, markdown, and XML files
- Validates {project-root}/_bmad/ references against source tree
- Checks relative path references, exec attributes, invoke-task tags
- Detects absolute path leaks (/Users/, /home/, C:\)
- Adds validate:refs npm script and CI step in quality.yaml

* feat: strip JSON example blocks to reduce false-positive broken refs

Add stripJsonExampleBlocks() to the markdown reference extractor so
bare JSON example/template blocks (braces on their own lines) are
removed before pattern matching. This prevents paths inside example
data from being flagged as broken references.

* feat: add line numbers, fix utility/ path mapping, improve verbose output

- Add utility/ to direct path mapping (was incorrectly falling through
  to src/modules/utility/)
- Show line numbers for broken references in markdown files
- Show YAML key path for broken references in YAML files
- Print file headers in verbose mode for all files with refs

* fix: correct verbose [OK]/[BROKEN] overlap and line number drift

Broken refs no longer print [OK] before [BROKEN] in --verbose mode.
Code block stripping now preserves newlines so offsetToLine() reports
accurate line numbers when code blocks precede broken references.

* fix: address review feedback, add CI annotations and step summary

Address alexeyv's review findings on PR #1494:
- Fix exec-attr prefix handling for {_bmad}/ and bare _bmad/ paths
- Fix mapInstalledToSource fallback (remove phantom src/modules/ mapping)
- Switch extractYamlRefs to parseDocument() for YAML line numbers

Add CI integration (stories 2-1, 2-2):
- Emit ::warning annotations for broken refs and abs-path leaks
- Write markdown table to $GITHUB_STEP_SUMMARY
- Guard both behind environment variable checks

Harden CI output:
- escapeAnnotation() encodes %, \r, \n per GitHub Actions spec
- escapeTableCell() escapes pipe chars in step summary table

---------

Co-authored-by: Alex Verkhovsky <alexey.verkhovsky@gmail.com>
Co-authored-by: Brian <bmadcode@gmail.com>
2026-02-03 13:13:38 -06:00
Brian Madison 323cd75efd x post premium example udpate for social post tool - do not add to changelog 2026-02-01 17:32:46 -06:00
Brian Madison b0c35d595f social post skill helper 2026-02-01 16:59:43 -06:00
41 changed files with 1599 additions and 260 deletions

Binary file not shown.

View File

@ -0,0 +1,169 @@
---
name: changelog-social
description: Generate social media announcements for Discord, Twitter, and LinkedIn from the latest changelog entry. Use when user asks to create release announcements, social posts, or share changelog updates. Reads CHANGELOG.md in current working directory. Reference examples/ for tone and format.
disable-model-invocation: true
---
# Changelog Social
Generate engaging social media announcements from changelog entries.
## Workflow
### Step 1: Extract Changelog Entry
Read `./CHANGELOG.md` and extract the latest version entry. The changelog follows this format:
```markdown
## [VERSION]
### 🎁 Features
* **Title** — Description
### 🐛 Bug Fixes
* **Title** — Description
### 📚 Documentation
* **Title** — Description
### 🔧 Maintenance
* **Title** — Description
```
Parse:
- **Version number** (e.g., `6.0.0-Beta.5`)
- **Features** - New functionality, enhancements
- **Bug Fixes** - Fixes users will care about
- **Documentation** - New or improved docs
- **Maintenance** - Dependency updates, tooling improvements
### Step 2: Get Git Contributors
Use git log to find contributors since the previous version. Get commits between the current version tag and the previous one:
```bash
# Find the previous version tag first
git tag --sort=-version:refname | head -5
# Get commits between versions with PR numbers and authors
git log <previous-tag>..<current-tag> --pretty=format:"%h|%s|%an" --grep="#"
```
Extract PR numbers from commit messages that contain `#` followed by digits. Compile unique contributors.
### Step 3: Generate Discord Announcement
**Limit: 2,000 characters per message.** Split into multiple messages if needed.
Use this template style:
```markdown
🚀 **BMad vVERSION RELEASED!**
🎉 [Brief hype sentence]
🪥 **KEY HIGHLIGHT** - [One-line summary]
🎯 **CATEGORY NAME**
• Feature one - brief description
• Feature two - brief description
• Coming soon: Future teaser
🔧 **ANOTHER CATEGORY**
• Fix or feature
• Another item
📚 **DOCS OR OTHER**
• Item
• Item with link
🌟 **COMMUNITY PHILOSOPHY** (optional - include for major releases)
• Everything is FREE - No paywalls
• Knowledge shared, not sold
📊 **STATS**
X commits | Y PRs merged | Z files changed
🙏 **CONTRIBUTORS**
@username1 (X PRs!), @username2 (Y PRs!)
@username3, @username4, username5 + dependabot 🛡️
Community-driven FTW! 🌟
📦 **INSTALL:**
`npx bmad-method@VERSION install`
⭐ **SUPPORT US:**
🌟 GitHub: github.com/bmad-code-org/BMAD-METHOD/
📺 YouTube: youtube.com/@BMadCode
☕ Donate: buymeacoffee.com/bmad
🔥 **Next version tease!**
```
**Content Strategy:**
- Focus on **user impact** - what's better for them?
- Highlight **annoying bugs fixed** that frustrated users
- Show **new capabilities** that enable workflows
- Keep it **punchy** - use emojis and short bullets
- Add **personality** - excitement, humor, gratitude
### Step 4: Generate Twitter Post
**Limit: 25,000 characters per tweet (Premium).** With Premium, use a single comprehensive post matching the Discord style (minus Discord-specific formatting). Aim for 1,500-3,000 characters for better engagement.
**Threads are optional** — only use for truly massive releases where you want multiple engagement points.
See `examples/twitter-example.md` for the single-post Premium format.
## Content Selection Guidelines
**Include:**
- New features that change workflows
- Bug fixes for annoying/blocking issues
- Documentation that helps users
- Performance improvements
- New agents or workflows
- Breaking changes (call out clearly)
**Skip/Minimize:**
- Internal refactoring
- Dependency updates (unless user-facing)
- Test improvements
- Minor style fixes
**Emphasize:**
- "Finally fixed" issues
- "Faster" operations
- "Easier" workflows
- "Now supports" capabilities
## Examples
Reference example posts in `examples/` for tone and formatting guidance:
- **discord-example.md** — Full Discord announcement with emojis, sections, contributor shout-outs
- **twitter-example.md** — Twitter thread format (5 tweets max for major releases)
- **linkedin-example.md** — Professional post for major/minor releases with significant features
**When to use LinkedIn:**
- Major version releases (e.g., v6.0.0 Beta, v7.0.0)
- Minor releases with exceptional new features
- Community milestone announcements
Read the appropriate example file before generating to match the established style and voice.
## Output Format
Present both announcements in clearly labeled sections:
```markdown
## Discord Announcement
[paste Discord content here]
## Twitter Post
[paste Twitter content here]
```
Offer to make adjustments if the user wants different emphasis, tone, or content.

View File

@ -0,0 +1,53 @@
🚀 **BMad v6.0.0-alpha.23 RELEASED!**
🎉 Huge update - almost beta!
🪟 **WINDOWS INSTALLER FIXED** - Menu arrows issue should be fixed! CRLF & ESM problems resolved.
🎯 **PRD WORKFLOWS IMPROVED**
• Validation & Edit workflows added!
• PRD Cohesion check ensures document flows beautifully
• Coming soon: Use of subprocess optimization (context saved!)
• Coming soon: Final format polish step in all workflows - Human consumption OR hyper-optimized LLM condensed initially!
🔧 **WORKFLOW CREATOR & VALIDATOR**
• Subprocess support for advanced optimization
• Path violation checks ensure integrity
• Beyond error checking - offers optimization & flow suggestions!
📚 **NEW DOCS SITE** - docs.bmad-method.org
• Diataxis framework: Tutorials, How-To, Explanations, References
• Current docs still being revised
• Tutorials, blogs & explainers coming soon!
💡 **BRAINSTORMING REVOLUTION**
• 100+ idea goal (quantity-first!)
• Anti-bias protocol (pivot every 10 ideas)
• Chain-of-thought + simulated temperature prompts
• Coming soon: SubProcessing (on-the-fly sub agents)
🌟 **COMMUNITY PHILOSOPHY**
• Everything is FREE - No paywalls, no gated content
• Knowledge shared, not sold
• No premium tiers - full access to our ideas
📊 **27 commits | 217 links converted | 42+ docs created**
🙏 **17 Community PR Authors in this release!**
@lum (6 PRs!), @q00 (3 PRs!), @phil (2 PRs!)
@mike, @alex, @ramiz, @sjennings + dependabot 🛡️
Community-driven FTW! 🌟
📦 **INSTALL ALPHA:**
`npx bmad-method install`
⭐ **SUPPORT US:**
🌟 GitHub: github.com/bmad-code-org/BMAD-METHOD/
📺 YouTube: youtube.com/@BMadCode
🎤 **SPEAKING & MEDIA**
Available for conferences, podcasts, media appearances!
Topics: AI-Native Organizations (Any Industry), BMad Method
DM on Discord for inquiries!
🔥 **V6 Beta is DAYS away!** January 22nd ETA - new features such as xyz and abc bug fixes!

View File

@ -0,0 +1,49 @@
🚀 **Announcing BMad Method v6.0.0 Beta - AI-Native Agile Development Framework**
I'm excited to share that BMad Method, the open-source AI-driven agile development framework, is entering Beta! After 27 alpha releases and countless community contributions, we're approaching a major milestone.
**What's New in v6.0.0-alpha.23**
🪟 **Windows Compatibility Fixed**
We've resolved the installer issues that affected Windows users. The menu arrows problem, CRLF handling, and ESM compatibility are all resolved.
🎯 **Enhanced PRD Workflows**
Our Product Requirements Document workflows now include validation and editing capabilities, with a new cohesion check that ensures your documents flow beautifully. Subprocess optimization is coming soon to save even more context.
🔧 **Workflow Creator & Validator**
New tools for creating and validating workflows with subprocess support, path violation checks, and optimization suggestions that go beyond simple error checking.
📚 **New Documentation Platform**
We've launched docs.bmad-method.org using the Diataxis framework - providing clear separation between tutorials, how-to guides, explanations, and references. Our documentation is being continuously revised and expanded.
💡 **Brainstorming Revolution**
Our brainstorming workflows now use research-backed techniques: 100+ idea goals, anti-bias protocols, chain-of-thought reasoning, and simulated temperature prompts for higher divergence.
**Our Philosophy**
Everything in BMad Method is FREE. No paywalls, no gated content, no premium tiers. We believe knowledge should be shared, not sold. This is community-driven development at its finest.
**The Stats**
- 27 commits in this release
- 217 documentation links converted
- 42+ new documents created
- 17 community PR authors contributed
**Get Started**
```
npx bmad-method@alpha install
```
**Learn More**
- GitHub: github.com/bmad-code-org/BMAD-METHOD
- YouTube: youtube.com/@BMadCode
- Docs: docs.bmad-method.org
**What's Next?**
Beta is just days away with an ETA of January 22nd. We're also available for conferences, podcasts, and media appearances to discuss AI-Native Organizations and the BMad Method.
Have you tried BMad Method yet? I'd love to hear about your experience in the comments!
#AI #SoftwareDevelopment #Agile #OpenSource #DevTools #LLM #AgentEngineering

View File

@ -0,0 +1,55 @@
🚀 **BMad v6.0.0-alpha.23 RELEASED!**
Huge update - we're almost at Beta! 🎉
🪟 **WINDOWS INSTALLER FIXED** - Menu arrows issue should be fixed! CRLF & ESM problems resolved.
🎯 **PRD WORKFLOWS IMPROVED**
• Validation & Edit workflows added!
• PRD Cohesion check ensures document flows beautifully
• Coming soon: Subprocess optimization (context saved!)
• Coming soon: Final format polish step in all workflows
🔧 **WORKFLOW CREATOR & VALIDATOR**
• Subprocess support for advanced optimization
• Path violation checks ensure integrity
• Beyond error checking - offers optimization & flow suggestions!
📚 **NEW DOCS SITE** - docs.bmad-method.org
• Diataxis framework: Tutorials, How-To, Explanations, References
• Current docs still being revised
• Tutorials, blogs & explainers coming soon!
💡 **BRAINSTORMING REVOLUTION**
• 100+ idea goal (quantity-first!)
• Anti-bias protocol (pivot every 10 ideas)
• Chain-of-thought + simulated temperature prompts
• Coming soon: SubProcessing (on-the-fly sub agents)
🌟 **COMMUNITY PHILOSOPHY**
• Everything is FREE - No paywalls, no gated content
• Knowledge shared, not sold
• No premium tiers - full access to our ideas
📊 **27 commits | 217 links converted | 42+ docs created**
🙏 **17 Community PR Authors in this release!**
@lum (6 PRs!), @q00 (3 PRs!), @phil (2 PRs!)
@mike, @alex, @ramiz, @sjennings + dependabot 🛡️
Community-driven FTW! 🌟
📦 **INSTALL ALPHA:**
`npx bmad-method install`
⭐ **SUPPORT US:**
🌟 GitHub: github.com/bmad-code-org/BMAD-METHOD/
📺 YouTube: youtube.com/@BMadCode
🎤 **SPEAKING & MEDIA**
Available for conferences, podcasts, media appearances!
Topics: AI-Native Organizations (Any Industry), BMad Method
DM on Discord for inquiries!
🔥 **V6 Beta is DAYS away!** January 22nd ETA!
#AI #DevTools #Agile #OpenSource #LLM #AgentEngineering

View File

@ -42,9 +42,25 @@ Publish the package.
Create release with changelog notes using `gh release create`. Create release with changelog notes using `gh release create`.
### Step 10: Confirm Completion ### Step 10: Create Social Announcement
Show npm and GitHub links. Create a social media announcement file at `_bmad-output/social/{repo-name}-release.md`.
Format:
```markdown
# {name} v{version} Released
## Highlights
{2-3 bullet points of key features/changes}
## Links
- GitHub: {release-url}
- npm: {npm-url}
```
### Step 11: Confirm Completion
Show npm, GitHub, and social announcement file paths.
## Error Handling ## Error Handling

View File

@ -113,3 +113,6 @@ jobs:
- name: Test agent compilation components - name: Test agent compilation components
run: npm run test:install run: npm run test:install
- name: Validate file references
run: npm run validate:refs

View File

@ -58,7 +58,7 @@ Build it, one story at a time.
| `correct-course` | Handle significant mid-sprint changes | Updated plan or re-routing | | `correct-course` | Handle significant mid-sprint changes | Updated plan or re-routing |
| `retrospective` | Review after epic completion | Lessons learned | | `retrospective` | Review after epic completion | Lessons learned |
**Quinn (QA Agent):** Built-in QA agent for test automation. Trigger with `QA` or `bmad-bmm-automate`. Generates standard API and E2E tests using your project's test framework. Beginner-friendly, no configuration needed. For advanced test strategy, install [Test Architect (TEA)](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/) module. **Quinn (QA Agent):** Built-in QA agent for test automation. Trigger with `QA` or `bmad-bmm-qa-automate`. Generates standard API and E2E tests using your project's test framework. Beginner-friendly, no configuration needed. For advanced test strategy, install [Test Architect (TEA)](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/) module.
## Quick Flow (Parallel Track) ## Quick Flow (Parallel Track)

18
package-lock.json generated
View File

@ -9,7 +9,8 @@
"version": "6.0.0-Beta.5", "version": "6.0.0-Beta.5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@clack/prompts": "^0.11.0", "@clack/core": "^1.0.0",
"@clack/prompts": "^1.0.0",
"@kayvan/markdown-tree-parser": "^1.6.1", "@kayvan/markdown-tree-parser": "^1.6.1",
"boxen": "^5.1.2", "boxen": "^5.1.2",
"chalk": "^4.1.2", "chalk": "^4.1.2",
@ -22,6 +23,7 @@
"ignore": "^7.0.5", "ignore": "^7.0.5",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"ora": "^5.4.1", "ora": "^5.4.1",
"picocolors": "^1.1.1",
"semver": "^7.6.3", "semver": "^7.6.3",
"wrap-ansi": "^7.0.0", "wrap-ansi": "^7.0.0",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
@ -756,9 +758,9 @@
} }
}, },
"node_modules/@clack/core": { "node_modules/@clack/core": {
"version": "0.5.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz", "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.0.tgz",
"integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==", "integrity": "sha512-Orf9Ltr5NeiEuVJS8Rk2XTw3IxNC2Bic3ash7GgYeA8LJ/zmSNpSQ/m5UAhe03lA6KFgklzZ5KTHs4OAMA/SAQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
@ -766,12 +768,12 @@
} }
}, },
"node_modules/@clack/prompts": { "node_modules/@clack/prompts": {
"version": "0.11.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.0.0.tgz",
"integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==", "integrity": "sha512-rWPXg9UaCFqErJVQ+MecOaWsozjaxol4yjnmYcGNipAWzdaWa2x+VJmKfGq7L0APwBohQOYdHC+9RO4qRXej+A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@clack/core": "0.5.0", "@clack/core": "1.0.0",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"sisteransi": "^1.0.5" "sisteransi": "^1.0.5"
} }

View File

@ -49,6 +49,7 @@
"test:coverage": "c8 --reporter=text --reporter=html npm run test:schemas", "test:coverage": "c8 --reporter=text --reporter=html npm run test:schemas",
"test:install": "node test/test-installation-components.js", "test:install": "node test/test-installation-components.js",
"test:schemas": "node test/test-agent-schema.js", "test:schemas": "node test/test-agent-schema.js",
"validate:refs": "node tools/validate-file-refs.js",
"validate:schemas": "node tools/validate-agent-schema.js" "validate:schemas": "node tools/validate-agent-schema.js"
}, },
"lint-staged": { "lint-staged": {
@ -68,7 +69,8 @@
] ]
}, },
"dependencies": { "dependencies": {
"@clack/prompts": "^0.11.0", "@clack/core": "^1.0.0",
"@clack/prompts": "^1.0.0",
"@kayvan/markdown-tree-parser": "^1.6.1", "@kayvan/markdown-tree-parser": "^1.6.1",
"boxen": "^5.1.2", "boxen": "^5.1.2",
"chalk": "^4.1.2", "chalk": "^4.1.2",
@ -81,6 +83,7 @@
"ignore": "^7.0.5", "ignore": "^7.0.5",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"ora": "^5.4.1", "ora": "^5.4.1",
"picocolors": "^1.1.1",
"semver": "^7.6.3", "semver": "^7.6.3",
"wrap-ansi": "^7.0.0", "wrap-ansi": "^7.0.0",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",

View File

@ -1,6 +1,6 @@
agent: agent:
metadata: metadata:
id: "_bmad/bmm/agents/quinn" id: "_bmad/bmm/agents/qa"
name: Quinn name: Quinn
title: QA Engineer title: QA Engineer
icon: 🧪 icon: 🧪
@ -54,4 +54,4 @@ agent:
For comprehensive test strategy, risk-based planning, quality gates, and enterprise features, For comprehensive test strategy, risk-based planning, quality gates, and enterprise features,
install the Test Architect (TEA) module: https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/ install the Test Architect (TEA) module: https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/
Ready to generate some tests? Just say `QA` or `bmad-bmm-automate`! Ready to generate some tests? Just say `QA` or `bmad-bmm-qa-automate`!

View File

@ -34,5 +34,5 @@ bmm,4-implementation,Validate Story,VS,35,_bmad/bmm/workflows/4-implementation/c
bmm,4-implementation,Create Story,CS,30,_bmad/bmm/workflows/4-implementation/create-story/workflow.yaml,bmad-bmm-create-story,true,sm,Create Mode,"Story cycle start: Prepare first found story in the sprint plan that is next, or if the command is run with a specific epic and story designation with context. Once complete, then VS then DS then CR then back to DS if needed or next CS or ER",implementation_artifacts,story, bmm,4-implementation,Create Story,CS,30,_bmad/bmm/workflows/4-implementation/create-story/workflow.yaml,bmad-bmm-create-story,true,sm,Create Mode,"Story cycle start: Prepare first found story in the sprint plan that is next, or if the command is run with a specific epic and story designation with context. Once complete, then VS then DS then CR then back to DS if needed or next CS or ER",implementation_artifacts,story,
bmm,4-implementation,Dev Story,DS,40,_bmad/bmm/workflows/4-implementation/dev-story/workflow.yaml,bmad-bmm-dev-story,true,dev,Create Mode,"Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed",,, bmm,4-implementation,Dev Story,DS,40,_bmad/bmm/workflows/4-implementation/dev-story/workflow.yaml,bmad-bmm-dev-story,true,dev,Create Mode,"Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed",,,
bmm,4-implementation,Code Review,CR,50,_bmad/bmm/workflows/4-implementation/code-review/workflow.yaml,bmad-bmm-code-review,false,dev,Create Mode,"Story cycle: If issues back to DS if approved then next CS or ER if epic complete",,, bmm,4-implementation,Code Review,CR,50,_bmad/bmm/workflows/4-implementation/code-review/workflow.yaml,bmad-bmm-code-review,false,dev,Create Mode,"Story cycle: If issues back to DS if approved then next CS or ER if epic complete",,,
bmm,4-implementation,QA Automation Test,QA,45,_bmad/bmm/workflows/qa/automate/workflow.yaml,bmad-bmm-qa-automate,false,quinn,Create Mode,"Generate automated API and E2E tests for implemented code using the project's existing test framework (detects existing well known in use test frameworks). Use after implementation to add test coverage. NOT for code review or story validation - use CR for that.",implementation_artifacts,"test suite", bmm,4-implementation,QA Automation Test,QA,45,_bmad/bmm/workflows/qa/automate/workflow.yaml,bmad-bmm-qa-automate,false,qa,Create Mode,"Generate automated API and E2E tests for implemented code using the project's existing test framework (detects existing well known in use test frameworks). Use after implementation to add test coverage. NOT for code review or story validation - use CR for that.",implementation_artifacts,"test suite",
bmm,4-implementation,Retrospective,ER,60,_bmad/bmm/workflows/4-implementation/retrospective/workflow.yaml,bmad-bmm-retrospective,false,sm,Create Mode,"Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC",implementation_artifacts,retrospective, bmm,4-implementation,Retrospective,ER,60,_bmad/bmm/workflows/4-implementation/retrospective/workflow.yaml,bmad-bmm-retrospective,false,sm,Create Mode,"Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC",implementation_artifacts,retrospective,

1 module phase name code sequence workflow-file command required agent options description output-location outputs
34 bmm 4-implementation Create Story CS 30 _bmad/bmm/workflows/4-implementation/create-story/workflow.yaml bmad-bmm-create-story true sm Create Mode Story cycle start: Prepare first found story in the sprint plan that is next, or if the command is run with a specific epic and story designation with context. Once complete, then VS then DS then CR then back to DS if needed or next CS or ER implementation_artifacts story
35 bmm 4-implementation Dev Story DS 40 _bmad/bmm/workflows/4-implementation/dev-story/workflow.yaml bmad-bmm-dev-story true dev Create Mode Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed
36 bmm 4-implementation Code Review CR 50 _bmad/bmm/workflows/4-implementation/code-review/workflow.yaml bmad-bmm-code-review false dev Create Mode Story cycle: If issues back to DS if approved then next CS or ER if epic complete
37 bmm 4-implementation QA Automation Test QA 45 _bmad/bmm/workflows/qa/automate/workflow.yaml bmad-bmm-qa-automate false quinn qa Create Mode Generate automated API and E2E tests for implemented code using the project's existing test framework (detects existing well known in use test frameworks). Use after implementation to add test coverage. NOT for code review or story validation - use CR for that. implementation_artifacts test suite
38 bmm 4-implementation Retrospective ER 60 _bmad/bmm/workflows/4-implementation/retrospective/workflow.yaml bmad-bmm-retrospective false sm Create Mode Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC implementation_artifacts retrospective

View File

@ -9,5 +9,7 @@ scientific,"research,algorithm,simulation,modeling,computational,analysis,data s
legaltech,"legal,law,contract,compliance,litigation,patent,attorney,court",high,"Legal ethics;Bar regulations;Data retention;Attorney-client privilege;Court system integration","Legal practice rules;Ethics requirements;Court filing systems;Document standards;Confidentiality","domain-research","legal technology ethics {date};law practice management software requirements;court filing system standards;attorney client privilege technology","ethics_compliance;data_retention;confidentiality_measures;court_integration" legaltech,"legal,law,contract,compliance,litigation,patent,attorney,court",high,"Legal ethics;Bar regulations;Data retention;Attorney-client privilege;Court system integration","Legal practice rules;Ethics requirements;Court filing systems;Document standards;Confidentiality","domain-research","legal technology ethics {date};law practice management software requirements;court filing system standards;attorney client privilege technology","ethics_compliance;data_retention;confidentiality_measures;court_integration"
insuretech,"insurance,claims,underwriting,actuarial,policy,risk,premium",high,"Insurance regulations;Actuarial standards;Data privacy;Fraud detection;State compliance","Insurance regulations by state;Actuarial methods;Risk modeling;Claims processing;Regulatory reporting","domain-research","insurance software regulations {date};actuarial standards software;insurance fraud detection;state insurance compliance","regulatory_requirements;risk_modeling;fraud_detection;reporting_compliance" insuretech,"insurance,claims,underwriting,actuarial,policy,risk,premium",high,"Insurance regulations;Actuarial standards;Data privacy;Fraud detection;State compliance","Insurance regulations by state;Actuarial methods;Risk modeling;Claims processing;Regulatory reporting","domain-research","insurance software regulations {date};actuarial standards software;insurance fraud detection;state insurance compliance","regulatory_requirements;risk_modeling;fraud_detection;reporting_compliance"
energy,"energy,utility,grid,solar,wind,power,electricity,oil,gas",high,"Grid compliance;NERC standards;Environmental regulations;Safety requirements;Real-time operations","Energy regulations;Grid standards;Environmental compliance;Safety protocols;SCADA systems","domain-research","energy sector software compliance {date};NERC CIP standards;smart grid requirements;renewable energy software standards","grid_compliance;safety_protocols;environmental_compliance;operational_requirements" energy,"energy,utility,grid,solar,wind,power,electricity,oil,gas",high,"Grid compliance;NERC standards;Environmental regulations;Safety requirements;Real-time operations","Energy regulations;Grid standards;Environmental compliance;Safety protocols;SCADA systems","domain-research","energy sector software compliance {date};NERC CIP standards;smart grid requirements;renewable energy software standards","grid_compliance;safety_protocols;environmental_compliance;operational_requirements"
process_control,"industrial automation,process control,PLC,SCADA,DCS,HMI,operational technology,OT,control system,cyberphysical,MES,historian,instrumentation,I&C,P&ID",high,"Functional safety;OT cybersecurity;Real-time control requirements;Legacy system integration;Process safety and hazard analysis;Environmental compliance and permitting;Engineering authority and PE requirements","Functional safety standards;OT security frameworks;Industrial protocols;Process control architecture;Plant reliability and maintainability","domain-research + technical-model","IEC 62443 OT cybersecurity requirements {date};functional safety software requirements {date};industrial process control architecture;ISA-95 manufacturing integration","functional_safety;ot_security;process_requirements;engineering_authority"
building_automation,"building automation,BAS,BMS,HVAC,smart building,lighting control,fire alarm,fire protection,fire suppression,life safety,elevator,access control,DDC,energy management,sequence of operations,commissioning",high,"Life safety codes;Building energy standards;Multi-trade coordination and interoperability;Commissioning and ongoing operational performance;Indoor environmental quality and occupant comfort;Engineering authority and PE requirements","Building automation protocols;HVAC and mechanical controls;Fire alarm, fire protection, and life safety design;Commissioning process and sequence of operations;Building codes and energy standards","domain-research","smart building software architecture {date};BACnet integration best practices;building automation cybersecurity {date};ASHRAE building standards","life_safety;energy_compliance;commissioning_requirements;engineering_authority"
gaming,"game,player,gameplay,level,character,multiplayer,quest",redirect,"REDIRECT TO GAME WORKFLOWS","Game design","game-brief","NA","NA" gaming,"game,player,gameplay,level,character,multiplayer,quest",redirect,"REDIRECT TO GAME WORKFLOWS","Game design","game-brief","NA","NA"
general,"",low,"Standard requirements;Basic security;User experience;Performance","General software practices","continue","software development best practices {date}","standard_requirements" general,"",low,"Standard requirements;Basic security;User experience;Performance","General software practices","continue","software development best practices {date}","standard_requirements"
1 domain signals complexity key_concerns required_knowledge suggested_workflow web_searches special_sections
9 legaltech legal,law,contract,compliance,litigation,patent,attorney,court high Legal ethics;Bar regulations;Data retention;Attorney-client privilege;Court system integration Legal practice rules;Ethics requirements;Court filing systems;Document standards;Confidentiality domain-research legal technology ethics {date};law practice management software requirements;court filing system standards;attorney client privilege technology ethics_compliance;data_retention;confidentiality_measures;court_integration
10 insuretech insurance,claims,underwriting,actuarial,policy,risk,premium high Insurance regulations;Actuarial standards;Data privacy;Fraud detection;State compliance Insurance regulations by state;Actuarial methods;Risk modeling;Claims processing;Regulatory reporting domain-research insurance software regulations {date};actuarial standards software;insurance fraud detection;state insurance compliance regulatory_requirements;risk_modeling;fraud_detection;reporting_compliance
11 energy energy,utility,grid,solar,wind,power,electricity,oil,gas high Grid compliance;NERC standards;Environmental regulations;Safety requirements;Real-time operations Energy regulations;Grid standards;Environmental compliance;Safety protocols;SCADA systems domain-research energy sector software compliance {date};NERC CIP standards;smart grid requirements;renewable energy software standards grid_compliance;safety_protocols;environmental_compliance;operational_requirements
12 process_control industrial automation,process control,PLC,SCADA,DCS,HMI,operational technology,OT,control system,cyberphysical,MES,historian,instrumentation,I&C,P&ID high Functional safety;OT cybersecurity;Real-time control requirements;Legacy system integration;Process safety and hazard analysis;Environmental compliance and permitting;Engineering authority and PE requirements Functional safety standards;OT security frameworks;Industrial protocols;Process control architecture;Plant reliability and maintainability domain-research + technical-model IEC 62443 OT cybersecurity requirements {date};functional safety software requirements {date};industrial process control architecture;ISA-95 manufacturing integration functional_safety;ot_security;process_requirements;engineering_authority
13 building_automation building automation,BAS,BMS,HVAC,smart building,lighting control,fire alarm,fire protection,fire suppression,life safety,elevator,access control,DDC,energy management,sequence of operations,commissioning high Life safety codes;Building energy standards;Multi-trade coordination and interoperability;Commissioning and ongoing operational performance;Indoor environmental quality and occupant comfort;Engineering authority and PE requirements Building automation protocols;HVAC and mechanical controls;Fire alarm, fire protection, and life safety design;Commissioning process and sequence of operations;Building codes and energy standards domain-research smart building software architecture {date};BACnet integration best practices;building automation cybersecurity {date};ASHRAE building standards life_safety;energy_compliance;commissioning_requirements;engineering_authority
14 gaming game,player,gameplay,level,character,multiplayer,quest redirect REDIRECT TO GAME WORKFLOWS Game design game-brief NA NA
15 general low Standard requirements;Basic security;User experience;Performance General software practices continue software development best practices {date} standard_requirements

View File

@ -8,4 +8,6 @@ productivity,"productivity,workflow,tasks,management,business,tools",medium,stan
media,"content,media,video,audio,streaming,broadcast",high,advanced,"CDN architecture, video encoding, streaming protocols, content delivery" media,"content,media,video,audio,streaming,broadcast",high,advanced,"CDN architecture, video encoding, streaming protocols, content delivery"
iot,"IoT,sensors,devices,embedded,smart,connected",high,advanced,"device communication, real-time data processing, edge computing, security" iot,"IoT,sensors,devices,embedded,smart,connected",high,advanced,"device communication, real-time data processing, edge computing, security"
government,"government,civic,public,admin,policy,regulation",high,enhanced,"accessibility standards, security clearance, data privacy, audit trails" government,"government,civic,public,admin,policy,regulation",high,enhanced,"accessibility standards, security clearance, data privacy, audit trails"
process_control,"industrial automation,process control,PLC,SCADA,DCS,HMI,operational technology,control system,cyberphysical,MES,instrumentation,I&C,P&ID",high,advanced,"industrial process control architecture, SCADA system design, OT cybersecurity architecture, real-time control systems"
building_automation,"building automation,BAS,BMS,HVAC,smart building,fire alarm,fire protection,fire suppression,life safety,elevator,DDC,access control,sequence of operations,commissioning",high,advanced,"building automation architecture, BACnet integration patterns, smart building design, building management system security"
gaming,"game,gaming,multiplayer,real-time,interactive,entertainment",high,advanced,"real-time multiplayer, game engine architecture, matchmaking, leaderboards" gaming,"game,gaming,multiplayer,real-time,interactive,entertainment",high,advanced,"real-time multiplayer, game engine architecture, matchmaking, leaderboards"
1 domain signals complexity_level suggested_workflow web_searches
8 media content,media,video,audio,streaming,broadcast high advanced CDN architecture, video encoding, streaming protocols, content delivery
9 iot IoT,sensors,devices,embedded,smart,connected high advanced device communication, real-time data processing, edge computing, security
10 government government,civic,public,admin,policy,regulation high enhanced accessibility standards, security clearance, data privacy, audit trails
11 process_control industrial automation,process control,PLC,SCADA,DCS,HMI,operational technology,control system,cyberphysical,MES,instrumentation,I&C,P&ID high advanced industrial process control architecture, SCADA system design, OT cybersecurity architecture, real-time control systems
12 building_automation building automation,BAS,BMS,HVAC,smart building,fire alarm,fire protection,fire suppression,life safety,elevator,DDC,access control,sequence of operations,commissioning high advanced building automation architecture, BACnet integration patterns, smart building design, building management system security
13 gaming game,gaming,multiplayer,real-time,interactive,entertainment high advanced real-time multiplayer, game engine architecture, matchmaking, leaderboards

View File

@ -1,4 +1,4 @@
<task id="_bmad/core/tasks/workflow.xml" name="Execute Workflow" standalone="false"> <task id="_bmad/core/tasks/workflow.xml" name="Execute Workflow" standalone="false" internal="true">
<objective>Execute given workflow by loading its configuration, following instructions, and producing output</objective> <objective>Execute given workflow by loading its configuration, following instructions, and producing output</objective>
<llm critical="true"> <llm critical="true">

View File

@ -164,7 +164,7 @@ async function runTests() {
try { try {
const builder = new YamlXmlBuilder(); const builder = new YamlXmlBuilder();
const qaAgentPath = path.join(projectRoot, 'src/bmm/agents/quinn.agent.yaml'); const qaAgentPath = path.join(projectRoot, 'src/bmm/agents/qa.agent.yaml');
const tempOutput = path.join(__dirname, 'temp-qa-agent.md'); const tempOutput = path.join(__dirname, 'temp-qa-agent.md');
try { try {

View File

@ -586,7 +586,11 @@ class ConfigCollector {
console.log(); console.log();
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName)); console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
let customize = true; let customize = true;
if (moduleName !== 'core') { if (moduleName === 'core') {
// Core module: no confirm prompt, so add spacing manually to match visual style
console.log(chalk.gray('│'));
} else {
// Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing)
const customizeAnswer = await prompts.prompt([ const customizeAnswer = await prompts.prompt([
{ {
type: 'confirm', type: 'confirm',

View File

@ -146,7 +146,7 @@ class DependencyResolver {
const content = await fs.readFile(file.path, 'utf8'); const content = await fs.readFile(file.path, 'utf8');
// Parse YAML frontmatter for explicit dependencies // Parse YAML frontmatter for explicit dependencies
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (frontmatterMatch) { if (frontmatterMatch) {
try { try {
// Pre-process to handle backticks in YAML values // Pre-process to handle backticks in YAML values

View File

@ -17,9 +17,7 @@ const { ManifestGenerator } = require('./manifest-generator');
const { IdeConfigManager } = require('./ide-config-manager'); const { IdeConfigManager } = require('./ide-config-manager');
const { CustomHandler } = require('../custom/handler'); const { CustomHandler } = require('../custom/handler');
const prompts = require('../../../lib/prompts'); const prompts = require('../../../lib/prompts');
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
// BMAD installation folder name - this is constant and should never change
const BMAD_FOLDER_NAME = '_bmad';
class Installer { class Installer {
constructor() { constructor() {
@ -697,9 +695,6 @@ class Installer {
config.skipIde = toolSelection.skipIde; config.skipIde = toolSelection.skipIde;
const ideConfigurations = toolSelection.configurations; const ideConfigurations = toolSelection.configurations;
// Add spacing after prompts before installation progress
console.log('');
if (spinner.isSpinning) { if (spinner.isSpinning) {
spinner.text = 'Continuing installation...'; spinner.text = 'Continuing installation...';
} else { } else {

View File

@ -2,6 +2,7 @@ const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const yaml = require('yaml'); const yaml = require('yaml');
const crypto = require('node:crypto'); const crypto = require('node:crypto');
const csv = require('csv-parse/sync');
const { getSourcePath, getModulePath } = require('../../../lib/project-root'); const { getSourcePath, getModulePath } = require('../../../lib/project-root');
// Load package.json for version info // Load package.json for version info
@ -21,6 +22,19 @@ class ManifestGenerator {
this.selectedIdes = []; this.selectedIdes = [];
} }
/**
* Clean text for CSV output by normalizing whitespace and escaping quotes
* @param {string} text - Text to clean
* @returns {string} Cleaned text safe for CSV
*/
cleanForCSV(text) {
if (!text) return '';
return text
.trim()
.replaceAll(/\s+/g, ' ') // Normalize all whitespace (including newlines) to single space
.replaceAll('"', '""'); // Escape quotes for CSV
}
/** /**
* Generate all manifests for the installation * Generate all manifests for the installation
* @param {string} bmadDir - _bmad * @param {string} bmadDir - _bmad
@ -161,7 +175,7 @@ class ManifestGenerator {
workflow = yaml.parse(content); workflow = yaml.parse(content);
} else { } else {
// Parse MD workflow with YAML frontmatter // Parse MD workflow with YAML frontmatter
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!frontmatterMatch) { if (!frontmatterMatch) {
if (debug) { if (debug) {
console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`); console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`);
@ -201,7 +215,7 @@ class ManifestGenerator {
// Workflows with standalone: false are filtered out above // Workflows with standalone: false are filtered out above
workflows.push({ workflows.push({
name: workflow.name, name: workflow.name,
description: workflow.description.replaceAll('"', '""'), // Escape quotes for CSV description: this.cleanForCSV(workflow.description),
module: moduleName, module: moduleName,
path: installPath, path: installPath,
}); });
@ -319,24 +333,15 @@ class ManifestGenerator {
const agentName = entry.name.replace('.md', ''); const agentName = entry.name.replace('.md', '');
// Helper function to clean and escape CSV content
const cleanForCSV = (text) => {
if (!text) return '';
return text
.trim()
.replaceAll(/\s+/g, ' ') // Normalize whitespace
.replaceAll('"', '""'); // Escape quotes for CSV
};
agents.push({ agents.push({
name: agentName, name: agentName,
displayName: nameMatch ? nameMatch[1] : agentName, displayName: nameMatch ? nameMatch[1] : agentName,
title: titleMatch ? titleMatch[1] : '', title: titleMatch ? titleMatch[1] : '',
icon: iconMatch ? iconMatch[1] : '', icon: iconMatch ? iconMatch[1] : '',
role: roleMatch ? cleanForCSV(roleMatch[1]) : '', role: roleMatch ? this.cleanForCSV(roleMatch[1]) : '',
identity: identityMatch ? cleanForCSV(identityMatch[1]) : '', identity: identityMatch ? this.cleanForCSV(identityMatch[1]) : '',
communicationStyle: styleMatch ? cleanForCSV(styleMatch[1]) : '', communicationStyle: styleMatch ? this.cleanForCSV(styleMatch[1]) : '',
principles: principlesMatch ? cleanForCSV(principlesMatch[1]) : '', principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '',
module: moduleName, module: moduleName,
path: installPath, path: installPath,
}); });
@ -385,6 +390,11 @@ class ManifestGenerator {
const filePath = path.join(dirPath, file); const filePath = path.join(dirPath, file);
const content = await fs.readFile(filePath, 'utf8'); const content = await fs.readFile(filePath, 'utf8');
// Skip internal/engine files (not user-facing tasks)
if (content.includes('internal="true"')) {
continue;
}
let name = file.replace(/\.(xml|md)$/, ''); let name = file.replace(/\.(xml|md)$/, '');
let displayName = name; let displayName = name;
let description = ''; let description = '';
@ -392,13 +402,13 @@ class ManifestGenerator {
if (file.endsWith('.md')) { if (file.endsWith('.md')) {
// Parse YAML frontmatter for .md tasks // Parse YAML frontmatter for .md tasks
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (frontmatterMatch) { if (frontmatterMatch) {
try { try {
const frontmatter = yaml.parse(frontmatterMatch[1]); const frontmatter = yaml.parse(frontmatterMatch[1]);
name = frontmatter.name || name; name = frontmatter.name || name;
displayName = frontmatter.displayName || frontmatter.name || name; displayName = frontmatter.displayName || frontmatter.name || name;
description = frontmatter.description || ''; description = this.cleanForCSV(frontmatter.description || '');
standalone = frontmatter.standalone === true || frontmatter.standalone === 'true'; standalone = frontmatter.standalone === true || frontmatter.standalone === 'true';
} catch { } catch {
// If YAML parsing fails, use defaults // If YAML parsing fails, use defaults
@ -411,7 +421,7 @@ class ManifestGenerator {
const descMatch = content.match(/description="([^"]+)"/); const descMatch = content.match(/description="([^"]+)"/);
const objMatch = content.match(/<objective>([^<]+)<\/objective>/); const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''; description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '');
const standaloneMatch = content.match(/<task[^>]+standalone="true"/); const standaloneMatch = content.match(/<task[^>]+standalone="true"/);
standalone = !!standaloneMatch; standalone = !!standaloneMatch;
@ -424,7 +434,7 @@ class ManifestGenerator {
tasks.push({ tasks.push({
name: name, name: name,
displayName: displayName, displayName: displayName,
description: description.replaceAll('"', '""'), description: description,
module: moduleName, module: moduleName,
path: installPath, path: installPath,
standalone: standalone, standalone: standalone,
@ -474,6 +484,11 @@ class ManifestGenerator {
const filePath = path.join(dirPath, file); const filePath = path.join(dirPath, file);
const content = await fs.readFile(filePath, 'utf8'); const content = await fs.readFile(filePath, 'utf8');
// Skip internal tools (same as tasks)
if (content.includes('internal="true"')) {
continue;
}
let name = file.replace(/\.(xml|md)$/, ''); let name = file.replace(/\.(xml|md)$/, '');
let displayName = name; let displayName = name;
let description = ''; let description = '';
@ -481,13 +496,13 @@ class ManifestGenerator {
if (file.endsWith('.md')) { if (file.endsWith('.md')) {
// Parse YAML frontmatter for .md tools // Parse YAML frontmatter for .md tools
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (frontmatterMatch) { if (frontmatterMatch) {
try { try {
const frontmatter = yaml.parse(frontmatterMatch[1]); const frontmatter = yaml.parse(frontmatterMatch[1]);
name = frontmatter.name || name; name = frontmatter.name || name;
displayName = frontmatter.displayName || frontmatter.name || name; displayName = frontmatter.displayName || frontmatter.name || name;
description = frontmatter.description || ''; description = this.cleanForCSV(frontmatter.description || '');
standalone = frontmatter.standalone === true || frontmatter.standalone === 'true'; standalone = frontmatter.standalone === true || frontmatter.standalone === 'true';
} catch { } catch {
// If YAML parsing fails, use defaults // If YAML parsing fails, use defaults
@ -500,7 +515,7 @@ class ManifestGenerator {
const descMatch = content.match(/description="([^"]+)"/); const descMatch = content.match(/description="([^"]+)"/);
const objMatch = content.match(/<objective>([^<]+)<\/objective>/); const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''; description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '');
const standaloneMatch = content.match(/<tool[^>]+standalone="true"/); const standaloneMatch = content.match(/<tool[^>]+standalone="true"/);
standalone = !!standaloneMatch; standalone = !!standaloneMatch;
@ -513,7 +528,7 @@ class ManifestGenerator {
tools.push({ tools.push({
name: name, name: name,
displayName: displayName, displayName: displayName,
description: description.replaceAll('"', '""'), description: description,
module: moduleName, module: moduleName,
path: installPath, path: installPath,
standalone: standalone, standalone: standalone,
@ -773,30 +788,23 @@ class ManifestGenerator {
*/ */
async writeAgentManifest(cfgDir) { async writeAgentManifest(cfgDir) {
const csvPath = path.join(cfgDir, 'agent-manifest.csv'); const csvPath = path.join(cfgDir, 'agent-manifest.csv');
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
// Read existing manifest to preserve entries // Read existing manifest to preserve entries
const existingEntries = new Map(); const existingEntries = new Map();
if (await fs.pathExists(csvPath)) { if (await fs.pathExists(csvPath)) {
const content = await fs.readFile(csvPath, 'utf8'); const content = await fs.readFile(csvPath, 'utf8');
const lines = content.split('\n').filter((line) => line.trim()); const records = csv.parse(content, {
columns: true,
// Skip header skip_empty_lines: true,
for (let i = 1; i < lines.length; i++) { });
const line = lines[i]; for (const record of records) {
if (line) { existingEntries.set(`${record.module}:${record.name}`, record);
// Parse CSV (simple parsing assuming no commas in quoted fields)
const parts = line.split('","');
if (parts.length >= 11) {
const name = parts[0].replace(/^"/, '');
const module = parts[8];
existingEntries.set(`${module}:${name}`, line);
}
}
} }
} }
// Create CSV header with persona fields // Create CSV header with persona fields
let csv = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n'; let csvContent = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n';
// Combine existing and new agents, preferring new data for duplicates // Combine existing and new agents, preferring new data for duplicates
const allAgents = new Map(); const allAgents = new Map();
@ -809,18 +817,38 @@ class ManifestGenerator {
// Add/update new agents // Add/update new agents
for (const agent of this.agents) { for (const agent of this.agents) {
const key = `${agent.module}:${agent.name}`; const key = `${agent.module}:${agent.name}`;
allAgents.set( allAgents.set(key, {
key, name: agent.name,
`"${agent.name}","${agent.displayName}","${agent.title}","${agent.icon}","${agent.role}","${agent.identity}","${agent.communicationStyle}","${agent.principles}","${agent.module}","${agent.path}"`, displayName: agent.displayName,
); title: agent.title,
icon: agent.icon,
role: agent.role,
identity: agent.identity,
communicationStyle: agent.communicationStyle,
principles: agent.principles,
module: agent.module,
path: agent.path,
});
} }
// Write all agents // Write all agents
for (const [, value] of allAgents) { for (const [, record] of allAgents) {
csv += value + '\n'; const row = [
escapeCsv(record.name),
escapeCsv(record.displayName),
escapeCsv(record.title),
escapeCsv(record.icon),
escapeCsv(record.role),
escapeCsv(record.identity),
escapeCsv(record.communicationStyle),
escapeCsv(record.principles),
escapeCsv(record.module),
escapeCsv(record.path),
].join(',');
csvContent += row + '\n';
} }
await fs.writeFile(csvPath, csv); await fs.writeFile(csvPath, csvContent);
return csvPath; return csvPath;
} }
@ -830,30 +858,23 @@ class ManifestGenerator {
*/ */
async writeTaskManifest(cfgDir) { async writeTaskManifest(cfgDir) {
const csvPath = path.join(cfgDir, 'task-manifest.csv'); const csvPath = path.join(cfgDir, 'task-manifest.csv');
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
// Read existing manifest to preserve entries // Read existing manifest to preserve entries
const existingEntries = new Map(); const existingEntries = new Map();
if (await fs.pathExists(csvPath)) { if (await fs.pathExists(csvPath)) {
const content = await fs.readFile(csvPath, 'utf8'); const content = await fs.readFile(csvPath, 'utf8');
const lines = content.split('\n').filter((line) => line.trim()); const records = csv.parse(content, {
columns: true,
// Skip header skip_empty_lines: true,
for (let i = 1; i < lines.length; i++) { });
const line = lines[i]; for (const record of records) {
if (line) { existingEntries.set(`${record.module}:${record.name}`, record);
// Parse CSV (simple parsing assuming no commas in quoted fields)
const parts = line.split('","');
if (parts.length >= 6) {
const name = parts[0].replace(/^"/, '');
const module = parts[3];
existingEntries.set(`${module}:${name}`, line);
}
}
} }
} }
// Create CSV header with standalone column // Create CSV header with standalone column
let csv = 'name,displayName,description,module,path,standalone\n'; let csvContent = 'name,displayName,description,module,path,standalone\n';
// Combine existing and new tasks // Combine existing and new tasks
const allTasks = new Map(); const allTasks = new Map();
@ -866,15 +887,30 @@ class ManifestGenerator {
// Add/update new tasks // Add/update new tasks
for (const task of this.tasks) { for (const task of this.tasks) {
const key = `${task.module}:${task.name}`; const key = `${task.module}:${task.name}`;
allTasks.set(key, `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}","${task.standalone}"`); allTasks.set(key, {
name: task.name,
displayName: task.displayName,
description: task.description,
module: task.module,
path: task.path,
standalone: task.standalone,
});
} }
// Write all tasks // Write all tasks
for (const [, value] of allTasks) { for (const [, record] of allTasks) {
csv += value + '\n'; const row = [
escapeCsv(record.name),
escapeCsv(record.displayName),
escapeCsv(record.description),
escapeCsv(record.module),
escapeCsv(record.path),
escapeCsv(record.standalone),
].join(',');
csvContent += row + '\n';
} }
await fs.writeFile(csvPath, csv); await fs.writeFile(csvPath, csvContent);
return csvPath; return csvPath;
} }
@ -884,30 +920,23 @@ class ManifestGenerator {
*/ */
async writeToolManifest(cfgDir) { async writeToolManifest(cfgDir) {
const csvPath = path.join(cfgDir, 'tool-manifest.csv'); const csvPath = path.join(cfgDir, 'tool-manifest.csv');
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
// Read existing manifest to preserve entries // Read existing manifest to preserve entries
const existingEntries = new Map(); const existingEntries = new Map();
if (await fs.pathExists(csvPath)) { if (await fs.pathExists(csvPath)) {
const content = await fs.readFile(csvPath, 'utf8'); const content = await fs.readFile(csvPath, 'utf8');
const lines = content.split('\n').filter((line) => line.trim()); const records = csv.parse(content, {
columns: true,
// Skip header skip_empty_lines: true,
for (let i = 1; i < lines.length; i++) { });
const line = lines[i]; for (const record of records) {
if (line) { existingEntries.set(`${record.module}:${record.name}`, record);
// Parse CSV (simple parsing assuming no commas in quoted fields)
const parts = line.split('","');
if (parts.length >= 6) {
const name = parts[0].replace(/^"/, '');
const module = parts[3];
existingEntries.set(`${module}:${name}`, line);
}
}
} }
} }
// Create CSV header with standalone column // Create CSV header with standalone column
let csv = 'name,displayName,description,module,path,standalone\n'; let csvContent = 'name,displayName,description,module,path,standalone\n';
// Combine existing and new tools // Combine existing and new tools
const allTools = new Map(); const allTools = new Map();
@ -920,15 +949,30 @@ class ManifestGenerator {
// Add/update new tools // Add/update new tools
for (const tool of this.tools) { for (const tool of this.tools) {
const key = `${tool.module}:${tool.name}`; const key = `${tool.module}:${tool.name}`;
allTools.set(key, `"${tool.name}","${tool.displayName}","${tool.description}","${tool.module}","${tool.path}","${tool.standalone}"`); allTools.set(key, {
name: tool.name,
displayName: tool.displayName,
description: tool.description,
module: tool.module,
path: tool.path,
standalone: tool.standalone,
});
} }
// Write all tools // Write all tools
for (const [, value] of allTools) { for (const [, record] of allTools) {
csv += value + '\n'; const row = [
escapeCsv(record.name),
escapeCsv(record.displayName),
escapeCsv(record.description),
escapeCsv(record.module),
escapeCsv(record.path),
escapeCsv(record.standalone),
].join(',');
csvContent += row + '\n';
} }
await fs.writeFile(csvPath, csv); await fs.writeFile(csvPath, csvContent);
return csvPath; return csvPath;
} }

View File

@ -297,7 +297,7 @@ class CustomHandler {
const agentFiles = await this.findFilesRecursively(sourceAgentsPath, ['.agent.yaml']); const agentFiles = await this.findFilesRecursively(sourceAgentsPath, ['.agent.yaml']);
for (const agentFile of agentFiles) { for (const agentFile of agentFiles) {
const relativePath = path.relative(sourceAgentsPath, agentFile); const relativePath = path.relative(sourceAgentsPath, agentFile).split(path.sep).join('/');
const targetDir = path.join(targetAgentsPath, path.dirname(relativePath)); const targetDir = path.join(targetAgentsPath, path.dirname(relativePath));
await fs.ensureDir(targetDir); await fs.ensureDir(targetDir);

View File

@ -3,6 +3,7 @@ const fs = require('fs-extra');
const chalk = require('chalk'); const chalk = require('chalk');
const { XmlHandler } = require('../../../lib/xml-handler'); const { XmlHandler } = require('../../../lib/xml-handler');
const { getSourcePath } = require('../../../lib/project-root'); const { getSourcePath } = require('../../../lib/project-root');
const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
/** /**
* Base class for IDE-specific setup * Base class for IDE-specific setup
@ -18,7 +19,7 @@ class BaseIdeSetup {
this.configFile = null; // Override in subclasses when detection is file-based this.configFile = null; // Override in subclasses when detection is file-based
this.detectionPaths = []; // Additional paths that indicate the IDE is configured this.detectionPaths = []; // Additional paths that indicate the IDE is configured
this.xmlHandler = new XmlHandler(); this.xmlHandler = new XmlHandler();
this.bmadFolderName = 'bmad'; // Default, can be overridden this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden
} }
/** /**
@ -57,7 +58,7 @@ class BaseIdeSetup {
if (this.configDir) { if (this.configDir) {
const configPath = path.join(projectDir, this.configDir); const configPath = path.join(projectDir, this.configDir);
if (await fs.pathExists(configPath)) { if (await fs.pathExists(configPath)) {
const bmadRulesPath = path.join(configPath, 'bmad'); const bmadRulesPath = path.join(configPath, BMAD_FOLDER_NAME);
if (await fs.pathExists(bmadRulesPath)) { if (await fs.pathExists(bmadRulesPath)) {
await fs.remove(bmadRulesPath); await fs.remove(bmadRulesPath);
console.log(chalk.dim(`Removed ${this.name} BMAD configuration`)); console.log(chalk.dim(`Removed ${this.name} BMAD configuration`));
@ -445,6 +446,11 @@ class BaseIdeSetup {
try { try {
const content = await fs.readFile(fullPath, 'utf8'); const content = await fs.readFile(fullPath, 'utf8');
// Skip internal/engine files (not user-facing tasks/tools)
if (content.includes('internal="true"')) {
continue;
}
// Check for standalone="true" in XML files // Check for standalone="true" in XML files
if (entry.name.endsWith('.xml')) { if (entry.name.endsWith('.xml')) {
// Look for standalone="true" in the opening tag (task or tool) // Look for standalone="true" in the opening tag (task or tool)

View File

@ -66,6 +66,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
*/ */
async installToTarget(projectDir, bmadDir, config, options) { async installToTarget(projectDir, bmadDir, config, options) {
const { target_dir, template_type, artifact_types } = config; const { target_dir, template_type, artifact_types } = config;
// Skip targets with explicitly empty artifact_types array
// This prevents creating empty directories when no artifacts will be written
if (Array.isArray(artifact_types) && artifact_types.length === 0) {
return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0 } };
}
const targetPath = path.join(projectDir, target_dir); const targetPath = path.join(projectDir, target_dir);
await this.ensureDir(targetPath); await this.ensureDir(targetPath);
@ -86,10 +93,11 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config); results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config);
} }
// Install tasks and tools // Install tasks and tools using template system (supports TOML for Gemini, MD for others)
if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) { if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) {
const taskToolGen = new TaskToolCommandGenerator(); const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName);
const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, targetPath); const { artifacts } = await taskToolGen.collectTaskToolArtifacts(bmadDir);
const taskToolResult = await this.writeTaskToolArtifacts(targetPath, artifacts, template_type, config);
results.tasks = taskToolResult.tasks || 0; results.tasks = taskToolResult.tasks || 0;
results.tools = taskToolResult.tools || 0; results.tools = taskToolResult.tools || 0;
} }
@ -180,6 +188,53 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
return count; return count;
} }
/**
* Write task/tool artifacts to target directory using templates
* @param {string} targetPath - Target directory path
* @param {Array} artifacts - Task/tool artifacts
* @param {string} templateType - Template type to use
* @param {Object} config - Installation configuration
* @returns {Promise<Object>} Counts of tasks and tools written
*/
async writeTaskToolArtifacts(targetPath, artifacts, templateType, config = {}) {
let taskCount = 0;
let toolCount = 0;
// Pre-load templates to avoid repeated file I/O in the loop
const taskTemplate = await this.loadTemplate(templateType, 'task', config, 'default-task');
const toolTemplate = await this.loadTemplate(templateType, 'tool', config, 'default-tool');
const { artifact_types } = config;
for (const artifact of artifacts) {
if (artifact.type !== 'task' && artifact.type !== 'tool') {
continue;
}
// Skip if the specific artifact type is not requested in config
if (artifact_types) {
if (artifact.type === 'task' && !artifact_types.includes('tasks')) continue;
if (artifact.type === 'tool' && !artifact_types.includes('tools')) continue;
}
// Use pre-loaded template based on artifact type
const { content: template, extension } = artifact.type === 'task' ? taskTemplate : toolTemplate;
const content = this.renderTemplate(template, artifact);
const filename = this.generateFilename(artifact, artifact.type, extension);
const filePath = path.join(targetPath, filename);
await this.writeFile(filePath, content);
if (artifact.type === 'task') {
taskCount++;
} else {
toolCount++;
}
}
return { tasks: taskCount, tools: toolCount };
}
/** /**
* Load template based on type and configuration * Load template based on type and configuration
* @param {string} templateType - Template type (claude, windsurf, etc.) * @param {string} templateType - Template type (claude, windsurf, etc.)
@ -316,10 +371,24 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
renderTemplate(template, artifact) { renderTemplate(template, artifact) {
// Use the appropriate path property based on artifact type // Use the appropriate path property based on artifact type
let pathToUse = artifact.relativePath || ''; let pathToUse = artifact.relativePath || '';
if (artifact.type === 'agent-launcher') { switch (artifact.type) {
pathToUse = artifact.agentPath || artifact.relativePath || ''; case 'agent-launcher': {
} else if (artifact.type === 'workflow-command') { pathToUse = artifact.agentPath || artifact.relativePath || '';
pathToUse = artifact.workflowPath || artifact.relativePath || '';
break;
}
case 'workflow-command': {
pathToUse = artifact.workflowPath || artifact.relativePath || '';
break;
}
case 'task':
case 'tool': {
pathToUse = artifact.path || artifact.relativePath || '';
break;
}
// No default
} }
let rendered = template let rendered = template
@ -351,8 +420,9 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
// Reuse central logic to ensure consistent naming conventions // Reuse central logic to ensure consistent naming conventions
const standardName = toDashPath(artifact.relativePath); const standardName = toDashPath(artifact.relativePath);
// Clean up potential double extensions from source files (e.g. .yaml.md -> .md) // Clean up potential double extensions from source files (e.g. .yaml.md, .xml.md -> .md)
const baseName = standardName.replace(/\.(yaml|yml)\.md$/, '.md'); // This handles any extensions that might slip through toDashPath()
const baseName = standardName.replace(/\.(md|yaml|yml|json|xml|toml)\.md$/i, '.md');
// If using default markdown, preserve the bmad-agent- prefix for agents // If using default markdown, preserve the bmad-agent- prefix for agents
if (extension === '.md') { if (extension === '.md') {

View File

@ -104,7 +104,10 @@ class CodexSetup extends BaseIdeSetup {
); );
taskArtifacts.push({ taskArtifacts.push({
type: 'task', type: 'task',
name: task.name,
displayName: task.name,
module: task.module, module: task.module,
path: task.path,
sourcePath: task.path, sourcePath: task.path,
relativePath: path.join(task.module, 'tasks', `${task.name}.md`), relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
content, content,
@ -116,7 +119,7 @@ class CodexSetup extends BaseIdeSetup {
const workflowCount = await workflowGenerator.writeDashArtifacts(destDir, workflowArtifacts); const workflowCount = await workflowGenerator.writeDashArtifacts(destDir, workflowArtifacts);
// Also write tasks using underscore format // Also write tasks using underscore format
const ttGen = new TaskToolCommandGenerator(); const ttGen = new TaskToolCommandGenerator(this.bmadFolderName);
const tasksWritten = await ttGen.writeDashArtifacts(destDir, taskArtifacts); const tasksWritten = await ttGen.writeDashArtifacts(destDir, taskArtifacts);
const written = agentCount + workflowCount + tasksWritten; const written = agentCount + workflowCount + tasksWritten;
@ -214,7 +217,10 @@ class CodexSetup extends BaseIdeSetup {
artifacts.push({ artifacts.push({
type: 'task', type: 'task',
name: task.name,
displayName: task.name,
module: task.module, module: task.module,
path: task.path,
sourcePath: task.path, sourcePath: task.path,
relativePath: path.join(task.module, 'tasks', `${task.name}.md`), relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
content, content,

View File

@ -1,6 +1,7 @@
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('node:path'); const path = require('node:path');
const chalk = require('chalk'); const chalk = require('chalk');
const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
/** /**
* IDE Manager - handles IDE-specific setup * IDE Manager - handles IDE-specific setup
@ -14,7 +15,7 @@ class IdeManager {
constructor() { constructor() {
this.handlers = new Map(); this.handlers = new Map();
this._initialized = false; this._initialized = false;
this.bmadFolderName = 'bmad'; // Default, can be overridden this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden
} }
/** /**
@ -73,6 +74,9 @@ class IdeManager {
if (HandlerClass) { if (HandlerClass) {
const instance = new HandlerClass(); const instance = new HandlerClass();
if (instance.name && typeof instance.name === 'string') { if (instance.name && typeof instance.name === 'string') {
if (typeof instance.setBmadFolderName === 'function') {
instance.setBmadFolderName(this.bmadFolderName);
}
this.handlers.set(instance.name, instance); this.handlers.set(instance.name, instance);
} }
} }
@ -100,7 +104,9 @@ class IdeManager {
if (!platformInfo.installer) continue; if (!platformInfo.installer) continue;
const handler = new ConfigDrivenIdeSetup(platformCode, platformInfo); const handler = new ConfigDrivenIdeSetup(platformCode, platformInfo);
handler.setBmadFolderName(this.bmadFolderName); if (typeof handler.setBmadFolderName === 'function') {
handler.setBmadFolderName(this.bmadFolderName);
}
this.handlers.set(platformCode, handler); this.handlers.set(platformCode, handler);
} }
} }

View File

@ -94,9 +94,6 @@ platforms:
- target_dir: .github/agents - target_dir: .github/agents
template_type: copilot_agents template_type: copilot_agents
artifact_types: [agents] artifact_types: [agents]
- target_dir: .vscode
template_type: vscode_settings
artifact_types: []
iflow: iflow:
name: "iFlow" name: "iFlow"

View File

@ -1,14 +1,14 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const chalk = require('chalk'); const chalk = require('chalk');
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = require('./path-utils'); const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils');
/** /**
* Generates launcher command files for each agent * Generates launcher command files for each agent
* Similar to WorkflowCommandGenerator but for agents * Similar to WorkflowCommandGenerator but for agents
*/ */
class AgentCommandGenerator { class AgentCommandGenerator {
constructor(bmadFolderName = 'bmad') { constructor(bmadFolderName = BMAD_FOLDER_NAME) {
this.templatePath = path.join(__dirname, '../templates/agent-command-template.md'); this.templatePath = path.join(__dirname, '../templates/agent-command-template.md');
this.bmadFolderName = bmadFolderName; this.bmadFolderName = bmadFolderName;
} }

View File

@ -141,13 +141,24 @@ async function getTasksFromDir(dirPath, moduleName) {
const files = await fs.readdir(dirPath); const files = await fs.readdir(dirPath);
for (const file of files) { for (const file of files) {
if (!file.endsWith('.md')) { // Include both .md and .xml task files
if (!file.endsWith('.md') && !file.endsWith('.xml')) {
continue; continue;
} }
const filePath = path.join(dirPath, file);
const content = await fs.readFile(filePath, 'utf8');
// Skip internal/engine files (not user-facing tasks)
if (content.includes('internal="true"')) {
continue;
}
// Remove extension to get task name
const ext = file.endsWith('.xml') ? '.xml' : '.md';
tasks.push({ tasks.push({
path: path.join(dirPath, file), path: filePath,
name: file.replace('.md', ''), name: file.replace(ext, ''),
module: moduleName, module: moduleName,
}); });
} }

View File

@ -18,6 +18,9 @@
const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools']; const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools'];
const AGENT_SEGMENT = 'agents'; const AGENT_SEGMENT = 'agents';
// BMAD installation folder name - centralized constant for all installers
const BMAD_FOLDER_NAME = '_bmad';
/** /**
* Convert hierarchical path to flat dash-separated name (NEW STANDARD) * Convert hierarchical path to flat dash-separated name (NEW STANDARD)
* Converts: 'bmm', 'agents', 'pm' 'bmad-agent-bmm-pm.md' * Converts: 'bmm', 'agents', 'pm' 'bmad-agent-bmm-pm.md'
@ -59,7 +62,9 @@ function toDashPath(relativePath) {
return 'bmad-unknown.md'; return 'bmad-unknown.md';
} }
const withoutExt = relativePath.replace('.md', ''); // Strip common file extensions to avoid double extensions in generated filenames
// e.g., 'create-story.xml' → 'create-story', 'workflow.yaml' → 'workflow'
const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, '');
const parts = withoutExt.split(/[/\\]/); const parts = withoutExt.split(/[/\\]/);
const module = parts[0]; const module = parts[0];
@ -183,7 +188,8 @@ function toUnderscoreName(module, type, name) {
* @deprecated Use toDashPath instead * @deprecated Use toDashPath instead
*/ */
function toUnderscorePath(relativePath) { function toUnderscorePath(relativePath) {
const withoutExt = relativePath.replace('.md', ''); // Strip common file extensions (same as toDashPath for consistency)
const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, '');
const parts = withoutExt.split(/[/\\]/); const parts = withoutExt.split(/[/\\]/);
const module = parts[0]; const module = parts[0];
@ -289,4 +295,5 @@ module.exports = {
TYPE_SEGMENTS, TYPE_SEGMENTS,
AGENT_SEGMENT, AGENT_SEGMENT,
BMAD_FOLDER_NAME,
}; };

View File

@ -2,12 +2,98 @@ const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const csv = require('csv-parse/sync'); const csv = require('csv-parse/sync');
const chalk = require('chalk'); const chalk = require('chalk');
const { toColonName, toColonPath, toDashPath } = require('./path-utils'); const { toColonName, toColonPath, toDashPath, BMAD_FOLDER_NAME } = require('./path-utils');
/** /**
* Generates command files for standalone tasks and tools * Generates command files for standalone tasks and tools
*/ */
class TaskToolCommandGenerator { class TaskToolCommandGenerator {
/**
* @param {string} bmadFolderName - Name of the BMAD folder for template rendering (default: '_bmad')
* Note: This parameter is accepted for API consistency with AgentCommandGenerator and
* WorkflowCommandGenerator, but is not used for path stripping. The manifest always stores
* filesystem paths with '_bmad/' prefix (the actual folder name), while bmadFolderName is
* used for template placeholder rendering ({{bmadFolderName}}).
*/
constructor(bmadFolderName = BMAD_FOLDER_NAME) {
this.bmadFolderName = bmadFolderName;
}
/**
* Collect task and tool artifacts for IDE installation
* @param {string} bmadDir - BMAD installation directory
* @returns {Promise<Object>} Artifacts array with metadata
*/
async collectTaskToolArtifacts(bmadDir) {
const tasks = await this.loadTaskManifest(bmadDir);
const tools = await this.loadToolManifest(bmadDir);
// Filter to only standalone items
const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
const artifacts = [];
const bmadPrefix = `${BMAD_FOLDER_NAME}/`;
// Collect task artifacts
for (const task of standaloneTasks) {
let taskPath = (task.path || '').replaceAll('\\', '/');
// Convert absolute paths to relative paths
if (path.isAbsolute(taskPath)) {
taskPath = path.relative(bmadDir, taskPath).replaceAll('\\', '/');
}
// Remove _bmad/ prefix if present to get relative path within bmad folder
if (taskPath.startsWith(bmadPrefix)) {
taskPath = taskPath.slice(bmadPrefix.length);
}
const taskExt = path.extname(taskPath) || '.md';
artifacts.push({
type: 'task',
name: task.name,
displayName: task.displayName || task.name,
description: task.description || `Execute ${task.displayName || task.name}`,
module: task.module,
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
relativePath: `${task.module}/tasks/${task.name}${taskExt}`,
path: taskPath,
});
}
// Collect tool artifacts
for (const tool of standaloneTools) {
let toolPath = (tool.path || '').replaceAll('\\', '/');
// Convert absolute paths to relative paths
if (path.isAbsolute(toolPath)) {
toolPath = path.relative(bmadDir, toolPath).replaceAll('\\', '/');
}
// Remove _bmad/ prefix if present to get relative path within bmad folder
if (toolPath.startsWith(bmadPrefix)) {
toolPath = toolPath.slice(bmadPrefix.length);
}
const toolExt = path.extname(toolPath) || '.md';
artifacts.push({
type: 'tool',
name: tool.name,
displayName: tool.displayName || tool.name,
description: tool.description || `Execute ${tool.displayName || tool.name}`,
module: tool.module,
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
relativePath: `${tool.module}/tools/${tool.name}${toolExt}`,
path: toolPath,
});
}
return {
artifacts,
counts: {
tasks: standaloneTasks.length,
tools: standaloneTools.length,
},
};
}
/** /**
* Generate task and tool commands from manifest CSVs * Generate task and tool commands from manifest CSVs
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory
@ -65,9 +151,35 @@ class TaskToolCommandGenerator {
const description = item.description || `Execute ${item.displayName || item.name}`; const description = item.description || `Execute ${item.displayName || item.name}`;
// Convert path to use {project-root} placeholder // Convert path to use {project-root} placeholder
// Handle undefined/missing path by constructing from module and name
let itemPath = item.path; let itemPath = item.path;
if (itemPath && typeof itemPath === 'string' && itemPath.startsWith('bmad/')) { if (!itemPath || typeof itemPath !== 'string') {
itemPath = `{project-root}/${itemPath}`; // Fallback: construct path from module and name if path is missing
const typePlural = type === 'task' ? 'tasks' : 'tools';
itemPath = `{project-root}/${this.bmadFolderName}/${item.module}/${typePlural}/${item.name}.md`;
} else {
// Normalize path separators to forward slashes
itemPath = itemPath.replaceAll('\\', '/');
// Extract relative path from absolute paths (Windows or Unix)
// Look for _bmad/ or bmad/ in the path and extract everything after it
// Match patterns like: /_bmad/core/tasks/... or /bmad/core/tasks/...
// Use [/\\] to handle both Unix forward slashes and Windows backslashes,
// and also paths without a leading separator (e.g., C:/_bmad/...)
const bmadMatch = itemPath.match(/[/\\]_bmad[/\\](.+)$/) || itemPath.match(/[/\\]bmad[/\\](.+)$/);
if (bmadMatch) {
// Found /_bmad/ or /bmad/ - use relative path after it
itemPath = `{project-root}/${this.bmadFolderName}/${bmadMatch[1]}`;
} else if (itemPath.startsWith(`${BMAD_FOLDER_NAME}/`)) {
// Relative path starting with _bmad/
itemPath = `{project-root}/${this.bmadFolderName}/${itemPath.slice(BMAD_FOLDER_NAME.length + 1)}`;
} else if (itemPath.startsWith('bmad/')) {
// Relative path starting with bmad/
itemPath = `{project-root}/${this.bmadFolderName}/${itemPath.slice(5)}`;
} else if (!itemPath.startsWith('{project-root}')) {
// For other relative paths, prefix with project root and bmad folder
itemPath = `{project-root}/${this.bmadFolderName}/${itemPath}`;
}
} }
return `--- return `---
@ -187,7 +299,7 @@ Follow all instructions in the ${type} file exactly as written.
// Generate command files for tasks // Generate command files for tasks
for (const task of standaloneTasks) { for (const task of standaloneTasks) {
const commandContent = this.generateCommandContent(task, 'task'); const commandContent = this.generateCommandContent(task, 'task');
// Use underscore format: bmad_bmm_name.md // Use dash format: bmad-bmm-name.md
const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`); const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`);
const commandPath = path.join(baseCommandsDir, flatName); const commandPath = path.join(baseCommandsDir, flatName);
await fs.ensureDir(path.dirname(commandPath)); await fs.ensureDir(path.dirname(commandPath));
@ -198,7 +310,7 @@ Follow all instructions in the ${type} file exactly as written.
// Generate command files for tools // Generate command files for tools
for (const tool of standaloneTools) { for (const tool of standaloneTools) {
const commandContent = this.generateCommandContent(tool, 'tool'); const commandContent = this.generateCommandContent(tool, 'tool');
// Use underscore format: bmad_bmm_name.md // Use dash format: bmad-bmm-name.md
const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`); const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`);
const commandPath = path.join(baseCommandsDir, flatName); const commandPath = path.join(baseCommandsDir, flatName);
await fs.ensureDir(path.dirname(commandPath)); await fs.ensureDir(path.dirname(commandPath));

View File

@ -2,13 +2,13 @@ const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const csv = require('csv-parse/sync'); const csv = require('csv-parse/sync');
const chalk = require('chalk'); const chalk = require('chalk');
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = require('./path-utils'); const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils');
/** /**
* Generates command files for each workflow in the manifest * Generates command files for each workflow in the manifest
*/ */
class WorkflowCommandGenerator { class WorkflowCommandGenerator {
constructor(bmadFolderName = 'bmad') { constructor(bmadFolderName = BMAD_FOLDER_NAME) {
this.templatePath = path.join(__dirname, '../templates/workflow-command-template.md'); this.templatePath = path.join(__dirname, '../templates/workflow-command-template.md');
this.bmadFolderName = bmadFolderName; this.bmadFolderName = bmadFolderName;
} }

View File

@ -0,0 +1,10 @@
---
name: '{{name}}'
description: '{{description}}'
---
# {{name}}
Read the entire task file at: {project-root}/{{bmadFolderName}}/{{path}}
Follow all instructions in the task file exactly as written.

View File

@ -0,0 +1,10 @@
---
name: '{{name}}'
description: '{{description}}'
---
# {{name}}
Read the entire tool file at: {project-root}/{{bmadFolderName}}/{{path}}
Follow all instructions in the tool file exactly as written.

View File

@ -0,0 +1,11 @@
description = "Executes the {{name}} task from the BMAD Method."
prompt = """
Execute the BMAD '{{name}}' task.
TASK INSTRUCTIONS:
1. LOAD the task file from {project-root}/{{bmadFolderName}}/{{path}}
2. READ its entire contents
3. FOLLOW every instruction precisely as specified
TASK FILE: {project-root}/{{bmadFolderName}}/{{path}}
"""

View File

@ -0,0 +1,11 @@
description = "Executes the {{name}} tool from the BMAD Method."
prompt = """
Execute the BMAD '{{name}}' tool.
TOOL INSTRUCTIONS:
1. LOAD the tool file from {project-root}/{{bmadFolderName}}/{{path}}
2. READ its entire contents
3. FOLLOW every instruction precisely as specified
TOOL FILE: {project-root}/{{bmadFolderName}}/{{path}}
"""

View File

@ -7,6 +7,7 @@ const { XmlHandler } = require('../../../lib/xml-handler');
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
const { filterCustomizationData } = require('../../../lib/agent/compiler'); const { filterCustomizationData } = require('../../../lib/agent/compiler');
const { ExternalModuleManager } = require('./external-manager'); const { ExternalModuleManager } = require('./external-manager');
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
/** /**
* Manages the installation, updating, and removal of BMAD modules. * Manages the installation, updating, and removal of BMAD modules.
@ -27,7 +28,7 @@ const { ExternalModuleManager } = require('./external-manager');
class ModuleManager { class ModuleManager {
constructor(options = {}) { constructor(options = {}) {
this.xmlHandler = new XmlHandler(); this.xmlHandler = new XmlHandler();
this.bmadFolderName = 'bmad'; // Default, can be overridden this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden
this.customModulePaths = new Map(); // Initialize custom module paths this.customModulePaths = new Map(); // Initialize custom module paths
this.externalModuleManager = new ExternalModuleManager(); // For external official modules this.externalModuleManager = new ExternalModuleManager(); // For external official modules
} }
@ -870,7 +871,7 @@ class ModuleManager {
for (const agentFile of agentFiles) { for (const agentFile of agentFiles) {
if (!agentFile.endsWith('.agent.yaml')) continue; if (!agentFile.endsWith('.agent.yaml')) continue;
const relativePath = path.relative(sourceAgentsPath, agentFile); const relativePath = path.relative(sourceAgentsPath, agentFile).split(path.sep).join('/');
const targetDir = path.join(targetAgentsPath, path.dirname(relativePath)); const targetDir = path.join(targetAgentsPath, path.dirname(relativePath));
await fs.ensureDir(targetDir); await fs.ensureDir(targetDir);

View File

@ -42,7 +42,7 @@ function findBmadConfig(startPath = process.cwd()) {
* @returns {string} Resolved path * @returns {string} Resolved path
*/ */
function resolvePath(pathStr, context) { function resolvePath(pathStr, context) {
return pathStr.replaceAll('{project-root}', context.projectRoot).replaceAll('{bmad-folder}', context_bmadFolder); return pathStr.replaceAll('{project-root}', context.projectRoot).replaceAll('{bmad-folder}', context.bmadFolder);
} }
/** /**

View File

@ -8,6 +8,8 @@
*/ */
let _clack = null; let _clack = null;
let _clackCore = null;
let _picocolors = null;
/** /**
* Lazy-load @clack/prompts (ESM module) * Lazy-load @clack/prompts (ESM module)
@ -20,6 +22,28 @@ async function getClack() {
return _clack; return _clack;
} }
/**
* Lazy-load @clack/core (ESM module)
* @returns {Promise<Object>} The clack core module
*/
async function getClackCore() {
if (!_clackCore) {
_clackCore = await import('@clack/core');
}
return _clackCore;
}
/**
* Lazy-load picocolors
* @returns {Promise<Object>} The picocolors module
*/
async function getPicocolors() {
if (!_picocolors) {
_picocolors = (await import('picocolors')).default;
}
return _picocolors;
}
/** /**
* Handle user cancellation gracefully * Handle user cancellation gracefully
* @param {any} value - The value to check * @param {any} value - The value to check
@ -191,6 +215,118 @@ async function groupMultiselect(options) {
return result; return result;
} }
/**
* Default filter function for autocomplete - case-insensitive label matching
* @param {string} search - Search string
* @param {Object} option - Option object with label
* @returns {boolean} Whether the option matches
*/
function defaultAutocompleteFilter(search, option) {
const label = option.label ?? String(option.value ?? '');
return label.toLowerCase().includes(search.toLowerCase());
}
/**
* Autocomplete multi-select prompt with type-ahead filtering
* Custom implementation that always shows "Space/Tab:" in the hint
* @param {Object} options - Prompt options
* @param {string} options.message - The question to ask
* @param {Array} options.options - Array of choices [{label, value, hint?}]
* @param {string} [options.placeholder] - Placeholder text for search input
* @param {Array} [options.initialValues] - Array of initially selected values
* @param {boolean} [options.required=false] - Whether at least one must be selected
* @param {number} [options.maxItems=5] - Maximum visible items in scrollable list
* @param {Function} [options.filter] - Custom filter function (search, option) => boolean
* @returns {Promise<Array>} Array of selected values
*/
async function autocompleteMultiselect(options) {
const core = await getClackCore();
const clack = await getClack();
const color = await getPicocolors();
const filterFn = options.filter ?? defaultAutocompleteFilter;
const prompt = new core.AutocompletePrompt({
options: options.options,
multiple: true,
filter: filterFn,
validate: () => {
if (options.required && prompt.selectedValues.length === 0) {
return 'Please select at least one item';
}
},
initialValue: options.initialValues,
render() {
const barColor = this.state === 'error' ? color.yellow : color.cyan;
const bar = barColor(clack.S_BAR);
const barEnd = barColor(clack.S_BAR_END);
const title = `${color.gray(clack.S_BAR)}\n${clack.symbol(this.state)} ${options.message}\n`;
const userInput = this.userInput;
const placeholder = options.placeholder || 'Type to search...';
const hasPlaceholder = userInput === '' && placeholder !== undefined;
// Show placeholder or user input with cursor
const searchDisplay =
this.isNavigating || hasPlaceholder ? color.dim(hasPlaceholder ? placeholder : userInput) : this.userInputWithCursor;
const allOptions = this.options;
const matchCount =
this.filteredOptions.length === allOptions.length
? ''
: color.dim(` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})`);
// Render option with checkbox
const renderOption = (opt, isHighlighted) => {
const isSelected = this.selectedValues.includes(opt.value);
const label = opt.label ?? String(opt.value ?? '');
const hintText = opt.hint && opt.value === this.focusedValue ? color.dim(` (${opt.hint})`) : '';
const checkbox = isSelected ? color.green(clack.S_CHECKBOX_SELECTED) : color.dim(clack.S_CHECKBOX_INACTIVE);
return isHighlighted ? `${checkbox} ${label}${hintText}` : `${checkbox} ${color.dim(label)}`;
};
switch (this.state) {
case 'submit': {
return `${title}${color.gray(clack.S_BAR)} ${color.dim(`${this.selectedValues.length} items selected`)}`;
}
case 'cancel': {
return `${title}${color.gray(clack.S_BAR)} ${color.strikethrough(color.dim(userInput))}`;
}
default: {
// Always show "SPACE:" regardless of isNavigating state
const hints = [`${color.dim('↑/↓')} to navigate`, `${color.dim('TAB/SPACE:')} select`, `${color.dim('ENTER:')} confirm`];
const noMatchesLine = this.filteredOptions.length === 0 && userInput ? [`${bar} ${color.yellow('No matches found')}`] : [];
const errorLine = this.state === 'error' ? [`${bar} ${color.yellow(this.error)}`] : [];
const headerLines = [...`${title}${bar}`.split('\n'), `${bar} ${searchDisplay}${matchCount}`, ...noMatchesLine, ...errorLine];
const footerLines = [`${bar} ${color.dim(hints.join(' • '))}`, `${barEnd}`];
const optionLines = clack.limitOptions({
cursor: this.cursor,
options: this.filteredOptions,
style: renderOption,
maxItems: options.maxItems || 5,
output: options.output,
rowPadding: headerLines.length + footerLines.length,
});
return [...headerLines, ...optionLines.map((line) => `${bar} ${line}`), ...footerLines].join('\n');
}
}
},
});
const result = await prompt.prompt();
await handleCancel(result);
return result;
}
/** /**
* Confirm prompt (replaces Inquirer 'confirm' type) * Confirm prompt (replaces Inquirer 'confirm' type)
* @param {Object} options - Prompt options * @param {Object} options - Prompt options
@ -211,7 +347,12 @@ async function confirm(options) {
} }
/** /**
* Text input prompt (replaces Inquirer 'input' type) * Text input prompt with Tab-to-fill-placeholder support (replaces Inquirer 'input' type)
*
* This custom implementation restores the Tab-to-fill-placeholder behavior that was
* intentionally removed in @clack/prompts v1.0.0 (placeholder became purely visual).
* Uses @clack/core's TextPrompt primitive with custom key handling.
*
* @param {Object} options - Prompt options * @param {Object} options - Prompt options
* @param {string} options.message - The question to ask * @param {string} options.message - The question to ask
* @param {string} [options.default] - Default value * @param {string} [options.default] - Default value
@ -220,20 +361,64 @@ async function confirm(options) {
* @returns {Promise<string>} User's input * @returns {Promise<string>} User's input
*/ */
async function text(options) { async function text(options) {
const clack = await getClack(); const core = await getClackCore();
const color = await getPicocolors();
// Use default as placeholder if placeholder not explicitly provided // Use default as placeholder if placeholder not explicitly provided
// This shows the default value as grayed-out hint text // This shows the default value as grayed-out hint text
const placeholder = options.placeholder === undefined ? options.default : options.placeholder; const placeholder = options.placeholder === undefined ? options.default : options.placeholder;
const defaultValue = options.default;
const result = await clack.text({ const prompt = new core.TextPrompt({
message: options.message, defaultValue,
defaultValue: options.default,
placeholder: typeof placeholder === 'string' ? placeholder : undefined,
validate: options.validate, validate: options.validate,
render() {
const title = `${color.gray('◆')} ${options.message}`;
// Show placeholder as dim text when input is empty
let valueDisplay;
if (this.state === 'error') {
valueDisplay = color.yellow(this.userInputWithCursor);
} else if (this.userInput) {
valueDisplay = this.userInputWithCursor;
} else if (placeholder) {
// Show placeholder with cursor indicator when empty
valueDisplay = `${color.inverse(color.hidden('_'))}${color.dim(placeholder)}`;
} else {
valueDisplay = color.inverse(color.hidden('_'));
}
const bar = color.gray('│');
// Handle different states
if (this.state === 'submit') {
return `${color.gray('◇')} ${options.message}\n${bar} ${color.dim(this.value || defaultValue || '')}`;
}
if (this.state === 'cancel') {
return `${color.gray('◇')} ${options.message}\n${bar} ${color.strikethrough(color.dim(this.userInput || ''))}`;
}
if (this.state === 'error') {
return `${color.yellow('▲')} ${options.message}\n${bar} ${valueDisplay}\n${color.yellow('│')} ${color.yellow(this.error)}`;
}
return `${title}\n${bar} ${valueDisplay}\n${bar}`;
},
}); });
// Add Tab key handler to fill placeholder into input
prompt.on('key', (char) => {
if (char === '\t' && placeholder && !prompt.userInput) {
// Use _setUserInput with write=true to populate the readline and update internal state
prompt._setUserInput(placeholder, true);
}
});
const result = await prompt.prompt();
await handleCancel(result); await handleCancel(result);
// TextPrompt's finalize handler already applies defaultValue for empty input
return result; return result;
} }
@ -423,6 +608,7 @@ module.exports = {
select, select,
multiselect, multiselect,
groupMultiselect, groupMultiselect,
autocompleteMultiselect,
confirm, confirm,
text, text,
password, password,

View File

@ -344,6 +344,9 @@ class UI {
/** /**
* Prompt for tool/IDE selection (called after module configuration) * Prompt for tool/IDE selection (called after module configuration)
* Uses a split prompt approach:
* 1. Recommended tools - standard multiselect for 3 preferred tools
* 2. Additional tools - autocompleteMultiselect with search capability
* @param {string} projectDir - Project directory to check for existing IDEs * @param {string} projectDir - Project directory to check for existing IDEs
* @returns {Object} Tool configuration * @returns {Object} Tool configuration
*/ */
@ -366,95 +369,123 @@ class UI {
const preferredIdes = ideManager.getPreferredIdes(); const preferredIdes = ideManager.getPreferredIdes();
const otherIdes = ideManager.getOtherIdes(); const otherIdes = ideManager.getOtherIdes();
// Build grouped options object for groupMultiselect // Determine which configured IDEs are in "preferred" vs "other" categories
const groupedOptions = {}; const configuredPreferred = configuredIdes.filter((id) => preferredIdes.some((ide) => ide.value === id));
const processedIdes = new Set(); const configuredOther = configuredIdes.filter((id) => otherIdes.some((ide) => ide.value === id));
const initialValues = [];
// First, add previously configured IDEs, marked with ✅ // Warn about previously configured tools that are no longer available
const allKnownValues = new Set([...preferredIdes, ...otherIdes].map((ide) => ide.value));
const unknownTools = configuredIdes.filter((id) => id && typeof id === 'string' && !allKnownValues.has(id));
if (unknownTools.length > 0) {
console.log(chalk.yellow(`⚠️ Previously configured tools are no longer available: ${unknownTools.join(', ')}`));
}
// ─────────────────────────────────────────────────────────────────────────────
// UPGRADE PATH: If tools already configured, show all tools with configured at top
// ─────────────────────────────────────────────────────────────────────────────
if (configuredIdes.length > 0) { if (configuredIdes.length > 0) {
const configuredGroup = []; const allTools = [...preferredIdes, ...otherIdes];
for (const ideValue of configuredIdes) {
// Skip empty or invalid IDE values
if (!ideValue || typeof ideValue !== 'string') {
continue;
}
// Find the IDE in either preferred or other lists // Sort: configured tools first, then preferred, then others
const preferredIde = preferredIdes.find((ide) => ide.value === ideValue); const sortedTools = [
const otherIde = otherIdes.find((ide) => ide.value === ideValue); ...allTools.filter((ide) => configuredIdes.includes(ide.value)),
const ide = preferredIde || otherIde; ...allTools.filter((ide) => !configuredIdes.includes(ide.value)),
];
if (ide) { const upgradeOptions = sortedTools.map((ide) => {
configuredGroup.push({ const isConfigured = configuredIdes.includes(ide.value);
label: `${ide.name}`, const isPreferred = preferredIdes.some((p) => p.value === ide.value);
value: ide.value, let label = ide.name;
}); if (isPreferred) label += ' ⭐';
processedIdes.add(ide.value); if (isConfigured) label += ' ✅';
initialValues.push(ide.value); // Pre-select configured IDEs return { label, value: ide.value };
} else {
// Warn about unrecognized IDE (but don't fail)
console.log(chalk.yellow(`⚠️ Previously configured IDE '${ideValue}' is no longer available`));
}
}
if (configuredGroup.length > 0) {
groupedOptions['Previously Configured'] = configuredGroup;
}
}
// Add preferred tools (excluding already processed)
const remainingPreferred = preferredIdes.filter((ide) => !processedIdes.has(ide.value));
if (remainingPreferred.length > 0) {
groupedOptions['Recommended Tools'] = remainingPreferred.map((ide) => {
processedIdes.add(ide.value);
return {
label: `${ide.name}`,
value: ide.value,
};
}); });
// Sort initialValues to match display order
const sortedInitialValues = sortedTools.filter((ide) => configuredIdes.includes(ide.value)).map((ide) => ide.value);
const upgradeSelected = await prompts.autocompleteMultiselect({
message: 'Integrate with',
options: upgradeOptions,
initialValues: sortedInitialValues,
required: false,
maxItems: 8,
});
const selectedIdes = upgradeSelected || [];
if (selectedIdes.length === 0) {
console.log('');
const confirmNoTools = await prompts.confirm({
message: 'No tools selected. Continue without installing any tools?',
default: false,
});
if (!confirmNoTools) {
return this.promptToolSelection(projectDir);
}
return { ides: [], skipIde: true };
}
// Display selected tools
this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
return { ides: selectedIdes, skipIde: false };
} }
// Add other tools (excluding already processed) // ─────────────────────────────────────────────────────────────────────────────
const remainingOther = otherIdes.filter((ide) => !processedIdes.has(ide.value)); // NEW INSTALL: Show all tools with search
if (remainingOther.length > 0) { // ─────────────────────────────────────────────────────────────────────────────
groupedOptions['Additional Tools'] = remainingOther.map((ide) => ({ const allTools = [...preferredIdes, ...otherIdes];
label: ide.name,
const allToolOptions = allTools.map((ide) => {
const isPreferred = preferredIdes.some((p) => p.value === ide.value);
let label = ide.name;
if (isPreferred) label += ' ⭐';
return {
label,
value: ide.value, value: ide.value,
})); };
}
// Add standalone "None" option at the end
groupedOptions[' '] = [
{
label: '⚠ None - I am not installing any tools',
value: '__NONE__',
},
];
let selectedIdes = [];
selectedIdes = await prompts.groupMultiselect({
message: `Select tools to configure ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
options: groupedOptions,
initialValues: initialValues.length > 0 ? initialValues : undefined,
required: true,
selectableGroups: false,
}); });
// If user selected both "__NONE__" and other tools, honor the "None" choice const selectedIdes = await prompts.autocompleteMultiselect({
if (selectedIdes && selectedIdes.includes('__NONE__') && selectedIdes.length > 1) { message: 'Select tools:',
console.log(); options: allToolOptions,
console.log(chalk.yellow('⚠️ "None - I am not installing any tools" was selected, so no tools will be configured.')); initialValues: configuredIdes.length > 0 ? configuredIdes : undefined,
console.log(); required: false,
selectedIdes = []; maxItems: 8,
} else if (selectedIdes && selectedIdes.includes('__NONE__')) { });
// Only "__NONE__" was selected
selectedIdes = []; const allSelectedIdes = selectedIdes || [];
// ─────────────────────────────────────────────────────────────────────────────
// STEP 3: Confirm if no tools selected
// ─────────────────────────────────────────────────────────────────────────────
if (allSelectedIdes.length === 0) {
console.log('');
const confirmNoTools = await prompts.confirm({
message: 'No tools selected. Continue without installing any tools?',
default: false,
});
if (!confirmNoTools) {
// User wants to select tools - recurse
return this.promptToolSelection(projectDir);
}
return {
ides: [],
skipIde: true,
};
} }
// Display selected tools
this.displaySelectedTools(allSelectedIdes, preferredIdes, allTools);
return { return {
ides: selectedIdes || [], ides: allSelectedIdes,
skipIde: !selectedIdes || selectedIdes.length === 0, skipIde: allSelectedIdes.length === 0,
}; };
} }
@ -1655,6 +1686,27 @@ class UI {
console.log(''); console.log('');
} }
/**
* Display list of selected tools after IDE selection
* @param {Array} selectedIdes - Array of selected IDE values
* @param {Array} preferredIdes - Array of preferred IDE objects
* @param {Array} allTools - Array of all tool objects
*/
displaySelectedTools(selectedIdes, preferredIdes, allTools) {
if (selectedIdes.length === 0) return;
const preferredValues = new Set(preferredIdes.map((ide) => ide.value));
console.log('');
console.log(chalk.dim(' Selected tools:'));
for (const ideValue of selectedIdes) {
const tool = allTools.find((t) => t.value === ideValue);
const name = tool?.name || ideValue;
const marker = preferredValues.has(ideValue) ? ' ⭐' : '';
console.log(chalk.dim(`${name}${marker}`));
}
}
} }
module.exports = { UI }; module.exports = { UI };

480
tools/validate-file-refs.js Normal file
View File

@ -0,0 +1,480 @@
/**
* File Reference Validator
*
* Validates cross-file references in BMAD source files (agents, workflows, tasks, steps).
* Catches broken file paths, missing referenced files, and absolute path leaks.
*
* What it checks:
* - {project-root}/_bmad/ references in YAML and markdown resolve to real src/ files
* - Relative path references (./file.md, ../data/file.csv) point to existing files
* - exec="..." and <invoke-task> targets exist
* - Step metadata (thisStepFile, nextStepFile) references are valid
* - Load directives (Load: `./file.md`) target existing files
* - No absolute paths (/Users/, /home/, C:\) leak into source files
*
* What it does NOT check (deferred):
* - {installed_path} variable interpolation (self-referential, low risk)
* - {{mustache}} template variables (runtime substitution)
* - {config_source}:key dynamic YAML dereferences
*
* Usage:
* node tools/validate-file-refs.js # Warn on broken references (exit 0)
* node tools/validate-file-refs.js --strict # Fail on broken references (exit 1)
* node tools/validate-file-refs.js --verbose # Show all checked references
*
* Default mode is warning-only (exit 0) so adoption is non-disruptive.
* Use --strict when you want CI or pre-commit to enforce valid references.
*/
const fs = require('node:fs');
const path = require('node:path');
const yaml = require('yaml');
const PROJECT_ROOT = path.resolve(__dirname, '..');
const SRC_DIR = path.join(PROJECT_ROOT, 'src');
const VERBOSE = process.argv.includes('--verbose');
const STRICT = process.argv.includes('--strict');
// --- Constants ---
// File extensions to scan
const SCAN_EXTENSIONS = new Set(['.yaml', '.yml', '.md', '.xml']);
// Skip directories
const SKIP_DIRS = new Set(['node_modules', '_module-installer', '.git']);
// Pattern: {project-root}/_bmad/ references
const PROJECT_ROOT_REF = /\{project-root\}\/_bmad\/([^\s'"<>})\]`]+)/g;
// Pattern: {_bmad}/ shorthand references
const BMAD_SHORTHAND_REF = /\{_bmad\}\/([^\s'"<>})\]`]+)/g;
// Pattern: exec="..." attributes
const EXEC_ATTR = /exec="([^"]+)"/g;
// Pattern: <invoke-task> content
const INVOKE_TASK = /<invoke-task>([^<]+)<\/invoke-task>/g;
// Pattern: relative paths in quotes
const RELATIVE_PATH_QUOTED = /['"](\.\.\/?[^'"]+\.(?:md|yaml|yml|xml|json|csv|txt))['"]/g;
const RELATIVE_PATH_DOT = /['"](\.\/[^'"]+\.(?:md|yaml|yml|xml|json|csv|txt))['"]/g;
// Pattern: step metadata
const STEP_META = /(?:thisStepFile|nextStepFile|continueStepFile|skipToStepFile|altStepFile|workflowFile):\s*['"](\.[^'"]+)['"]/g;
// Pattern: Load directives
const LOAD_DIRECTIVE = /Load[:\s]+`(\.[^`]+)`/g;
// Pattern: absolute path leaks
const ABS_PATH_LEAK = /(?:\/Users\/|\/home\/|[A-Z]:\\\\)/;
// --- Output Escaping ---
function escapeAnnotation(str) {
return str.replaceAll('%', '%25').replaceAll('\r', '%0D').replaceAll('\n', '%0A');
}
function escapeTableCell(str) {
return String(str).replaceAll('|', String.raw`\|`);
}
// Path prefixes/patterns that only exist in installed structure, not in source
const INSTALL_ONLY_PATHS = ['_config/'];
// Files that are generated at install time and don't exist in the source tree
const INSTALL_GENERATED_FILES = ['config.yaml'];
// Variables that indicate a path is not statically resolvable
const UNRESOLVABLE_VARS = [
'{output_folder}',
'{value}',
'{timestamp}',
'{config_source}:',
'{installed_path}',
'{shared_path}',
'{planning_artifacts}',
'{research_topic}',
'{user_name}',
'{communication_language}',
'{epic_number}',
'{next_epic_num}',
'{epic_num}',
'{part_id}',
'{count}',
'{date}',
'{outputFile}',
'{nextStepFile}',
];
// --- File Discovery ---
function getSourceFiles(dir) {
const files = [];
function walk(currentDir) {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
if (SKIP_DIRS.has(entry.name)) continue;
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
walk(fullPath);
} else if (entry.isFile() && SCAN_EXTENSIONS.has(path.extname(entry.name))) {
files.push(fullPath);
}
}
}
walk(dir);
return files;
}
// --- Code Block Stripping ---
function stripCodeBlocks(content) {
return content.replaceAll(/```[\s\S]*?```/g, (m) => m.replaceAll(/[^\n]/g, ''));
}
function stripJsonExampleBlocks(content) {
// Strip bare JSON example blocks: { and } each on their own line.
// These are example/template data (not real file references).
return content.replaceAll(/^\{\s*\n(?:.*\n)*?^\}\s*$/gm, (m) => m.replaceAll(/[^\n]/g, ''));
}
// --- Path Mapping ---
function mapInstalledToSource(refPath) {
// Strip {project-root}/_bmad/ or {_bmad}/ prefix
let cleaned = refPath.replace(/^\{project-root\}\/_bmad\//, '').replace(/^\{_bmad\}\//, '');
// Also handle bare _bmad/ prefix (seen in some invoke-task)
cleaned = cleaned.replace(/^_bmad\//, '');
// Skip install-only paths (generated at install time, not in source)
if (isInstallOnly(cleaned)) return null;
// core/, bmm/, and utility/ are directly under src/
if (cleaned.startsWith('core/') || cleaned.startsWith('bmm/') || cleaned.startsWith('utility/')) {
return path.join(SRC_DIR, cleaned);
}
// Fallback: map directly under src/
return path.join(SRC_DIR, cleaned);
}
// --- Reference Extraction ---
function isResolvable(refStr) {
// Skip refs containing unresolvable runtime variables
if (refStr.includes('{{')) return false;
for (const v of UNRESOLVABLE_VARS) {
if (refStr.includes(v)) return false;
}
return true;
}
function isInstallOnly(cleanedPath) {
// Skip paths that only exist in the installed _bmad/ structure, not in src/
for (const prefix of INSTALL_ONLY_PATHS) {
if (cleanedPath.startsWith(prefix)) return true;
}
// Skip files that are generated during installation
const basename = path.basename(cleanedPath);
for (const generated of INSTALL_GENERATED_FILES) {
if (basename === generated) return true;
}
return false;
}
function extractYamlRefs(filePath, content) {
const refs = [];
let doc;
try {
doc = yaml.parseDocument(content);
} catch {
return refs; // Skip unparseable YAML (schema validator handles this)
}
function checkValue(value, range, keyPath) {
if (typeof value !== 'string') return;
if (!isResolvable(value)) return;
const line = range ? offsetToLine(content, range[0]) : undefined;
// Check for {project-root}/_bmad/ refs
const prMatch = value.match(/\{project-root\}\/_bmad\/[^\s'"<>})\]`]+/);
if (prMatch) {
refs.push({ file: filePath, raw: prMatch[0], type: 'project-root', line, key: keyPath });
}
// Check for {_bmad}/ refs
const bmMatch = value.match(/\{_bmad\}\/[^\s'"<>})\]`]+/);
if (bmMatch) {
refs.push({ file: filePath, raw: bmMatch[0], type: 'project-root', line, key: keyPath });
}
// Check for relative paths
const relMatch = value.match(/^\.\.?\/[^\s'"<>})\]`]+\.(?:md|yaml|yml|xml|json|csv|txt)$/);
if (relMatch) {
refs.push({ file: filePath, raw: relMatch[0], type: 'relative', line, key: keyPath });
}
}
function walkNode(node, keyPath) {
if (!node) return;
if (yaml.isMap(node)) {
for (const item of node.items) {
const key = item.key && item.key.value !== undefined ? item.key.value : '?';
const childPath = keyPath ? `${keyPath}.${key}` : String(key);
walkNode(item.value, childPath);
}
} else if (yaml.isSeq(node)) {
for (const [i, item] of node.items.entries()) {
walkNode(item, `${keyPath}[${i}]`);
}
} else if (yaml.isScalar(node)) {
checkValue(node.value, node.range, keyPath);
}
}
walkNode(doc.contents, '');
return refs;
}
function offsetToLine(content, offset) {
let line = 1;
for (let i = 0; i < offset && i < content.length; i++) {
if (content[i] === '\n') line++;
}
return line;
}
function extractMarkdownRefs(filePath, content) {
const refs = [];
const stripped = stripJsonExampleBlocks(stripCodeBlocks(content));
function runPattern(regex, type) {
regex.lastIndex = 0;
let match;
while ((match = regex.exec(stripped)) !== null) {
const raw = match[1];
if (!isResolvable(raw)) continue;
refs.push({ file: filePath, raw, type, line: offsetToLine(stripped, match.index) });
}
}
// {project-root}/_bmad/ refs
runPattern(PROJECT_ROOT_REF, 'project-root');
// {_bmad}/ shorthand
runPattern(BMAD_SHORTHAND_REF, 'project-root');
// exec="..." attributes
runPattern(EXEC_ATTR, 'exec-attr');
// <invoke-task> tags
runPattern(INVOKE_TASK, 'invoke-task');
// Step metadata
runPattern(STEP_META, 'relative');
// Load directives
runPattern(LOAD_DIRECTIVE, 'relative');
// Relative paths in quotes
runPattern(RELATIVE_PATH_QUOTED, 'relative');
runPattern(RELATIVE_PATH_DOT, 'relative');
return refs;
}
// --- Reference Resolution ---
function resolveRef(ref) {
if (ref.type === 'project-root') {
return mapInstalledToSource(ref.raw);
}
if (ref.type === 'relative') {
return path.resolve(path.dirname(ref.file), ref.raw);
}
if (ref.type === 'exec-attr') {
let execPath = ref.raw;
if (execPath.includes('{project-root}')) {
return mapInstalledToSource(execPath);
}
if (execPath.includes('{_bmad}')) {
return mapInstalledToSource(execPath);
}
if (execPath.startsWith('_bmad/')) {
return mapInstalledToSource(execPath);
}
// Relative exec path
return path.resolve(path.dirname(ref.file), execPath);
}
if (ref.type === 'invoke-task') {
// Extract file path from invoke-task content
const prMatch = ref.raw.match(/\{project-root\}\/_bmad\/([^\s'"<>})\]`]+)/);
if (prMatch) return mapInstalledToSource(prMatch[0]);
const bmMatch = ref.raw.match(/\{_bmad\}\/([^\s'"<>})\]`]+)/);
if (bmMatch) return mapInstalledToSource(bmMatch[0]);
const bareMatch = ref.raw.match(/_bmad\/([^\s'"<>})\]`]+)/);
if (bareMatch) return mapInstalledToSource(bareMatch[0]);
return null; // Can't resolve — skip
}
return null;
}
// --- Absolute Path Leak Detection ---
function checkAbsolutePathLeaks(filePath, content) {
const leaks = [];
const stripped = stripCodeBlocks(content);
const lines = stripped.split('\n');
for (const [i, line] of lines.entries()) {
if (ABS_PATH_LEAK.test(line)) {
leaks.push({ file: filePath, line: i + 1, content: line.trim() });
}
}
return leaks;
}
// --- Main ---
console.log(`\nValidating file references in: ${SRC_DIR}`);
console.log(`Mode: ${STRICT ? 'STRICT (exit 1 on issues)' : 'WARNING (exit 0)'}${VERBOSE ? ' + VERBOSE' : ''}\n`);
const files = getSourceFiles(SRC_DIR);
console.log(`Found ${files.length} source files\n`);
let totalRefs = 0;
let brokenRefs = 0;
let totalLeaks = 0;
let filesWithIssues = 0;
const allIssues = []; // Collect for $GITHUB_STEP_SUMMARY
for (const filePath of files) {
const relativePath = path.relative(PROJECT_ROOT, filePath);
const content = fs.readFileSync(filePath, 'utf-8');
const ext = path.extname(filePath);
// Extract references
let refs;
if (ext === '.yaml' || ext === '.yml') {
refs = extractYamlRefs(filePath, content);
} else {
refs = extractMarkdownRefs(filePath, content);
}
// Resolve and check
const broken = [];
if (VERBOSE && refs.length > 0) {
console.log(`\n${relativePath}`);
}
for (const ref of refs) {
totalRefs++;
const resolved = resolveRef(ref);
if (resolved && !fs.existsSync(resolved)) {
// For paths without extensions, also check if it's a directory
const hasExt = path.extname(resolved) !== '';
if (!hasExt) {
// Could be a directory reference — skip if not clearly a file
continue;
}
broken.push({ ref, resolved: path.relative(PROJECT_ROOT, resolved) });
brokenRefs++;
continue;
}
if (VERBOSE && resolved) {
console.log(` [OK] ${ref.raw}`);
}
}
// Check absolute path leaks
const leaks = checkAbsolutePathLeaks(filePath, content);
totalLeaks += leaks.length;
// Report issues for this file
if (broken.length > 0 || leaks.length > 0) {
filesWithIssues++;
if (!VERBOSE) {
console.log(`\n${relativePath}`);
}
for (const { ref, resolved } of broken) {
const location = ref.line ? `line ${ref.line}` : ref.key ? `key: ${ref.key}` : '';
console.log(` [BROKEN] ${ref.raw}${location ? ` (${location})` : ''}`);
console.log(` Target not found: ${resolved}`);
allIssues.push({ file: relativePath, line: ref.line || 1, ref: ref.raw, issue: 'broken ref' });
if (process.env.GITHUB_ACTIONS) {
const line = ref.line || 1;
console.log(`::warning file=${relativePath},line=${line}::${escapeAnnotation(`Broken reference: ${ref.raw}${resolved}`)}`);
}
}
for (const leak of leaks) {
console.log(` [ABS-PATH] Line ${leak.line}: ${leak.content}`);
allIssues.push({ file: relativePath, line: leak.line, ref: leak.content, issue: 'abs-path' });
if (process.env.GITHUB_ACTIONS) {
console.log(`::warning file=${relativePath},line=${leak.line}::${escapeAnnotation(`Absolute path leak: ${leak.content}`)}`);
}
}
}
}
// Summary
console.log(`\n${'─'.repeat(60)}`);
console.log(`\nSummary:`);
console.log(` Files scanned: ${files.length}`);
console.log(` References checked: ${totalRefs}`);
console.log(` Broken references: ${brokenRefs}`);
console.log(` Absolute path leaks: ${totalLeaks}`);
const hasIssues = brokenRefs > 0 || totalLeaks > 0;
if (hasIssues) {
console.log(`\n ${filesWithIssues} file(s) with issues`);
if (STRICT) {
console.log(`\n [STRICT MODE] Exiting with failure.`);
} else {
console.log(`\n Run with --strict to treat warnings as errors.`);
}
} else {
console.log(`\n All file references valid!`);
}
console.log('');
// Write GitHub Actions step summary
if (process.env.GITHUB_STEP_SUMMARY) {
let summary = '## File Reference Validation\n\n';
if (allIssues.length > 0) {
summary += '| File | Line | Reference | Issue |\n';
summary += '|------|------|-----------|-------|\n';
for (const issue of allIssues) {
summary += `| ${escapeTableCell(issue.file)} | ${issue.line} | ${escapeTableCell(issue.ref)} | ${issue.issue} |\n`;
}
summary += '\n';
}
summary += `**${files.length} files scanned, ${totalRefs} references checked, ${brokenRefs + totalLeaks} issues found**\n`;
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
}
process.exit(hasIssues && STRICT ? 1 : 0);

View File

@ -110,41 +110,7 @@ export default defineConfig({
collapsed: true, collapsed: true,
autogenerate: { directory: 'reference' }, autogenerate: { directory: 'reference' },
}, },
{ // TEA docs moved to standalone module site; keep BMM sidebar focused.
label: 'TEA - Testing in BMAD',
collapsed: true,
items: [
{
label: 'Tutorials',
autogenerate: { directory: 'tea/tutorials' },
},
{
label: 'How-To Guides',
items: [
{
label: 'Workflows',
autogenerate: { directory: 'tea/how-to/workflows' },
},
{
label: 'Customization',
autogenerate: { directory: 'tea/how-to/customization' },
},
{
label: 'Brownfield',
autogenerate: { directory: 'tea/how-to/brownfield' },
},
],
},
{
label: 'Explanation',
autogenerate: { directory: 'tea/explanation' },
},
{
label: 'Reference',
autogenerate: { directory: 'tea/reference' },
},
],
},
], ],
// Credits in footer // Credits in footer