diff --git a/.github/scripts/discord-helpers.sh b/.github/scripts/discord-helpers.sh
new file mode 100644
index 00000000..191b9037
--- /dev/null
+++ b/.github/scripts/discord-helpers.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+# Discord notification helper functions
+
+# Escape markdown special chars and @mentions for safe Discord display
+# Bracket expression: ] must be first, then other chars. In POSIX bracket expr, \ is literal.
+esc() { sed -e 's/[][\*_()~`>]/\\&/g' -e 's/@/@ /g'; }
+
+# Truncate to $1 chars (or 80 if wall-of-text with <3 spaces)
+trunc() {
+ local max=$1
+ local txt=$(tr '\n\r' ' ' | cut -c1-"$max")
+ local spaces=$(printf '%s' "$txt" | tr -cd ' ' | wc -c)
+ [ "$spaces" -lt 3 ] && [ ${#txt} -gt 80 ] && txt=$(printf '%s' "$txt" | cut -c1-80)
+ printf '%s' "$txt"
+}
diff --git a/.github/workflows/discord.yaml b/.github/workflows/discord.yaml
index 13316da7..109bbb16 100644
--- a/.github/workflows/discord.yaml
+++ b/.github/workflows/discord.yaml
@@ -1,16 +1,286 @@
name: Discord Notification
-"on": [pull_request, release, create, delete, issue_comment, pull_request_review, pull_request_review_comment]
+on:
+ pull_request:
+ types: [opened, closed, reopened, ready_for_review]
+ release:
+ types: [published]
+ create:
+ delete:
+ issue_comment:
+ types: [created]
+ pull_request_review:
+ types: [submitted]
+ pull_request_review_comment:
+ types: [created]
+ issues:
+ types: [opened, closed, reopened]
+
+env:
+ MAX_TITLE: 100
+ MAX_BODY: 250
jobs:
- notify:
+ pull_request:
+ if: github.event_name == 'pull_request'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.repository.default_branch }}
+ sparse-checkout: .github/scripts
+ sparse-checkout-cone-mode: false
+ - name: Notify Discord
+ env:
+ WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+ ACTION: ${{ github.event.action }}
+ MERGED: ${{ github.event.pull_request.merged }}
+ PR_NUM: ${{ github.event.pull_request.number }}
+ PR_URL: ${{ github.event.pull_request.html_url }}
+ PR_TITLE: ${{ github.event.pull_request.title }}
+ PR_USER: ${{ github.event.pull_request.user.login }}
+ PR_BODY: ${{ github.event.pull_request.body }}
+ run: |
+ set -o pipefail
+ source .github/scripts/discord-helpers.sh
+ [ -z "$WEBHOOK" ] && exit 0
+
+ if [ "$ACTION" = "opened" ]; then ICON="๐"; LABEL="New PR"
+ elif [ "$ACTION" = "closed" ] && [ "$MERGED" = "true" ]; then ICON="๐"; LABEL="Merged"
+ elif [ "$ACTION" = "closed" ]; then ICON="โ"; LABEL="Closed"
+ elif [ "$ACTION" = "reopened" ]; then ICON="๐"; LABEL="Reopened"
+ else ICON="๐"; LABEL="Ready"; fi
+
+ TITLE=$(printf '%s' "$PR_TITLE" | trunc $MAX_TITLE | esc)
+ [ ${#PR_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..."
+ BODY=$(printf '%s' "$PR_BODY" | trunc $MAX_BODY | esc)
+ [ -n "$PR_BODY" ] && [ ${#PR_BODY} -gt $MAX_BODY ] && BODY="${BODY}..."
+ [ -n "$BODY" ] && BODY=" ยท $BODY"
+ USER=$(printf '%s' "$PR_USER" | esc)
+
+ MSG="$ICON **[$LABEL #$PR_NUM: $TITLE](<$PR_URL>)**"$'\n'"by @$USER$BODY"
+ jq -n --arg content "$MSG" '{content: $content}' | curl -sf --retry 2 -X POST "$WEBHOOK" -H "Content-Type: application/json" -d @-
+
+ issues:
+ if: github.event_name == 'issues'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.repository.default_branch }}
+ sparse-checkout: .github/scripts
+ sparse-checkout-cone-mode: false
+ - name: Notify Discord
+ env:
+ WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+ ACTION: ${{ github.event.action }}
+ ISSUE_NUM: ${{ github.event.issue.number }}
+ ISSUE_URL: ${{ github.event.issue.html_url }}
+ ISSUE_TITLE: ${{ github.event.issue.title }}
+ ISSUE_USER: ${{ github.event.issue.user.login }}
+ ISSUE_BODY: ${{ github.event.issue.body }}
+ ACTOR: ${{ github.actor }}
+ run: |
+ set -o pipefail
+ source .github/scripts/discord-helpers.sh
+ [ -z "$WEBHOOK" ] && exit 0
+
+ if [ "$ACTION" = "opened" ]; then ICON="๐"; LABEL="New Issue"; USER="$ISSUE_USER"
+ elif [ "$ACTION" = "closed" ]; then ICON="โ
"; LABEL="Closed"; USER="$ACTOR"
+ else ICON="๐"; LABEL="Reopened"; USER="$ACTOR"; fi
+
+ TITLE=$(printf '%s' "$ISSUE_TITLE" | trunc $MAX_TITLE | esc)
+ [ ${#ISSUE_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..."
+ BODY=$(printf '%s' "$ISSUE_BODY" | trunc $MAX_BODY | esc)
+ [ -n "$ISSUE_BODY" ] && [ ${#ISSUE_BODY} -gt $MAX_BODY ] && BODY="${BODY}..."
+ [ -n "$BODY" ] && BODY=" ยท $BODY"
+ USER=$(printf '%s' "$USER" | esc)
+
+ MSG="$ICON **[$LABEL #$ISSUE_NUM: $TITLE](<$ISSUE_URL>)**"$'\n'"by @$USER$BODY"
+ jq -n --arg content "$MSG" '{content: $content}' | curl -sf --retry 2 -X POST "$WEBHOOK" -H "Content-Type: application/json" -d @-
+
+ issue_comment:
+ if: github.event_name == 'issue_comment'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.repository.default_branch }}
+ sparse-checkout: .github/scripts
+ sparse-checkout-cone-mode: false
+ - name: Notify Discord
+ env:
+ WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+ IS_PR: ${{ github.event.issue.pull_request && 'true' || 'false' }}
+ ISSUE_NUM: ${{ github.event.issue.number }}
+ ISSUE_TITLE: ${{ github.event.issue.title }}
+ COMMENT_URL: ${{ github.event.comment.html_url }}
+ COMMENT_USER: ${{ github.event.comment.user.login }}
+ COMMENT_BODY: ${{ github.event.comment.body }}
+ run: |
+ set -o pipefail
+ source .github/scripts/discord-helpers.sh
+ [ -z "$WEBHOOK" ] && exit 0
+
+ [ "$IS_PR" = "true" ] && TYPE="PR" || TYPE="Issue"
+
+ TITLE=$(printf '%s' "$ISSUE_TITLE" | trunc $MAX_TITLE | esc)
+ [ ${#ISSUE_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..."
+ BODY=$(printf '%s' "$COMMENT_BODY" | trunc $MAX_BODY | esc)
+ [ ${#COMMENT_BODY} -gt $MAX_BODY ] && BODY="${BODY}..."
+ USER=$(printf '%s' "$COMMENT_USER" | esc)
+
+ MSG="๐ฌ **[Comment on $TYPE #$ISSUE_NUM: $TITLE](<$COMMENT_URL>)**"$'\n'"@$USER: $BODY"
+ jq -n --arg content "$MSG" '{content: $content}' | curl -sf --retry 2 -X POST "$WEBHOOK" -H "Content-Type: application/json" -d @-
+
+ pull_request_review:
+ if: github.event_name == 'pull_request_review'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.repository.default_branch }}
+ sparse-checkout: .github/scripts
+ sparse-checkout-cone-mode: false
+ - name: Notify Discord
+ env:
+ WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+ STATE: ${{ github.event.review.state }}
+ PR_NUM: ${{ github.event.pull_request.number }}
+ PR_TITLE: ${{ github.event.pull_request.title }}
+ REVIEW_URL: ${{ github.event.review.html_url }}
+ REVIEW_USER: ${{ github.event.review.user.login }}
+ REVIEW_BODY: ${{ github.event.review.body }}
+ run: |
+ set -o pipefail
+ source .github/scripts/discord-helpers.sh
+ [ -z "$WEBHOOK" ] && exit 0
+
+ if [ "$STATE" = "approved" ]; then ICON="โ
"; LABEL="Approved"
+ elif [ "$STATE" = "changes_requested" ]; then ICON="๐ง"; LABEL="Changes Requested"
+ else ICON="๐"; LABEL="Reviewed"; fi
+
+ TITLE=$(printf '%s' "$PR_TITLE" | trunc $MAX_TITLE | esc)
+ [ ${#PR_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..."
+ BODY=$(printf '%s' "$REVIEW_BODY" | trunc $MAX_BODY | esc)
+ [ -n "$REVIEW_BODY" ] && [ ${#REVIEW_BODY} -gt $MAX_BODY ] && BODY="${BODY}..."
+ [ -n "$BODY" ] && BODY=": $BODY"
+ USER=$(printf '%s' "$REVIEW_USER" | esc)
+
+ MSG="$ICON **[$LABEL PR #$PR_NUM: $TITLE](<$REVIEW_URL>)**"$'\n'"@$USER$BODY"
+ jq -n --arg content "$MSG" '{content: $content}' | curl -sf --retry 2 -X POST "$WEBHOOK" -H "Content-Type: application/json" -d @-
+
+ pull_request_review_comment:
+ if: github.event_name == 'pull_request_review_comment'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.repository.default_branch }}
+ sparse-checkout: .github/scripts
+ sparse-checkout-cone-mode: false
+ - name: Notify Discord
+ env:
+ WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+ PR_NUM: ${{ github.event.pull_request.number }}
+ PR_TITLE: ${{ github.event.pull_request.title }}
+ COMMENT_URL: ${{ github.event.comment.html_url }}
+ COMMENT_USER: ${{ github.event.comment.user.login }}
+ COMMENT_BODY: ${{ github.event.comment.body }}
+ run: |
+ set -o pipefail
+ source .github/scripts/discord-helpers.sh
+ [ -z "$WEBHOOK" ] && exit 0
+
+ TITLE=$(printf '%s' "$PR_TITLE" | trunc $MAX_TITLE | esc)
+ [ ${#PR_TITLE} -gt $MAX_TITLE ] && TITLE="${TITLE}..."
+ BODY=$(printf '%s' "$COMMENT_BODY" | trunc $MAX_BODY | esc)
+ [ ${#COMMENT_BODY} -gt $MAX_BODY ] && BODY="${BODY}..."
+ USER=$(printf '%s' "$COMMENT_USER" | esc)
+
+ MSG="๐ญ **[Review Comment PR #$PR_NUM: $TITLE](<$COMMENT_URL>)**"$'\n'"@$USER: $BODY"
+ jq -n --arg content "$MSG" '{content: $content}' | curl -sf --retry 2 -X POST "$WEBHOOK" -H "Content-Type: application/json" -d @-
+
+ release:
+ if: github.event_name == 'release'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.repository.default_branch }}
+ sparse-checkout: .github/scripts
+ sparse-checkout-cone-mode: false
+ - name: Notify Discord
+ env:
+ WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+ TAG: ${{ github.event.release.tag_name }}
+ NAME: ${{ github.event.release.name }}
+ URL: ${{ github.event.release.html_url }}
+ RELEASE_BODY: ${{ github.event.release.body }}
+ run: |
+ set -o pipefail
+ source .github/scripts/discord-helpers.sh
+ [ -z "$WEBHOOK" ] && exit 0
+
+ REL_NAME=$(printf '%s' "$NAME" | trunc $MAX_TITLE | esc)
+ [ ${#NAME} -gt $MAX_TITLE ] && REL_NAME="${REL_NAME}..."
+ BODY=$(printf '%s' "$RELEASE_BODY" | trunc $MAX_BODY | esc)
+ [ -n "$RELEASE_BODY" ] && [ ${#RELEASE_BODY} -gt $MAX_BODY ] && BODY="${BODY}..."
+ [ -n "$BODY" ] && BODY=" ยท $BODY"
+ TAG_ESC=$(printf '%s' "$TAG" | esc)
+
+ MSG="๐ **[Release $TAG_ESC: $REL_NAME](<$URL>)**"$'\n'"$BODY"
+ jq -n --arg content "$MSG" '{content: $content}' | curl -sf --retry 2 -X POST "$WEBHOOK" -H "Content-Type: application/json" -d @-
+
+ create:
+ if: github.event_name == 'create'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.repository.default_branch }}
+ sparse-checkout: .github/scripts
+ sparse-checkout-cone-mode: false
+ - name: Notify Discord
+ env:
+ WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+ REF_TYPE: ${{ github.event.ref_type }}
+ REF: ${{ github.event.ref }}
+ ACTOR: ${{ github.actor }}
+ REPO_URL: ${{ github.event.repository.html_url }}
+ run: |
+ set -o pipefail
+ source .github/scripts/discord-helpers.sh
+ [ -z "$WEBHOOK" ] && exit 0
+
+ [ "$REF_TYPE" = "branch" ] && ICON="๐ฟ" || ICON="๐ท๏ธ"
+ REF_TRUNC=$(printf '%s' "$REF" | trunc $MAX_TITLE)
+ [ ${#REF} -gt $MAX_TITLE ] && REF_TRUNC="${REF_TRUNC}..."
+ REF_ESC=$(printf '%s' "$REF_TRUNC" | esc)
+ REF_URL=$(jq -rn --arg ref "$REF" '$ref | @uri')
+ ACTOR_ESC=$(printf '%s' "$ACTOR" | esc)
+ MSG="$ICON **${REF_TYPE^} created: [$REF_ESC](<$REPO_URL/tree/$REF_URL>)** by @$ACTOR_ESC"
+ jq -n --arg content "$MSG" '{content: $content}' | curl -sf --retry 2 -X POST "$WEBHOOK" -H "Content-Type: application/json" -d @-
+
+ delete:
+ if: github.event_name == 'delete'
runs-on: ubuntu-latest
steps:
- name: Notify Discord
- uses: sarisia/actions-status-discord@v1
- if: always()
- with:
- webhook: ${{ secrets.DISCORD_WEBHOOK }}
- status: ${{ job.status }}
- title: "Triggered by ${{ github.event_name }}"
- color: 0x5865F2
+ env:
+ WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+ REF_TYPE: ${{ github.event.ref_type }}
+ REF: ${{ github.event.ref }}
+ ACTOR: ${{ github.actor }}
+ run: |
+ set -o pipefail
+ [ -z "$WEBHOOK" ] && exit 0
+ esc() { sed -e 's/[][\*_()~`>]/\\&/g' -e 's/@/@ /g'; }
+ trunc() { tr '\n\r' ' ' | cut -c1-"$1"; }
+
+ REF_TRUNC=$(printf '%s' "$REF" | trunc 100)
+ [ ${#REF} -gt 100 ] && REF_TRUNC="${REF_TRUNC}..."
+ REF_ESC=$(printf '%s' "$REF_TRUNC" | esc)
+ ACTOR_ESC=$(printf '%s' "$ACTOR" | esc)
+ MSG="๐๏ธ **${REF_TYPE^} deleted: $REF_ESC** by @$ACTOR_ESC"
+ jq -n --arg content "$MSG" '{content: $content}' | curl -sf --retry 2 -X POST "$WEBHOOK" -H "Content-Type: application/json" -d @-
diff --git a/.gitignore b/.gitignore
index cef2ce1a..47a82e6e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -70,4 +70,6 @@ z*/
.codex
.github/chatmodes
.agent
-.agentvibes/
\ No newline at end of file
+.agentvibes/
+.kiro/
+.roo
diff --git a/.prettierignore b/.prettierignore
index 24d5d69f..0d37dfb9 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,6 +1,9 @@
# Test fixtures with intentionally broken/malformed files
test/fixtures/**
+# Contributor Covenant (external standard)
+CODE_OF_CONDUCT.md
+
# BMAD runtime folders (user-specific, not in repo)
.bmad/
.bmad*/
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..27b04993
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+the official BMAD Discord server (https://discord.com/invite/gk8jAdXWmj) - DM a moderator or flag a post.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/src/core/workflows/brainstorming/workflow.md b/src/core/workflows/brainstorming/workflow.md
index f2793fe7..156a9bb5 100644
--- a/src/core/workflows/brainstorming/workflow.md
+++ b/src/core/workflows/brainstorming/workflow.md
@@ -1,5 +1,5 @@
---
-name: Brainstorming Session
+name: brainstorming-session
description: Facilitate interactive brainstorming sessions using diverse creative techniques and ideation methods
context_file: '' # Optional context file path for project-specific guidance
---
diff --git a/src/core/workflows/party-mode/workflow.md b/src/core/workflows/party-mode/workflow.md
index 26d7a507..b3147ad0 100644
--- a/src/core/workflows/party-mode/workflow.md
+++ b/src/core/workflows/party-mode/workflow.md
@@ -1,5 +1,5 @@
---
-name: Party Mode
+name: party-mode
description: Orchestrates group discussions between all installed BMAD agents, enabling natural multi-agent conversations
---
diff --git a/src/modules/bmb/workflows/create-agent/workflow.md b/src/modules/bmb/workflows/create-agent/workflow.md
index 0893ff68..503df318 100644
--- a/src/modules/bmb/workflows/create-agent/workflow.md
+++ b/src/modules/bmb/workflows/create-agent/workflow.md
@@ -1,5 +1,5 @@
---
-name: Create Agent
+name: create-agent
description: Interactive workflow to build BMAD Core compliant agents with optional brainstorming, persona development, and command structure
web_bundle: true
---
diff --git a/src/modules/bmb/workflows/create-workflow/workflow.md b/src/modules/bmb/workflows/create-workflow/workflow.md
index 55d80e94..6b4140d5 100644
--- a/src/modules/bmb/workflows/create-workflow/workflow.md
+++ b/src/modules/bmb/workflows/create-workflow/workflow.md
@@ -1,5 +1,5 @@
---
-name: Create Workflow
+name: create-workflow
description: Create structured standalone workflows using markdown-based step architecture
web_bundle: true
---
diff --git a/src/modules/bmb/workflows/edit-agent/workflow.md b/src/modules/bmb/workflows/edit-agent/workflow.md
index 0c7927fd..81462cbb 100644
--- a/src/modules/bmb/workflows/edit-agent/workflow.md
+++ b/src/modules/bmb/workflows/edit-agent/workflow.md
@@ -1,5 +1,5 @@
---
-name: Edit Agent
+name: edit-agent
description: Edit existing BMAD agents while following all best practices and conventions
web_bundle: false
---
diff --git a/src/modules/bmb/workflows/edit-workflow/workflow.md b/src/modules/bmb/workflows/edit-workflow/workflow.md
index 9a275bc3..d4d62f96 100644
--- a/src/modules/bmb/workflows/edit-workflow/workflow.md
+++ b/src/modules/bmb/workflows/edit-workflow/workflow.md
@@ -1,5 +1,5 @@
---
-name: Edit Workflow
+name: edit-workflow
description: Intelligent workflow editor that helps modify existing workflows while following best practices
web_bundle: true
---
diff --git a/src/modules/bmb/workflows/workflow-compliance-check/workflow.md b/src/modules/bmb/workflows/workflow-compliance-check/workflow.md
index 049366b4..2fb39bd2 100644
--- a/src/modules/bmb/workflows/workflow-compliance-check/workflow.md
+++ b/src/modules/bmb/workflows/workflow-compliance-check/workflow.md
@@ -1,5 +1,5 @@
---
-name: Workflow Compliance Check
+name: workflow-compliance-check
description: Systematic validation of workflows against BMAD standards with adversarial analysis and detailed reporting
web_bundle: false
---
diff --git a/src/modules/bmm/docs/workflows-implementation.md b/src/modules/bmm/docs/workflows-implementation.md
index 7d756097..39d6d591 100644
--- a/src/modules/bmm/docs/workflows-implementation.md
+++ b/src/modules/bmm/docs/workflows-implementation.md
@@ -108,7 +108,8 @@ Stories move through these states in the sprint status file:
**As Needed:**
-- Run `workflow-status` anytime to check progress
+- Run `sprint-status` anytime in Phase 4 to inspect sprint-status.yaml and get the next implementation command
+- Run `workflow-status` for cross-phase routing and project-level paths
- Run `correct-course` if significant changes needed
---
@@ -155,7 +156,7 @@ PRD (PM) โ Architecture (Architect)
## Troubleshooting
**Q: Which workflow should I run next?**
-A: Run `workflow-status` - it reads the sprint status file and tells you exactly what to do.
+A: Run `workflow-status` - it reads the sprint status file and tells you exactly what to do. During implementation (Phase 4) run `sprint-status` (fast check against sprint-status.yaml).
**Q: Story needs significant changes mid-implementation?**
A: Run `correct-course` to analyze impact and route appropriately.
diff --git a/src/modules/bmm/workflows/1-analysis/product-brief/workflow.md b/src/modules/bmm/workflows/1-analysis/product-brief/workflow.md
index d2a7ab71..a070d3ce 100644
--- a/src/modules/bmm/workflows/1-analysis/product-brief/workflow.md
+++ b/src/modules/bmm/workflows/1-analysis/product-brief/workflow.md
@@ -1,5 +1,5 @@
---
-name: Product Brief Workflow
+name: create-product-brief
description: Create comprehensive product briefs through collaborative step-by-step discovery as creative Business Analyst working with the user as peers.
web_bundle: true
---
diff --git a/src/modules/bmm/workflows/1-analysis/research/workflow.md b/src/modules/bmm/workflows/1-analysis/research/workflow.md
index 8ca1ea3e..cbbacfd9 100644
--- a/src/modules/bmm/workflows/1-analysis/research/workflow.md
+++ b/src/modules/bmm/workflows/1-analysis/research/workflow.md
@@ -1,6 +1,7 @@
---
-name: Research Workflow
+name: research
description: Conduct comprehensive research across multiple domains using current web data and verified sources - Market, Technical, Domain and other research types.
+web_bundle: true
---
# Research Workflow
diff --git a/src/modules/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md b/src/modules/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md
index 03514f8d..1810e94d 100644
--- a/src/modules/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md
+++ b/src/modules/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md
@@ -1,3 +1,9 @@
+---
+name: create-ux-design
+description: Work with a peer UX Design expert to plan your applications UX patterns, look and feel.
+web_bundle: true
+---
+
# Create UX Design Workflow
**Goal:** Create comprehensive UX design specifications through collaborative visual exploration and informed decision-making where you act as a UX facilitator working with a product stakeholder.
diff --git a/src/modules/bmm/workflows/2-plan-workflows/prd/workflow.md b/src/modules/bmm/workflows/2-plan-workflows/prd/workflow.md
index 6cee6a80..224f24fe 100644
--- a/src/modules/bmm/workflows/2-plan-workflows/prd/workflow.md
+++ b/src/modules/bmm/workflows/2-plan-workflows/prd/workflow.md
@@ -1,7 +1,7 @@
---
-name: PRD Workflow
+name: create-prd
description: Creates a comprehensive PRDs through collaborative step-by-step discovery between two product managers working as peers.
-main_config: `{project-root}/{bmad_folder}/bmm/config.yaml`
+main_config: '{project-root}/{bmad_folder}/bmm/config.yaml'
web_bundle: true
---
diff --git a/src/modules/bmm/workflows/3-solutioning/architecture/workflow.md b/src/modules/bmm/workflows/3-solutioning/architecture/workflow.md
index b59b48e2..7d5deeb7 100644
--- a/src/modules/bmm/workflows/3-solutioning/architecture/workflow.md
+++ b/src/modules/bmm/workflows/3-solutioning/architecture/workflow.md
@@ -1,6 +1,7 @@
---
-name: Architecture Workflow
+name: create-architecture
description: Collaborative architectural decision facilitation for AI-agent consistency. Replaces template-driven architecture with intelligent, adaptive conversation that produces a decision-focused architecture document optimized for preventing agent conflicts.
+web_bundle: true
---
# Architecture Workflow
diff --git a/src/modules/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md b/src/modules/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md
index 2590627a..2975980a 100644
--- a/src/modules/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md
+++ b/src/modules/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md
@@ -1,5 +1,5 @@
---
-name: 'Create Epics and Stories'
+name: create-epics-stories
description: 'Transform PRD requirements and Architecture decisions into comprehensive stories organized by user value. This workflow requires completed PRD + Architecture documents (UX recommended if UI exists) and breaks down requirements into implementation-ready epics and user stories that incorporate all available technical and design context. Creates detailed, actionable stories with complete acceptance criteria for development teams.'
web_bundle: true
---
diff --git a/src/modules/bmm/workflows/3-solutioning/implementation-readiness/workflow.md b/src/modules/bmm/workflows/3-solutioning/implementation-readiness/workflow.md
index 989b659d..2483cde8 100644
--- a/src/modules/bmm/workflows/3-solutioning/implementation-readiness/workflow.md
+++ b/src/modules/bmm/workflows/3-solutioning/implementation-readiness/workflow.md
@@ -1,5 +1,5 @@
---
-name: 'Implementation Readiness'
+name: check-implementation-readiness
description: 'Critical validation workflow that assesses PRD, Architecture, and Epics & Stories for completeness and alignment before implementation. Uses adversarial review approach to find gaps and issues.'
web_bundle: false
---
diff --git a/src/modules/bmm/workflows/4-implementation/sprint-status/instructions.md b/src/modules/bmm/workflows/4-implementation/sprint-status/instructions.md
new file mode 100644
index 00000000..84a92ea4
--- /dev/null
+++ b/src/modules/bmm/workflows/4-implementation/sprint-status/instructions.md
@@ -0,0 +1,174 @@
+# Sprint Status - Multi-Mode Service
+
+The workflow execution engine is governed by: {project-root}/{bmad_folder}/core/tasks/workflow.xml
+You MUST have already loaded and processed: {project-root}/{bmad_folder}/bmm/workflows/4-implementation/sprint-status/workflow.yaml
+Modes: interactive (default), validate, data
+โ ๏ธ ABSOLUTELY NO TIME ESTIMATES. Do NOT mention hours, days, weeks, or timelines.
+
+
+
+
+ Set mode = {{mode}} if provided by caller; otherwise mode = "interactive"
+
+
+ Jump to Step 20
+
+
+
+ Jump to Step 30
+
+
+
+ Continue to Step 1
+
+
+
+
+ Try {sprint_status_file}
+
+
+ Exit workflow
+
+ Continue to Step 2
+
+
+
+ Read the FULL file: {sprint_status_file}
+ Parse fields: generated, project, project_key, tracking_system, story_location
+ Parse development_status map. Classify keys:
+ - Epics: keys starting with "epic-" (and not ending with "-retrospective")
+ - Retrospectives: keys ending with "-retrospective"
+ - Stories: everything else (e.g., 1-2-login-form)
+ Count story statuses: backlog, drafted, ready-for-dev, in-progress, review, done
+ Count epic statuses: backlog, contexted
+ Detect risks:
+ - Stories in review but no reviewer assigned context โ suggest `/bmad:bmm:workflows:code-review`
+ - Stories in in-progress with no ready-for-dev items behind them โ keep focus on the active story
+ - All epics backlog/contexted but no stories drafted โ prompt to run `/bmad:bmm:workflows:create-story`
+
+
+
+ Pick the next recommended workflow using priority:
+ 1. If any story status == in-progress โ recommend `dev-story` for the first in-progress story
+ 2. Else if any story status == review โ recommend `code-review` for the first review story
+ 3. Else if any story status == ready-for-dev โ recommend `dev-story`
+ 4. Else if any story status == drafted โ recommend `story-ready`
+ 5. Else if any story status == backlog โ recommend `create-story`
+ 6. Else if any epic status == backlog โ recommend `epic-tech-context`
+ 7. Else if retrospectives are optional โ recommend `retrospective`
+ 8. Else โ All implementation items done; suggest `workflow-status` to plan next phase
+ Store selected recommendation as: next_story_id, next_workflow_id, next_agent (SM/DEV as appropriate)
+
+
+
+
+
+
+
+ Pick an option:
+1) Run recommended workflow now
+2) Show all stories grouped by status
+3) Show raw sprint-status.yaml
+4) Exit
+Choice:
+
+
+
+
+
+
+
+
+
+
+ Display the full contents of {sprint_status_file}
+
+
+
+ Exit workflow
+
+
+
+
+
+
+
+
+ Load and parse {sprint_status_file} same as Step 2
+ Compute recommendation same as Step 3
+ next_workflow_id = {{next_workflow_id}}
+ next_story_id = {{next_story_id}}
+ count_backlog = {{count_backlog}}
+ count_drafted = {{count_drafted}}
+ count_ready = {{count_ready}}
+ count_in_progress = {{count_in_progress}}
+ count_review = {{count_review}}
+ count_done = {{count_done}}
+ epic_backlog = {{epic_backlog}}
+ epic_contexted = {{epic_contexted}}
+ warnings = {{risks}}
+ Return to caller
+
+
+
+
+
+
+
+ Check that {sprint_status_file} exists
+
+ is_valid = false
+ error = "sprint-status.yaml missing"
+ suggestion = "Run sprint-planning to create it"
+ Return
+
+ Read file and verify it has a development_status section with at least one entry
+
+ is_valid = false
+ error = "development_status missing or empty"
+ suggestion = "Re-run sprint-planning or repair the file manually"
+ Return
+
+ is_valid = true
+ message = "sprint-status.yaml present and parsable"
+
+
+
diff --git a/src/modules/bmm/workflows/4-implementation/sprint-status/workflow.yaml b/src/modules/bmm/workflows/4-implementation/sprint-status/workflow.yaml
new file mode 100644
index 00000000..45a4d105
--- /dev/null
+++ b/src/modules/bmm/workflows/4-implementation/sprint-status/workflow.yaml
@@ -0,0 +1,35 @@
+# Sprint Status - Implementation Tracker
+name: sprint-status
+description: "Summarize sprint-status.yaml, surface risks, and route to the right implementation workflow."
+author: "BMad"
+
+# Critical variables from config
+config_source: "{project-root}/{bmad_folder}/bmm/config.yaml"
+output_folder: "{config_source}:output_folder"
+user_name: "{config_source}:user_name"
+communication_language: "{config_source}:communication_language"
+document_output_language: "{config_source}:document_output_language"
+date: system-generated
+sprint_artifacts: "{config_source}:sprint_artifacts"
+
+# Workflow components
+installed_path: "{project-root}/{bmad_folder}/bmm/workflows/4-implementation/sprint-status"
+instructions: "{installed_path}/instructions.md"
+
+# Inputs
+variables:
+ sprint_status_file: "{sprint_artifacts}/sprint-status.yaml || {output_folder}/sprint-status.yaml"
+ tracking_system: "file-system"
+
+# Smart input file references
+input_file_patterns:
+ sprint_status:
+ description: "Sprint status file generated by sprint-planning"
+ whole: "{sprint_artifacts}/sprint-status.yaml || {output_folder}/sprint-status.yaml"
+ load_strategy: "FULL_LOAD"
+
+# Standalone so IDE commands get generated
+standalone: true
+
+# No web bundle needed
+web_bundle: false
diff --git a/src/modules/bmm/workflows/bmad-quick-flow/quick-dev/instructions.md b/src/modules/bmm/workflows/bmad-quick-flow/quick-dev/instructions.md
index b2b8a331..b1635173 100644
--- a/src/modules/bmm/workflows/bmad-quick-flow/quick-dev/instructions.md
+++ b/src/modules/bmm/workflows/bmad-quick-flow/quick-dev/instructions.md
@@ -32,18 +32,115 @@
- **[t] Plan first** - Create tech-spec then implement
+
+
+
+Evaluate escalation threshold against user input (minimal tokens, no file loading):
+
+**Triggers escalation** (if 2+ signals present):
+
+- Multiple components mentioned (e.g., dashboard + api + database)
+- System-level language (e.g., platform, integration, architecture)
+- Uncertainty about approach (e.g., "how should I", "best way to")
+- Multi-layer scope (e.g., UI + backend + data together)
+- Extended timeframe (e.g., "this week", "over the next few days")
+
+**Reduces signal:**
+
+- Simplicity markers (e.g., "just", "quickly", "fix", "bug", "typo", "simple", "basic", "minor")
+- Single file/component focus
+- Confident, specific request
+
+Use holistic judgment, not mechanical keyword matching.
+
+
+
+ **[t] Plan first** - Create tech-spec then implement
**[e] Execute directly** - Start now
-
- Load and execute {create_tech_spec_workflow}
- Continue to implementation after spec complete
+
+ Load and execute {create_tech_spec_workflow}
+ Continue to implementation after spec complete
+
+
+
+ Any additional guidance before I begin? (patterns, files, constraints) Or "go" to start.
+ step_2
+
+
-
- Any additional guidance before I begin? (patterns, files, constraints) Or "go" to start.
- step_2
+
+
+ Load {project_levels} and evaluate user input against detection_hints.keywords
+ Determine level (0-4) using scale-adaptive definitions
+
+
+
+ **[t] Plan first** - Create tech-spec then implement
+
+**[e] Execute directly** - Start now
+
+
+ Load and execute {create_tech_spec_workflow}
+ Continue to implementation after spec complete
+
+
+
+ Any additional guidance before I begin? (patterns, files, constraints) Or "go" to start.
+ step_2
+
+
+
+
+ This looks like a focused feature with multiple components.
+
+**[t] Create tech-spec first** (recommended)
+**[w] Seems bigger than quick-dev** โ see what BMad Method recommends (workflow-init)
+**[e] Execute directly**
+
+
+ Load and execute {create_tech_spec_workflow}
+ Continue to implementation after spec complete
+
+
+
+ Load and execute {workflow_init}
+ EXIT quick-dev - user has been routed to BMad Method
+
+
+
+ Any additional guidance before I begin? (patterns, files, constraints) Or "go" to start.
+ step_2
+
+
+
+
+
+ This sounds like platform/system work.
+
+**[w] Start BMad Method** (recommended) (workflow-init)
+**[t] Create tech-spec** (lighter planning)
+**[e] Execute directly** - feeling lucky
+
+
+ Load and execute {workflow_init}
+ EXIT quick-dev - user has been routed to BMad Method
+
+
+
+ Load and execute {create_tech_spec_workflow}
+ Continue to implementation after spec complete
+
+
+
+ Any additional guidance before I begin? (patterns, files, constraints) Or "go" to start.
+ step_2
+
+
+
+
diff --git a/src/modules/bmm/workflows/bmad-quick-flow/quick-dev/workflow.yaml b/src/modules/bmm/workflows/bmad-quick-flow/quick-dev/workflow.yaml
index 3f9ca77e..7c2de639 100644
--- a/src/modules/bmm/workflows/bmad-quick-flow/quick-dev/workflow.yaml
+++ b/src/modules/bmm/workflows/bmad-quick-flow/quick-dev/workflow.yaml
@@ -25,5 +25,9 @@ create_tech_spec_workflow: "{project-root}/{bmad_folder}/bmm/workflows/bmad-quic
party_mode_exec: "{project-root}/{bmad_folder}/core/workflows/party-mode/workflow.md"
advanced_elicitation: "{project-root}/{bmad_folder}/core/tasks/advanced-elicitation.xml"
+# Routing resources (lazy-loaded)
+project_levels: "{project-root}/{bmad_folder}/bmm/workflows/workflow-status/project-levels.yaml"
+workflow_init: "{project-root}/{bmad_folder}/bmm/workflows/workflow-status/init/workflow.yaml"
+
standalone: true
web_bundle: false
diff --git a/src/modules/bmm/workflows/generate-project-context/workflow.md b/src/modules/bmm/workflows/generate-project-context/workflow.md
index a9c463e9..934ebff9 100644
--- a/src/modules/bmm/workflows/generate-project-context/workflow.md
+++ b/src/modules/bmm/workflows/generate-project-context/workflow.md
@@ -1,5 +1,5 @@
---
-name: Generate Project Context
+name: generate-project-context
description: Creates a concise project_context.md file with critical rules and patterns that AI agents must follow when implementing code. Optimized for LLM context efficiency.
---
diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js
index 1dbb8ea6..644fd494 100644
--- a/tools/cli/installers/lib/core/manifest-generator.js
+++ b/tools/cli/installers/lib/core/manifest-generator.js
@@ -105,7 +105,7 @@ class ManifestGenerator {
}
/**
- * Recursively find and parse workflow.yaml files
+ * Recursively find and parse workflow.yaml and workflow.md files
*/
async getWorkflowsFromPath(basePath, moduleName) {
const workflows = [];
@@ -126,11 +126,23 @@ class ManifestGenerator {
// Recurse into subdirectories
const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
await findWorkflows(fullPath, newRelativePath);
- } else if (entry.name === 'workflow.yaml') {
- // Parse workflow file
+ } else if (entry.name === 'workflow.yaml' || entry.name === 'workflow.md') {
+ // Parse workflow file (both YAML and MD formats)
try {
const content = await fs.readFile(fullPath, 'utf8');
- const workflow = yaml.load(content);
+
+ let workflow;
+ if (entry.name === 'workflow.yaml') {
+ // Parse YAML workflow
+ workflow = yaml.load(content);
+ } else {
+ // Parse MD workflow with YAML frontmatter
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
+ if (!frontmatterMatch) {
+ continue; // Skip MD files without frontmatter
+ }
+ workflow = yaml.load(frontmatterMatch[1]);
+ }
// Skip template workflows (those with placeholder values)
if (workflow.name && workflow.name.includes('{') && workflow.name.includes('}')) {
@@ -141,18 +153,15 @@ class ManifestGenerator {
// Build relative path for installation
const installPath =
moduleName === 'core'
- ? `${this.bmadFolderName}/core/workflows/${relativePath}/workflow.yaml`
- : `${this.bmadFolderName}/${moduleName}/workflows/${relativePath}/workflow.yaml`;
-
- // Check for standalone property (default: false)
- const standalone = workflow.standalone === true;
+ ? `${this.bmadFolderName}/core/workflows/${relativePath}/${entry.name}`
+ : `${this.bmadFolderName}/${moduleName}/workflows/${relativePath}/${entry.name}`;
+ // ALL workflows now generate commands - no standalone property needed
workflows.push({
name: workflow.name,
description: workflow.description.replaceAll('"', '""'), // Escape quotes for CSV
module: moduleName,
path: installPath,
- standalone: standalone,
});
// Add to files list
@@ -541,12 +550,12 @@ class ManifestGenerator {
async writeWorkflowManifest(cfgDir) {
const csvPath = path.join(cfgDir, 'workflow-manifest.csv');
- // Create CSV header with standalone column
- let csv = 'name,description,module,path,standalone\n';
+ // Create CSV header - removed standalone column as ALL workflows now generate commands
+ let csv = 'name,description,module,path\n';
- // Add all workflows
+ // Add all workflows - no standalone property needed anymore
for (const workflow of this.workflows) {
- csv += `"${workflow.name}","${workflow.description}","${workflow.module}","${workflow.path}","${workflow.standalone}"\n`;
+ csv += `"${workflow.name}","${workflow.description}","${workflow.module}","${workflow.path}"\n`;
}
await fs.writeFile(csvPath, csv);
diff --git a/tools/cli/installers/lib/ide/auggie.js b/tools/cli/installers/lib/ide/auggie.js
index 24b809ca..04e08788 100644
--- a/tools/cli/installers/lib/ide/auggie.js
+++ b/tools/cli/installers/lib/ide/auggie.js
@@ -3,6 +3,7 @@ const fs = require('fs-extra');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
+const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
/**
* Auggie CLI setup handler
@@ -33,10 +34,23 @@ class AuggieSetup extends BaseIdeSetup {
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
- // Get tasks, tools, and workflows (standalone only)
+ // Get tasks, tools, and workflows (ALL workflows now generate commands)
const tasks = await this.getTasks(bmadDir, true);
const tools = await this.getTools(bmadDir, true);
- const workflows = await this.getWorkflows(bmadDir, true);
+
+ // Get ALL workflows using the new workflow command generator
+ const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
+ const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
+
+ // Convert workflow artifacts to expected format
+ const workflows = workflowArtifacts
+ .filter((artifact) => artifact.type === 'workflow-command')
+ .map((artifact) => ({
+ module: artifact.module,
+ name: path.basename(artifact.relativePath, '.md'),
+ path: artifact.sourcePath,
+ content: artifact.content,
+ }));
const bmadCommandsDir = path.join(location, 'bmad');
const agentsDir = path.join(bmadCommandsDir, 'agents');
@@ -73,13 +87,11 @@ class AuggieSetup extends BaseIdeSetup {
await this.writeFile(targetPath, commandContent);
}
- // Install workflows
+ // Install workflows (already generated commands)
for (const workflow of workflows) {
- const content = await this.readFile(workflow.path);
- const commandContent = this.createWorkflowCommand(workflow, content);
-
+ // Use the pre-generated workflow command content
const targetPath = path.join(workflowsDir, `${workflow.module}-${workflow.name}.md`);
- await this.writeFile(targetPath, commandContent);
+ await this.writeFile(targetPath, workflow.content);
}
const totalInstalled = agentArtifacts.length + tasks.length + tools.length + workflows.length;
diff --git a/tools/cli/installers/lib/ide/crush.js b/tools/cli/installers/lib/ide/crush.js
index c49424bf..0bef6952 100644
--- a/tools/cli/installers/lib/ide/crush.js
+++ b/tools/cli/installers/lib/ide/crush.js
@@ -3,6 +3,7 @@ const fs = require('fs-extra');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
+const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
/**
* Crush IDE setup handler
@@ -34,10 +35,23 @@ class CrushSetup extends BaseIdeSetup {
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
- // Get tasks, tools, and workflows (standalone only)
+ // Get tasks, tools, and workflows (ALL workflows now generate commands)
const tasks = await this.getTasks(bmadDir, true);
const tools = await this.getTools(bmadDir, true);
- const workflows = await this.getWorkflows(bmadDir, true);
+
+ // Get ALL workflows using the new workflow command generator
+ const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
+ const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
+
+ // Convert workflow artifacts to expected format for organizeByModule
+ const workflows = workflowArtifacts
+ .filter((artifact) => artifact.type === 'workflow-command')
+ .map((artifact) => ({
+ module: artifact.module,
+ name: path.basename(artifact.relativePath, '.md'),
+ path: artifact.sourcePath,
+ content: artifact.content,
+ }));
// Organize by module
const agentCount = await this.organizeByModule(commandsDir, agentArtifacts, tasks, tools, workflows, projectDir);
@@ -113,13 +127,12 @@ class CrushSetup extends BaseIdeSetup {
toolCount++;
}
- // Copy module-specific workflows
+ // Copy module-specific workflow commands (already generated)
const moduleWorkflows = workflows.filter((w) => w.module === module);
for (const workflow of moduleWorkflows) {
- const content = await this.readFile(workflow.path);
- const commandContent = this.createWorkflowCommand(workflow, content);
+ // Use the pre-generated workflow command content
const targetPath = path.join(moduleWorkflowsDir, `${workflow.name}.md`);
- await this.writeFile(targetPath, commandContent);
+ await this.writeFile(targetPath, workflow.content);
workflowCount++;
}
}
diff --git a/tools/cli/installers/lib/ide/cursor.js b/tools/cli/installers/lib/ide/cursor.js
index e7d92838..183bbced 100644
--- a/tools/cli/installers/lib/ide/cursor.js
+++ b/tools/cli/installers/lib/ide/cursor.js
@@ -2,6 +2,7 @@ const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
+const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
/**
* Cursor IDE setup handler
@@ -53,10 +54,22 @@ class CursorSetup extends BaseIdeSetup {
// Convert artifacts to agent format for index creation
const agents = agentArtifacts.map((a) => ({ module: a.module, name: a.name }));
- // Get tasks, tools, and workflows (standalone only)
+ // Get tasks, tools, and workflows (ALL workflows now generate commands)
const tasks = await this.getTasks(bmadDir, true);
const tools = await this.getTools(bmadDir, true);
- const workflows = await this.getWorkflows(bmadDir, true);
+
+ // Get ALL workflows using the new workflow command generator
+ const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
+ const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
+
+ // Convert artifacts to workflow objects for directory creation
+ const workflows = workflowArtifacts
+ .filter((artifact) => artifact.type === 'workflow-command')
+ .map((artifact) => ({
+ module: artifact.module,
+ name: path.basename(artifact.relativePath, '.md'),
+ path: artifact.sourcePath,
+ }));
// Create directories for each module
const modules = new Set();
@@ -113,18 +126,21 @@ class CursorSetup extends BaseIdeSetup {
toolCount++;
}
- // Process and copy workflows
+ // Process and copy workflow commands (generated, not raw workflows)
let workflowCount = 0;
- for (const workflow of workflows) {
- const content = await this.readAndProcess(workflow.path, {
- module: workflow.module,
- name: workflow.name,
- });
+ for (const artifact of workflowArtifacts) {
+ if (artifact.type === 'workflow-command') {
+ // Add MDC metadata header to workflow command
+ const content = this.wrapLauncherWithMDC(artifact.content, {
+ module: artifact.module,
+ name: path.basename(artifact.relativePath, '.md'),
+ });
- const targetPath = path.join(bmadRulesDir, workflow.module, 'workflows', `${workflow.name}.mdc`);
+ const targetPath = path.join(bmadRulesDir, artifact.module, 'workflows', `${path.basename(artifact.relativePath, '.md')}.mdc`);
- await this.writeFile(targetPath, content);
- workflowCount++;
+ await this.writeFile(targetPath, content);
+ workflowCount++;
+ }
}
// Create BMAD index file (but NOT .cursorrules - user manages that)
diff --git a/tools/cli/installers/lib/ide/gemini.js b/tools/cli/installers/lib/ide/gemini.js
index 7de51742..10dd04b9 100644
--- a/tools/cli/installers/lib/ide/gemini.js
+++ b/tools/cli/installers/lib/ide/gemini.js
@@ -4,6 +4,7 @@ const yaml = require('js-yaml');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
+const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
/**
* Gemini CLI setup handler
@@ -68,9 +69,13 @@ class GeminiSetup extends BaseIdeSetup {
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
- // Get tasks
+ // Get tasks and workflows (ALL workflows now generate commands)
const tasks = await this.getTasks(bmadDir);
+ // Get ALL workflows using the new workflow command generator
+ const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
+ const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
+
// Install agents as TOML files with bmad- prefix (flat structure)
let agentCount = 0;
for (const artifact of agentArtifacts) {
@@ -98,17 +103,37 @@ class GeminiSetup extends BaseIdeSetup {
console.log(chalk.green(` โ Added task: /bmad:tasks:${task.module}:${task.name}`));
}
+ // Install workflows as TOML files with bmad- prefix (flat structure)
+ let workflowCount = 0;
+ for (const artifact of workflowArtifacts) {
+ if (artifact.type === 'workflow-command') {
+ // Create TOML wrapper around workflow command content
+ const tomlContent = await this.createWorkflowToml(artifact);
+
+ // Flat structure: bmad-workflow-{module}-{name}.toml
+ const workflowName = path.basename(artifact.relativePath, '.md');
+ const tomlPath = path.join(commandsDir, `bmad-workflow-${artifact.module}-${workflowName}.toml`);
+ await this.writeFile(tomlPath, tomlContent);
+ workflowCount++;
+
+ console.log(chalk.green(` โ Added workflow: /bmad:workflows:${artifact.module}:${workflowName}`));
+ }
+ }
+
console.log(chalk.green(`โ ${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents configured`));
console.log(chalk.dim(` - ${taskCount} tasks configured`));
+ console.log(chalk.dim(` - ${workflowCount} workflows configured`));
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
console.log(chalk.dim(` - Agent activation: /bmad:agents:{agent-name}`));
console.log(chalk.dim(` - Task activation: /bmad:tasks:{task-name}`));
+ console.log(chalk.dim(` - Workflow activation: /bmad:workflows:{workflow-name}`));
return {
success: true,
agents: agentCount,
tasks: taskCount,
+ workflows: workflowCount,
};
}
@@ -179,6 +204,27 @@ ${contentWithoutFrontmatter}
return tomlContent;
}
+ /**
+ * Create workflow TOML content from artifact
+ */
+ async createWorkflowToml(artifact) {
+ // Extract description from artifact content
+ const descriptionMatch = artifact.content.match(/description:\s*"([^"]+)"/);
+ const description = descriptionMatch
+ ? descriptionMatch[1]
+ : `BMAD ${artifact.module.toUpperCase()} Workflow: ${path.basename(artifact.relativePath, '.md')}`;
+
+ // Strip frontmatter from command content
+ const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
+ const contentWithoutFrontmatter = artifact.content.replace(frontmatterRegex, '').trim();
+
+ return `description = "${description}"
+prompt = """
+${contentWithoutFrontmatter}
+"""
+`;
+ }
+
/**
* Cleanup Gemini configuration - surgically remove only BMAD files
*/
diff --git a/tools/cli/installers/lib/ide/iflow.js b/tools/cli/installers/lib/ide/iflow.js
index df32d1e7..bbe6d470 100644
--- a/tools/cli/installers/lib/ide/iflow.js
+++ b/tools/cli/installers/lib/ide/iflow.js
@@ -3,6 +3,7 @@ const fs = require('fs-extra');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
+const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
/**
* iFlow CLI setup handler
@@ -29,9 +30,11 @@ class IFlowSetup extends BaseIdeSetup {
const commandsDir = path.join(iflowDir, this.commandsDir, 'bmad');
const agentsDir = path.join(commandsDir, 'agents');
const tasksDir = path.join(commandsDir, 'tasks');
+ const workflowsDir = path.join(commandsDir, 'workflows');
await this.ensureDir(agentsDir);
await this.ensureDir(tasksDir);
+ await this.ensureDir(workflowsDir);
// Generate agent launchers
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
@@ -47,9 +50,13 @@ class IFlowSetup extends BaseIdeSetup {
agentCount++;
}
- // Get tasks
+ // Get tasks and workflows (ALL workflows now generate commands)
const tasks = await this.getTasks(bmadDir);
+ // Get ALL workflows using the new workflow command generator
+ const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
+ const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
+
// Setup tasks as commands
let taskCount = 0;
for (const task of tasks) {
@@ -61,15 +68,27 @@ class IFlowSetup extends BaseIdeSetup {
taskCount++;
}
+ // Setup workflows as commands (already generated)
+ let workflowCount = 0;
+ for (const artifact of workflowArtifacts) {
+ if (artifact.type === 'workflow-command') {
+ const targetPath = path.join(workflowsDir, `${artifact.module}-${path.basename(artifact.relativePath, '.md')}.md`);
+ await this.writeFile(targetPath, artifact.content);
+ workflowCount++;
+ }
+ }
+
console.log(chalk.green(`โ ${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agent commands created`));
console.log(chalk.dim(` - ${taskCount} task commands created`));
+ console.log(chalk.dim(` - ${workflowCount} workflow commands created`));
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
return {
success: true,
agents: agentCount,
tasks: taskCount,
+ workflows: workflowCount,
};
}
diff --git a/tools/cli/installers/lib/ide/kiro-cli.js b/tools/cli/installers/lib/ide/kiro-cli.js
new file mode 100644
index 00000000..c2702900
--- /dev/null
+++ b/tools/cli/installers/lib/ide/kiro-cli.js
@@ -0,0 +1,327 @@
+const path = require('node:path');
+const { BaseIdeSetup } = require('./_base-ide');
+const chalk = require('chalk');
+const fs = require('fs-extra');
+const yaml = require('js-yaml');
+
+/**
+ * Kiro CLI setup handler for BMad Method
+ */
+class KiroCliSetup extends BaseIdeSetup {
+ constructor() {
+ super('kiro-cli', 'Kiro CLI', false);
+ this.configDir = '.kiro';
+ this.agentsDir = 'agents';
+ }
+
+ /**
+ * Cleanup old BMAD installation before reinstalling
+ * @param {string} projectDir - Project directory
+ */
+ async cleanup(projectDir) {
+ const bmadAgentsDir = path.join(projectDir, this.configDir, this.agentsDir);
+
+ if (await fs.pathExists(bmadAgentsDir)) {
+ // Remove existing BMad agents
+ const files = await fs.readdir(bmadAgentsDir);
+ for (const file of files) {
+ if (file.startsWith('bmad-') || file.includes('bmad')) {
+ await fs.remove(path.join(bmadAgentsDir, file));
+ }
+ }
+ console.log(chalk.dim(` Cleaned old BMAD agents from ${this.name}`));
+ }
+ }
+
+ /**
+ * Setup Kiro CLI configuration with BMad agents
+ * @param {string} projectDir - Project directory
+ * @param {string} bmadDir - BMAD installation directory
+ * @param {Object} options - Setup options
+ */
+ async setup(projectDir, bmadDir, options = {}) {
+ console.log(chalk.cyan(`Setting up ${this.name}...`));
+
+ await this.cleanup(projectDir);
+
+ const kiroDir = path.join(projectDir, this.configDir);
+ const agentsDir = path.join(kiroDir, this.agentsDir);
+
+ await this.ensureDir(agentsDir);
+
+ // Create BMad agents from source YAML files
+ await this.createBmadAgentsFromSource(agentsDir, projectDir);
+
+ console.log(chalk.green(`โ ${this.name} configured with BMad agents`));
+ }
+
+ /**
+ * Create BMad agent definitions from source YAML files
+ * @param {string} agentsDir - Agents directory
+ * @param {string} projectDir - Project directory
+ */
+ async createBmadAgentsFromSource(agentsDir, projectDir) {
+ const sourceDir = path.join(__dirname, '../../../../../src/modules');
+
+ // Find all agent YAML files
+ const agentFiles = await this.findAgentFiles(sourceDir);
+
+ for (const agentFile of agentFiles) {
+ try {
+ await this.processAgentFile(agentFile, agentsDir, projectDir);
+ } catch (error) {
+ console.warn(chalk.yellow(`โ ๏ธ Failed to process ${agentFile}: ${error.message}`));
+ }
+ }
+ }
+
+ /**
+ * Find all agent YAML files in modules and core
+ * @param {string} sourceDir - Source modules directory
+ * @returns {Array} Array of agent file paths
+ */
+ async findAgentFiles(sourceDir) {
+ const agentFiles = [];
+
+ // Check core agents
+ const coreAgentsDir = path.join(__dirname, '../../../../../src/core/agents');
+ if (await fs.pathExists(coreAgentsDir)) {
+ const files = await fs.readdir(coreAgentsDir);
+
+ for (const file of files) {
+ if (file.endsWith('.agent.yaml')) {
+ agentFiles.push(path.join(coreAgentsDir, file));
+ }
+ }
+ }
+
+ // Check module agents
+ if (!(await fs.pathExists(sourceDir))) {
+ return agentFiles;
+ }
+
+ const modules = await fs.readdir(sourceDir);
+
+ for (const module of modules) {
+ const moduleAgentsDir = path.join(sourceDir, module, 'agents');
+
+ if (await fs.pathExists(moduleAgentsDir)) {
+ const files = await fs.readdir(moduleAgentsDir);
+
+ for (const file of files) {
+ if (file.endsWith('.agent.yaml')) {
+ agentFiles.push(path.join(moduleAgentsDir, file));
+ }
+ }
+ }
+ }
+
+ return agentFiles;
+ }
+
+ /**
+ * Validate BMad Core compliance
+ * @param {Object} agentData - Agent YAML data
+ * @returns {boolean} True if compliant
+ */
+ validateBmadCompliance(agentData) {
+ const requiredFields = ['agent.metadata.id', 'agent.persona.role', 'agent.persona.principles'];
+
+ for (const field of requiredFields) {
+ const keys = field.split('.');
+ let current = agentData;
+
+ for (const key of keys) {
+ if (!current || !current[key]) {
+ return false;
+ }
+ current = current[key];
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Process individual agent YAML file
+ * @param {string} agentFile - Path to agent YAML file
+ * @param {string} agentsDir - Target agents directory
+ * @param {string} projectDir - Project directory
+ */
+ async processAgentFile(agentFile, agentsDir, projectDir) {
+ const yamlContent = await fs.readFile(agentFile, 'utf8');
+ const agentData = yaml.load(yamlContent);
+
+ if (!this.validateBmadCompliance(agentData)) {
+ return;
+ }
+
+ // Extract module from file path
+ const normalizedPath = path.normalize(agentFile);
+ const pathParts = normalizedPath.split(path.sep);
+ const basename = path.basename(agentFile, '.agent.yaml');
+
+ // Find the module name from path
+ let moduleName = 'unknown';
+ if (pathParts.includes('src')) {
+ const srcIndex = pathParts.indexOf('src');
+ if (srcIndex + 3 < pathParts.length) {
+ const folderAfterSrc = pathParts[srcIndex + 1];
+ // Handle both src/core/agents and src/modules/[module]/agents patterns
+ if (folderAfterSrc === 'core') {
+ moduleName = 'core';
+ } else if (folderAfterSrc === 'modules') {
+ moduleName = pathParts[srcIndex + 2]; // The actual module name
+ }
+ }
+ }
+
+ // Extract the agent name from the ID path in YAML if available
+ let agentBaseName = basename;
+ if (agentData.agent && agentData.agent.metadata && agentData.agent.metadata.id) {
+ const idPath = agentData.agent.metadata.id;
+ agentBaseName = path.basename(idPath, '.md');
+ }
+
+ const agentName = `bmad-${moduleName}-${agentBaseName}`;
+ const sanitizedAgentName = this.sanitizeAgentName(agentName);
+
+ // Create JSON definition
+ await this.createAgentDefinitionFromYaml(agentsDir, sanitizedAgentName, agentData);
+
+ // Create prompt file
+ await this.createAgentPromptFromYaml(agentsDir, sanitizedAgentName, agentData, projectDir);
+ }
+
+ /**
+ * Sanitize agent name for file naming
+ * @param {string} name - Agent name
+ * @returns {string} Sanitized name
+ */
+ sanitizeAgentName(name) {
+ return name
+ .toLowerCase()
+ .replaceAll(/\s+/g, '-')
+ .replaceAll(/[^a-z0-9-]/g, '');
+ }
+
+ /**
+ * Create agent JSON definition from YAML data
+ * @param {string} agentsDir - Agents directory
+ * @param {string} agentName - Agent name (role-based)
+ * @param {Object} agentData - Agent YAML data
+ */
+ async createAgentDefinitionFromYaml(agentsDir, agentName, agentData) {
+ const personName = agentData.agent.metadata.name;
+ const role = agentData.agent.persona.role;
+
+ const agentConfig = {
+ name: agentName,
+ description: `${personName} - ${role}`,
+ prompt: `file://./${agentName}-prompt.md`,
+ tools: ['*'],
+ mcpServers: {},
+ useLegacyMcpJson: true,
+ resources: [],
+ };
+
+ const agentPath = path.join(agentsDir, `${agentName}.json`);
+ await fs.writeJson(agentPath, agentConfig, { spaces: 2 });
+ }
+
+ /**
+ * Create agent prompt from YAML data
+ * @param {string} agentsDir - Agents directory
+ * @param {string} agentName - Agent name (role-based)
+ * @param {Object} agentData - Agent YAML data
+ * @param {string} projectDir - Project directory
+ */
+ async createAgentPromptFromYaml(agentsDir, agentName, agentData, projectDir) {
+ const promptPath = path.join(agentsDir, `${agentName}-prompt.md`);
+
+ // Generate prompt from YAML data
+ const prompt = this.generatePromptFromYaml(agentData);
+ await fs.writeFile(promptPath, prompt);
+ }
+
+ /**
+ * Generate prompt content from YAML data
+ * @param {Object} agentData - Agent YAML data
+ * @returns {string} Generated prompt
+ */
+ generatePromptFromYaml(agentData) {
+ const agent = agentData.agent;
+ const name = agent.metadata.name;
+ const icon = agent.metadata.icon || '๐ค';
+ const role = agent.persona.role;
+ const identity = agent.persona.identity;
+ const style = agent.persona.communication_style;
+ const principles = agent.persona.principles;
+
+ let prompt = `# ${name} ${icon}\n\n`;
+ prompt += `## Role\n${role}\n\n`;
+
+ if (identity) {
+ prompt += `## Identity\n${identity}\n\n`;
+ }
+
+ if (style) {
+ prompt += `## Communication Style\n${style}\n\n`;
+ }
+
+ if (principles) {
+ prompt += `## Principles\n`;
+ if (typeof principles === 'string') {
+ // Handle multi-line string principles
+ prompt += principles + '\n\n';
+ } else if (Array.isArray(principles)) {
+ // Handle array principles
+ for (const principle of principles) {
+ prompt += `- ${principle}\n`;
+ }
+ prompt += '\n';
+ }
+ }
+
+ // Add menu items if available
+ if (agent.menu && agent.menu.length > 0) {
+ prompt += `## Available Workflows\n`;
+ for (let i = 0; i < agent.menu.length; i++) {
+ const item = agent.menu[i];
+ prompt += `${i + 1}. **${item.trigger}**: ${item.description}\n`;
+ }
+ prompt += '\n';
+ }
+
+ prompt += `## Instructions\nYou are ${name}, part of the BMad Method. Follow your role and principles while assisting users with their development needs.\n`;
+
+ return prompt;
+ }
+
+ /**
+ * Check if Kiro CLI is available
+ * @returns {Promise} True if available
+ */
+ async isAvailable() {
+ try {
+ const { execSync } = require('node:child_process');
+ execSync('kiro-cli --version', { stdio: 'ignore' });
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ /**
+ * Get installation instructions
+ * @returns {string} Installation instructions
+ */
+ getInstallInstructions() {
+ return `Install Kiro CLI:
+ curl -fsSL https://github.com/aws/kiro-cli/releases/latest/download/install.sh | bash
+
+ Or visit: https://github.com/aws/kiro-cli`;
+ }
+}
+
+module.exports = { KiroCliSetup };
diff --git a/tools/cli/installers/lib/ide/opencode.js b/tools/cli/installers/lib/ide/opencode.js
index b3cf03f3..e6c861a7 100644
--- a/tools/cli/installers/lib/ide/opencode.js
+++ b/tools/cli/installers/lib/ide/opencode.js
@@ -47,7 +47,7 @@ class OpenCodeSetup extends BaseIdeSetup {
agentCount++;
}
- // Install workflow commands with flat naming: bmad-workflow-{module}-{name}.md
+ // Install workflow commands with flat naming: bmad-{module}-{workflow-name}
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
@@ -55,10 +55,10 @@ class OpenCodeSetup extends BaseIdeSetup {
for (const artifact of workflowArtifacts) {
if (artifact.type === 'workflow-command') {
const commandContent = artifact.content;
- // Flat structure: bmad-workflow-{module}-{name}.md
+ // Flat structure: bmad-{module}-{workflow-name}.md
// artifact.relativePath is like: bmm/workflows/plan-project.md
const workflowName = path.basename(artifact.relativePath, '.md');
- const targetPath = path.join(commandsBaseDir, `bmad-workflow-${artifact.module}-${workflowName}.md`);
+ const targetPath = path.join(commandsBaseDir, `bmad-${artifact.module}-${workflowName}.md`);
await this.writeFile(targetPath, commandContent);
workflowCommandCount++;
}
diff --git a/tools/cli/installers/lib/ide/roo.js b/tools/cli/installers/lib/ide/roo.js
index 22f333f6..1352b311 100644
--- a/tools/cli/installers/lib/ide/roo.js
+++ b/tools/cli/installers/lib/ide/roo.js
@@ -5,34 +5,13 @@ const { AgentCommandGenerator } = require('./shared/agent-command-generator');
/**
* Roo IDE setup handler
- * Creates custom modes in .roomodes file
+ * Creates custom commands in .roo/commands directory
*/
class RooSetup extends BaseIdeSetup {
constructor() {
super('roo', 'Roo Code');
- this.configFile = '.roomodes';
- this.defaultPermissions = {
- dev: {
- description: 'Development files',
- fileRegex: String.raw`.*\.(js|jsx|ts|tsx|py|java|cpp|c|h|cs|go|rs|php|rb|swift)$`,
- },
- config: {
- description: 'Configuration files',
- fileRegex: String.raw`.*\.(json|yaml|yml|toml|xml|ini|env|config)$`,
- },
- docs: {
- description: 'Documentation files',
- fileRegex: String.raw`.*\.(md|mdx|rst|txt|doc|docx)$`,
- },
- styles: {
- description: 'Style and design files',
- fileRegex: String.raw`.*\.(css|scss|sass|less|stylus)$`,
- },
- all: {
- description: 'All files',
- fileRegex: '.*',
- },
- };
+ this.configDir = '.roo';
+ this.commandsDir = 'commands';
}
/**
@@ -44,94 +23,96 @@ class RooSetup extends BaseIdeSetup {
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
- // Check for existing .roomodes file
- const roomodesPath = path.join(projectDir, this.configFile);
- let existingModes = [];
- let existingContent = '';
+ // Create .roo/commands directory
+ const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir);
+ await this.ensureDir(rooCommandsDir);
- if (await this.pathExists(roomodesPath)) {
- existingContent = await this.readFile(roomodesPath);
- // Parse existing modes to avoid duplicates
- const modeMatches = existingContent.matchAll(/- slug: ([\w-]+)/g);
- for (const match of modeMatches) {
- existingModes.push(match[1]);
- }
- console.log(chalk.yellow(`Found existing .roomodes file with ${existingModes.length} modes`));
- }
-
- // Generate agent launchers (though Roo will reference the actual .bmad agents)
+ // Generate agent launchers
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
- // Always use 'all' permissions - users can customize in .roomodes file
- const permissionChoice = 'all';
-
- // Create modes content
- let newModesContent = '';
let addedCount = 0;
let skippedCount = 0;
for (const artifact of agentArtifacts) {
- const slug = `bmad-${artifact.module}-${artifact.name}`;
+ const commandName = `bmad-${artifact.module}-agent-${artifact.name}`;
+ const commandPath = path.join(rooCommandsDir, `${commandName}.md`);
// Skip if already exists
- if (existingModes.includes(slug)) {
- console.log(chalk.dim(` Skipping ${slug} - already exists`));
+ if (await this.pathExists(commandPath)) {
+ console.log(chalk.dim(` Skipping ${commandName} - already exists`));
skippedCount++;
continue;
}
- // Read the actual agent file from .bmad for metadata extraction
+ // Read the actual agent file from .bmad for metadata extraction (installed agents are .md files)
const agentPath = path.join(bmadDir, artifact.module, 'agents', `${artifact.name}.md`);
const content = await this.readFile(agentPath);
- // Create mode entry that references the actual .bmad agent
- const modeEntry = await this.createModeEntry(
- { module: artifact.module, name: artifact.name, path: agentPath },
- content,
- permissionChoice,
- projectDir,
- );
+ // Create command file that references the actual .bmad agent
+ await this.createCommandFile({ module: artifact.module, name: artifact.name, path: agentPath }, content, commandPath, projectDir);
- newModesContent += modeEntry;
addedCount++;
- console.log(chalk.green(` โ Added mode: ${slug}`));
+ console.log(chalk.green(` โ Added command: ${commandName}`));
}
- // Build final content
- let finalContent = '';
- if (existingContent) {
- // Append to existing content
- finalContent = existingContent.trim() + '\n' + newModesContent;
- } else {
- // Create new .roomodes file
- finalContent = 'customModes:\n' + newModesContent;
- }
-
- // Write .roomodes file
- await this.writeFile(roomodesPath, finalContent);
-
console.log(chalk.green(`โ ${this.name} configured:`));
- console.log(chalk.dim(` - ${addedCount} modes added`));
+ console.log(chalk.dim(` - ${addedCount} commands added`));
if (skippedCount > 0) {
- console.log(chalk.dim(` - ${skippedCount} modes skipped (already exist)`));
+ console.log(chalk.dim(` - ${skippedCount} commands skipped (already exist)`));
}
- console.log(chalk.dim(` - Configuration file: ${this.configFile}`));
- console.log(chalk.dim(` - Permission level: all (unrestricted)`));
- console.log(chalk.yellow(`\n ๐ก Tip: Edit ${this.configFile} to customize file permissions per agent`));
- console.log(chalk.dim(` Modes will be available when you open this project in Roo Code`));
+ console.log(chalk.dim(` - Commands directory: ${this.configDir}/${this.commandsDir}/bmad/`));
+ console.log(chalk.dim(` Commands will be available when you open this project in Roo Code`));
return {
success: true,
- modes: addedCount,
+ commands: addedCount,
skipped: skippedCount,
};
}
/**
- * Create a mode entry for an agent
+ * Create a unified command file for agents
+ * @param {string} commandPath - Path where to write the command file
+ * @param {Object} options - Command options
+ * @param {string} options.name - Display name for the command
+ * @param {string} options.description - Description for the command
+ * @param {string} options.agentPath - Path to the agent file (relative to project root)
+ * @param {string} [options.icon] - Icon emoji (defaults to ๐ค)
+ * @param {string} [options.extraContent] - Additional content to include before activation
*/
- async createModeEntry(agent, content, permissionChoice, projectDir) {
+ async createAgentCommandFile(commandPath, options) {
+ const { name, description, agentPath, icon = '๐ค', extraContent = '' } = options;
+
+ // Build command content with YAML frontmatter
+ let commandContent = `---\n`;
+ commandContent += `name: '${icon} ${name}'\n`;
+ commandContent += `description: '${description}'\n`;
+ commandContent += `---\n\n`;
+
+ commandContent += `You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.\n\n`;
+
+ // Add any extra content (e.g., warnings for custom agents)
+ if (extraContent) {
+ commandContent += `${extraContent}\n\n`;
+ }
+
+ commandContent += `\n`;
+ commandContent += `1. LOAD the FULL agent file from @${agentPath}\n`;
+ commandContent += `2. READ its entire contents - this contains the complete agent persona, menu, and instructions\n`;
+ commandContent += `3. Execute ALL activation steps exactly as written in the agent file\n`;
+ commandContent += `4. Follow the agent's persona and menu system precisely\n`;
+ commandContent += `5. Stay in character throughout the session\n`;
+ commandContent += `\n`;
+
+ // Write command file
+ await this.writeFile(commandPath, commandContent);
+ }
+
+ /**
+ * Create a command file for an agent
+ */
+ async createCommandFile(agent, content, commandPath, projectDir) {
// Extract metadata from agent content
const titleMatch = content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
@@ -142,66 +123,16 @@ class RooSetup extends BaseIdeSetup {
const whenToUseMatch = content.match(/whenToUse="([^"]+)"/);
const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`;
- // Get the activation header from central template
- const activationHeader = await this.getAgentCommandHeader();
-
- const roleDefinitionMatch = content.match(/roleDefinition="([^"]+)"/);
- const roleDefinition = roleDefinitionMatch
- ? roleDefinitionMatch[1]
- : `You are a ${title} specializing in ${title.toLowerCase()} tasks and responsibilities.`;
-
// Get relative path
const relativePath = path.relative(projectDir, agent.path).replaceAll('\\', '/');
- // Determine permissions
- const permissions = this.getPermissionsForAgent(agent, permissionChoice);
-
- // Build mode entry
- const slug = `bmad-${agent.module}-${agent.name}`;
- let modeEntry = ` - slug: ${slug}\n`;
- modeEntry += ` name: '${icon} ${title}'\n`;
-
- if (permissions && permissions.description) {
- modeEntry += ` description: '${permissions.description}'\n`;
- }
-
- modeEntry += ` roleDefinition: ${roleDefinition}\n`;
- modeEntry += ` whenToUse: ${whenToUse}\n`;
- modeEntry += ` customInstructions: ${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`;
- modeEntry += ` groups:\n`;
- modeEntry += ` - read\n`;
-
- if (permissions && permissions.fileRegex) {
- modeEntry += ` - - edit\n`;
- modeEntry += ` - fileRegex: ${permissions.fileRegex}\n`;
- modeEntry += ` description: ${permissions.description}\n`;
- } else {
- modeEntry += ` - edit\n`;
- }
-
- return modeEntry;
- }
-
- /**
- * Get permissions configuration for an agent
- */
- getPermissionsForAgent(agent, permissionChoice) {
- if (permissionChoice === 'custom') {
- // Custom logic based on agent name/module
- if (agent.name.includes('dev') || agent.name.includes('code')) {
- return this.defaultPermissions.dev;
- } else if (agent.name.includes('doc') || agent.name.includes('write')) {
- return this.defaultPermissions.docs;
- } else if (agent.name.includes('config') || agent.name.includes('setup')) {
- return this.defaultPermissions.config;
- } else if (agent.name.includes('style') || agent.name.includes('css')) {
- return this.defaultPermissions.styles;
- }
- // Default to all for custom agents
- return this.defaultPermissions.all;
- }
-
- return this.defaultPermissions[permissionChoice] || null;
+ // Use unified method
+ await this.createAgentCommandFile(commandPath, {
+ name: title,
+ description: whenToUse,
+ agentPath: relativePath,
+ icon: icon,
+ });
}
/**
@@ -219,8 +150,26 @@ class RooSetup extends BaseIdeSetup {
*/
async cleanup(projectDir) {
const fs = require('fs-extra');
- const roomodesPath = path.join(projectDir, this.configFile);
+ const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir);
+ if (await fs.pathExists(rooCommandsDir)) {
+ const files = await fs.readdir(rooCommandsDir);
+ let removedCount = 0;
+
+ for (const file of files) {
+ if (file.startsWith('bmad-') && file.endsWith('.md')) {
+ await fs.remove(path.join(rooCommandsDir, file));
+ removedCount++;
+ }
+ }
+
+ if (removedCount > 0) {
+ console.log(chalk.dim(`Removed ${removedCount} BMAD commands from .roo/commands/`));
+ }
+ }
+
+ // Also clean up old .roomodes file if it exists
+ const roomodesPath = path.join(projectDir, '.roomodes');
if (await fs.pathExists(roomodesPath)) {
const content = await fs.readFile(roomodesPath, 'utf8');
@@ -245,7 +194,9 @@ class RooSetup extends BaseIdeSetup {
// Write back filtered content
await fs.writeFile(roomodesPath, filteredLines.join('\n'));
- console.log(chalk.dim(`Removed ${removedCount} BMAD modes from .roomodes`));
+ if (removedCount > 0) {
+ console.log(chalk.dim(`Removed ${removedCount} BMAD modes from legacy .roomodes file`));
+ }
}
}
@@ -254,68 +205,53 @@ class RooSetup extends BaseIdeSetup {
* @param {string} projectDir - Project directory
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
* @param {string} agentPath - Path to compiled agent (relative to project root)
- * @param {Object} metadata - Agent metadata
+ * @param {Object} metadata - Agent metadata (unused, kept for compatibility)
* @returns {Object} Installation result
*/
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
- const roomodesPath = path.join(projectDir, this.configFile);
- let existingContent = '';
+ const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir);
+ await this.ensureDir(rooCommandsDir);
- // Read existing .roomodes file
- if (await this.pathExists(roomodesPath)) {
- existingContent = await this.readFile(roomodesPath);
- }
+ const commandName = `bmad-custom-agent-${agentName.toLowerCase()}`;
+ const commandPath = path.join(rooCommandsDir, `${commandName}.md`);
- // Create custom agent mode entry
- const slug = `bmad-custom-${agentName.toLowerCase()}`;
- const modeEntry = ` - slug: ${slug}
- name: 'BMAD Custom: ${agentName}'
- description: |
- Custom BMAD agent: ${agentName}
-
- **โ ๏ธ IMPORTANT**: Run @${agentPath} first to load the complete agent!
-
- This is a launcher for the custom BMAD agent "${agentName}". The agent will follow the persona and instructions from the main agent file.
- prompt: |
- @${agentPath}
- always: false
- permissions: all
-`;
-
- // Check if mode already exists
- if (existingContent.includes(slug)) {
+ // Check if command already exists
+ if (await this.pathExists(commandPath)) {
return {
ide: 'roo',
- path: this.configFile,
- command: agentName,
+ path: path.join(this.configDir, this.commandsDir, `${commandName}.md`),
+ command: commandName,
type: 'custom-agent-launcher',
alreadyExists: true,
};
}
- // Build final content
- let finalContent = '';
- if (existingContent) {
- // Find customModes section or add it
- if (existingContent.includes('customModes:')) {
- // Append to existing customModes
- finalContent = existingContent + modeEntry;
- } else {
- // Add customModes section
- finalContent = existingContent.trim() + '\n\ncustomModes:\n' + modeEntry;
- }
- } else {
- // Create new .roomodes file with customModes
- finalContent = 'customModes:\n' + modeEntry;
- }
+ // Read the custom agent file to extract metadata (same as regular agents)
+ const fullAgentPath = path.join(projectDir, agentPath);
+ const content = await this.readFile(fullAgentPath);
- // Write .roomodes file
- await this.writeFile(roomodesPath, finalContent);
+ // Extract metadata from agent content
+ const titleMatch = content.match(/title="([^"]+)"/);
+ const title = titleMatch ? titleMatch[1] : this.formatTitle(agentName);
+
+ const iconMatch = content.match(/icon="([^"]+)"/);
+ const icon = iconMatch ? iconMatch[1] : '๐ค';
+
+ const whenToUseMatch = content.match(/whenToUse="([^"]+)"/);
+ const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`;
+
+ // Use unified method without extra content (clean)
+ await this.createAgentCommandFile(commandPath, {
+ name: title,
+ description: whenToUse,
+ agentPath: agentPath,
+ icon: icon,
+ });
return {
ide: 'roo',
- path: this.configFile,
- command: slug,
+ path: path.join(this.configDir, this.commandsDir, `${commandName}.md`),
+ command: commandName,
type: 'custom-agent-launcher',
};
}
diff --git a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js
index 27184bc9..d05b985e 100644
--- a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js
+++ b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js
@@ -90,6 +90,11 @@ async function getAgentsFromDir(dirPath, moduleName) {
continue;
}
+ // Skip README files and other non-agent files
+ if (file.toLowerCase() === 'readme.md' || file.toLowerCase().startsWith('readme-')) {
+ continue;
+ }
+
if (file.includes('.customize.')) {
continue;
}
@@ -101,6 +106,11 @@ async function getAgentsFromDir(dirPath, moduleName) {
continue;
}
+ // Only include files that have agent-specific content (compiled agents have tag)
+ if (!content.includes(' w.standalone === 'true' || w.standalone === true);
+ // ALL workflows now generate commands - no standalone filtering
+ const allWorkflows = workflows;
// Base commands directory
const baseCommandsDir = path.join(projectDir, '.claude', 'commands', 'bmad');
let generatedCount = 0;
- // Generate a command file for each standalone workflow, organized by module
- for (const workflow of standaloneWorkflows) {
+ // Generate a command file for each workflow, organized by module
+ for (const workflow of allWorkflows) {
const moduleWorkflowsDir = path.join(baseCommandsDir, workflow.module, 'workflows');
await fs.ensureDir(moduleWorkflowsDir);
@@ -46,7 +46,7 @@ class WorkflowCommandGenerator {
}
// Also create a workflow launcher README in each module
- const groupedWorkflows = this.groupWorkflowsByModule(standaloneWorkflows);
+ const groupedWorkflows = this.groupWorkflowsByModule(allWorkflows);
await this.createModuleWorkflowLaunchers(baseCommandsDir, groupedWorkflows);
return { generated: generatedCount };
@@ -59,12 +59,12 @@ class WorkflowCommandGenerator {
return { artifacts: [], counts: { commands: 0, launchers: 0 } };
}
- // Filter to only standalone workflows
- const standaloneWorkflows = workflows.filter((w) => w.standalone === 'true' || w.standalone === true);
+ // ALL workflows now generate commands - no standalone filtering
+ const allWorkflows = workflows;
const artifacts = [];
- for (const workflow of standaloneWorkflows) {
+ for (const workflow of allWorkflows) {
const commandContent = await this.generateCommandContent(workflow, bmadDir);
artifacts.push({
type: 'workflow-command',
@@ -75,7 +75,7 @@ class WorkflowCommandGenerator {
});
}
- const groupedWorkflows = this.groupWorkflowsByModule(standaloneWorkflows);
+ const groupedWorkflows = this.groupWorkflowsByModule(allWorkflows);
for (const [module, launcherContent] of Object.entries(this.buildModuleWorkflowLaunchers(groupedWorkflows))) {
artifacts.push({
type: 'workflow-launcher',
@@ -89,7 +89,7 @@ class WorkflowCommandGenerator {
return {
artifacts,
counts: {
- commands: standaloneWorkflows.length,
+ commands: allWorkflows.length,
launchers: Object.keys(groupedWorkflows).length,
},
};
@@ -99,8 +99,13 @@ class WorkflowCommandGenerator {
* Generate command content for a workflow
*/
async generateCommandContent(workflow, bmadDir) {
- // Load the template
- const template = await fs.readFile(this.templatePath, 'utf8');
+ // Determine template based on workflow file type
+ const isMarkdownWorkflow = workflow.path.endsWith('workflow.md');
+ const templateName = isMarkdownWorkflow ? 'workflow-commander.md' : 'workflow-command-template.md';
+ const templatePath = path.join(path.dirname(this.templatePath), templateName);
+
+ // Load the appropriate template
+ const template = await fs.readFile(templatePath, 'utf8');
// Convert source path to installed path
// From: /Users/.../src/modules/bmm/workflows/.../workflow.yaml
@@ -130,9 +135,7 @@ class WorkflowCommandGenerator {
.replaceAll('{{workflow_path}}', workflowPath)
.replaceAll('{{core_workflow_path}}', coreWorkflowPath)
.replaceAll('{bmad_folder}', this.bmadFolderName)
- .replaceAll('{*bmad_folder*}', '{bmad_folder}')
- .replaceAll('{{interactive}}', workflow.interactive)
- .replaceAll('{{author}}', workflow.author || 'BMAD');
+ .replaceAll('{*bmad_folder*}', '{bmad_folder}');
}
/**
diff --git a/tools/cli/installers/lib/ide/templates/workflow-commander.md b/tools/cli/installers/lib/ide/templates/workflow-commander.md
new file mode 100644
index 00000000..3645c1a2
--- /dev/null
+++ b/tools/cli/installers/lib/ide/templates/workflow-commander.md
@@ -0,0 +1,5 @@
+---
+description: '{{description}}'
+---
+
+IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL @{{workflow_path}}, READ its entire contents and follow its directions exactly!