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 01026ba6..47a82e6e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -71,4 +71,5 @@ z*/
.github/chatmodes
.agent
.agentvibes/
-.kiro/
\ No newline at end of file
+.kiro/
+.roo
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/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/code-review/workflow.yaml b/src/modules/bmm/workflows/4-implementation/code-review/workflow.yaml
index bea3b9dc..c055db20 100644
--- a/src/modules/bmm/workflows/4-implementation/code-review/workflow.yaml
+++ b/src/modules/bmm/workflows/4-implementation/code-review/workflow.yaml
@@ -1,6 +1,6 @@
# Review Story Workflow
name: code-review
-description: "Perform an ADVERSARIAL Senior Developer code review that finds 3-10 specific problems in every story. Challenges everything: code quality, test coverage, architecture compliance, security, performance. NEVER accepts 'looks good' - must find minimum issues and can auto-fix with user approval."
+description: "Perform an ADVERSARIAL Senior Developer code review that finds 3-10 specific problems in every story. Challenges everything: code quality, test coverage, architecture compliance, security, performance. NEVER accepts `looks good` - must find minimum issues and can auto-fix with user approval."
author: "BMad"
# Critical variables from config
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/src/modules/bmm/workflows/testarch/ci/github-actions-template.yaml b/src/modules/bmm/workflows/testarch/ci/github-actions-template.yaml
index 0eefd180..9f09a73f 100644
--- a/src/modules/bmm/workflows/testarch/ci/github-actions-template.yaml
+++ b/src/modules/bmm/workflows/testarch/ci/github-actions-template.yaml
@@ -27,10 +27,21 @@ jobs:
steps:
- uses: actions/checkout@v4
+ - name: Determine Node version
+ id: node-version
+ run: |
+ if [ -f .nvmrc ]; then
+ echo "value=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
+ echo "Using Node from .nvmrc"
+ else
+ echo "value=24" >> "$GITHUB_OUTPUT"
+ echo "Using default Node 24 (current LTS)"
+ fi
+
- name: Setup Node.js
uses: actions/setup-node@v4
with:
- node-version-file: ".nvmrc"
+ node-version: ${{ steps.node-version.outputs.value }}
cache: "npm"
- name: Install dependencies
@@ -54,10 +65,21 @@ jobs:
steps:
- uses: actions/checkout@v4
+ - name: Determine Node version
+ id: node-version
+ run: |
+ if [ -f .nvmrc ]; then
+ echo "value=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
+ echo "Using Node from .nvmrc"
+ else
+ echo "value=22" >> "$GITHUB_OUTPUT"
+ echo "Using default Node 22 (current LTS)"
+ fi
+
- name: Setup Node.js
uses: actions/setup-node@v4
with:
- node-version-file: ".nvmrc"
+ node-version: ${{ steps.node-version.outputs.value }}
cache: "npm"
- name: Cache Playwright browsers
@@ -99,10 +121,21 @@ jobs:
steps:
- uses: actions/checkout@v4
+ - name: Determine Node version
+ id: node-version
+ run: |
+ if [ -f .nvmrc ]; then
+ echo "value=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
+ echo "Using Node from .nvmrc"
+ else
+ echo "value=22" >> "$GITHUB_OUTPUT"
+ echo "Using default Node 22 (current LTS)"
+ fi
+
- name: Setup Node.js
uses: actions/setup-node@v4
with:
- node-version-file: ".nvmrc"
+ node-version: ${{ steps.node-version.outputs.value }}
cache: "npm"
- name: Cache Playwright browsers
diff --git a/src/modules/bmm/workflows/testarch/ci/gitlab-ci-template.yaml b/src/modules/bmm/workflows/testarch/ci/gitlab-ci-template.yaml
index e3b433da..f5336de4 100644
--- a/src/modules/bmm/workflows/testarch/ci/gitlab-ci-template.yaml
+++ b/src/modules/bmm/workflows/testarch/ci/gitlab-ci-template.yaml
@@ -15,6 +15,8 @@ variables:
npm_config_cache: "$CI_PROJECT_DIR/.npm"
# Playwright browser cache
PLAYWRIGHT_BROWSERS_PATH: "$CI_PROJECT_DIR/.cache/ms-playwright"
+ # Default Node version when .nvmrc is missing
+ DEFAULT_NODE_VERSION: "24"
# Caching configuration
cache:
@@ -29,19 +31,32 @@ cache:
# Lint stage - Code quality checks
lint:
stage: lint
- image: node:20
- script:
+ image: node:$DEFAULT_NODE_VERSION
+ before_script:
+ - |
+ NODE_VERSION=$(cat .nvmrc 2>/dev/null || echo "$DEFAULT_NODE_VERSION")
+ echo "Using Node $NODE_VERSION"
+ npm install -g n
+ n "$NODE_VERSION"
+ node -v
- npm ci
+ script:
- npm run lint
timeout: 5 minutes
# Test stage - Parallel execution with sharding
.test-template: &test-template
stage: test
- image: node:20
+ image: node:$DEFAULT_NODE_VERSION
needs:
- lint
before_script:
+ - |
+ NODE_VERSION=$(cat .nvmrc 2>/dev/null || echo "$DEFAULT_NODE_VERSION")
+ echo "Using Node $NODE_VERSION"
+ npm install -g n
+ n "$NODE_VERSION"
+ node -v
- npm ci
- npx playwright install --with-deps chromium
artifacts:
@@ -75,7 +90,7 @@ test:shard-4:
# Burn-in stage - Flaky test detection
burn-in:
stage: burn-in
- image: node:20
+ image: node:$DEFAULT_NODE_VERSION
needs:
- test:shard-1
- test:shard-2
@@ -86,6 +101,12 @@ burn-in:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_PIPELINE_SOURCE == "schedule"'
before_script:
+ - |
+ NODE_VERSION=$(cat .nvmrc 2>/dev/null || echo "$DEFAULT_NODE_VERSION")
+ echo "Using Node $NODE_VERSION"
+ npm install -g n
+ n "$NODE_VERSION"
+ node -v
- npm ci
- npx playwright install --with-deps chromium
script:
diff --git a/src/modules/bmm/workflows/testarch/ci/instructions.md b/src/modules/bmm/workflows/testarch/ci/instructions.md
index 9bd87940..9241e93c 100644
--- a/src/modules/bmm/workflows/testarch/ci/instructions.md
+++ b/src/modules/bmm/workflows/testarch/ci/instructions.md
@@ -61,8 +61,8 @@ Scaffolds a production-ready CI/CD quality pipeline with test execution, burn-in
- Ask user if unable to auto-detect
5. **Read Environment Configuration**
- - Check for `.nvmrc` to determine Node version
- - Default to Node 20 LTS if not found
+ - Use `.nvmrc` for Node version if present
+ - If missing, default to a current LTS (Node 24) or newer instead of a fixed old version
- Read `package.json` to identify dependencies (affects caching strategy)
**Halt Condition:** If preflight checks fail, stop immediately and report which requirement failed.
diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js
index c43a197b..db8333bb 100644
--- a/tools/cli/installers/lib/core/installer.js
+++ b/tools/cli/installers/lib/core/installer.js
@@ -51,6 +51,7 @@ class Installer {
this.configCollector = new ConfigCollector();
this.ideConfigManager = new IdeConfigManager();
this.installedFiles = []; // Track all installed files
+ this.ttsInjectedFiles = []; // Track files with TTS injection applied
}
/**
@@ -146,8 +147,8 @@ class Installer {
content = content.replaceAll('{*bmad_folder*}', '{bmad_folder}');
}
- // Process AgentVibes injection points
- content = this.processTTSInjectionPoints(content);
+ // Process AgentVibes injection points (pass targetPath for tracking)
+ content = this.processTTSInjectionPoints(content, targetPath);
// Write to target with replaced content
await fs.ensureDir(path.dirname(targetPath));
@@ -226,10 +227,14 @@ class Installer {
* - src/modules/bmm/agents/*.md (rules sections)
* - TTS Hook: .claude/hooks/bmad-speak.sh (in AgentVibes repo)
*/
- processTTSInjectionPoints(content) {
+ processTTSInjectionPoints(content, targetPath = null) {
// Check if AgentVibes is enabled (set during installation configuration)
const enableAgentVibes = this.enableAgentVibes || false;
+ // Check if content contains any TTS injection markers
+ const hasPartyMode = content.includes('');
+ const hasAgentTTS = content.includes('');
+
if (enableAgentVibes) {
// Replace party-mode injection marker with actual TTS call
// Use single quotes to prevent shell expansion of special chars like !
@@ -253,6 +258,12 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
IMPORTANT: Use single quotes as shown - do NOT escape special characters like ! or $ inside single quotes
Run in background (&) to avoid blocking`,
);
+
+ // Track files that had TTS injection applied
+ if (targetPath && (hasPartyMode || hasAgentTTS)) {
+ const injectionType = hasPartyMode ? 'party-mode' : 'agent-tts';
+ this.ttsInjectedFiles.push({ path: targetPath, type: injectionType });
+ }
} else {
// Strip injection markers cleanly when AgentVibes is disabled
content = content.replaceAll(/\n?/g, '');
@@ -1021,6 +1032,8 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
modules: config.modules,
ides: config.ides,
customFiles: customFiles.length > 0 ? customFiles : undefined,
+ ttsInjectedFiles: this.enableAgentVibes && this.ttsInjectedFiles.length > 0 ? this.ttsInjectedFiles : undefined,
+ agentVibesEnabled: this.enableAgentVibes || false,
});
// Offer cleanup for legacy files (only for updates, not fresh installs, and only if not skipped)
@@ -1526,13 +1539,16 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Build YAML + customize to .md
const customizeExists = await fs.pathExists(customizePath);
- const xmlContent = await this.xmlHandler.buildFromYaml(yamlPath, customizeExists ? customizePath : null, {
+ let xmlContent = await this.xmlHandler.buildFromYaml(yamlPath, customizeExists ? customizePath : null, {
includeMetadata: true,
});
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
+ // Process TTS injection points (pass targetPath for tracking)
+ xmlContent = this.processTTSInjectionPoints(xmlContent, mdPath);
+
// Write the built .md file to bmad/{module}/agents/ with POSIX-compliant final newline
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
await fs.writeFile(mdPath, content, 'utf8');
@@ -1628,13 +1644,16 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
// Build YAML to XML .md
- const xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, {
+ let xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, {
includeMetadata: true,
});
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
+ // Process TTS injection points (pass targetPath for tracking)
+ xmlContent = this.processTTSInjectionPoints(xmlContent, targetMdPath);
+
// Write the built .md file with POSIX-compliant final newline
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
await fs.writeFile(targetMdPath, content, 'utf8');
@@ -1722,13 +1741,16 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
// Build YAML + customize to .md
- const xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, {
+ let xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, {
includeMetadata: true,
});
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
+ // Process TTS injection points (pass targetPath for tracking)
+ xmlContent = this.processTTSInjectionPoints(xmlContent, targetMdPath);
+
// Write the rebuilt .md file with POSIX-compliant final newline
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
await fs.writeFile(targetMdPath, content, 'utf8');
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
index 5ea9bc5f..c2702900 100644
--- a/tools/cli/installers/lib/ide/kiro-cli.js
+++ b/tools/cli/installers/lib/ide/kiro-cli.js
@@ -156,16 +156,41 @@ class KiroCliSetup extends BaseIdeSetup {
return;
}
- // Extract agent name from ID path (e.g., "{bmad_folder}/bmm/agents/analyst.md" -> "analyst")
- const idPath = agentData.agent.metadata.id;
- const basename = path.basename(idPath, '.md');
- const agentName = basename.startsWith('bmad-') ? basename : `bmad-${basename}`;
+ // 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, agentName, agentData);
+ await this.createAgentDefinitionFromYaml(agentsDir, sanitizedAgentName, agentData);
// Create prompt file
- await this.createAgentPromptFromYaml(agentsDir, agentName, agentData, projectDir);
+ await this.createAgentPromptFromYaml(agentsDir, sanitizedAgentName, agentData, projectDir);
}
/**
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
@@ -127,9 +132,7 @@ class WorkflowCommandGenerator {
.replaceAll('{{description}}', workflow.description)
.replaceAll('{{workflow_path}}', workflowPath)
.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!
diff --git a/tools/cli/lib/agent/compiler.js b/tools/cli/lib/agent/compiler.js
index ba9b1557..3df6845b 100644
--- a/tools/cli/lib/agent/compiler.js
+++ b/tools/cli/lib/agent/compiler.js
@@ -482,10 +482,39 @@ function compileAgent(yamlContent, answers = {}, agentName = '', targetPath = ''
};
}
+/**
+ * Process TTS injection markers in content
+ * @param {string} content - Content to process
+ * @param {boolean} enableAgentVibes - Whether AgentVibes is enabled
+ * @returns {Object} { content: string, hadInjection: boolean }
+ */
+function processTTSInjectionPoints(content, enableAgentVibes) {
+ const hasAgentTTS = content.includes('');
+
+ if (enableAgentVibes && hasAgentTTS) {
+ // Replace agent-tts injection marker with TTS rule
+ content = content.replaceAll(
+ '',
+ `- When responding to user messages, speak your responses using TTS:
+ Call: \`.claude/hooks/bmad-speak.sh '{agent-id}' '{response-text}'\` after each response
+ Replace {agent-id} with YOUR agent ID from tag at top of this file
+ Replace {response-text} with the text you just output to the user
+ IMPORTANT: Use single quotes as shown - do NOT escape special characters like ! or $ inside single quotes
+ Run in background (&) to avoid blocking`,
+ );
+ return { content, hadInjection: true };
+ } else if (!enableAgentVibes && hasAgentTTS) {
+ // Strip injection markers when disabled
+ content = content.replaceAll(/\n?/g, '');
+ }
+
+ return { content, hadInjection: false };
+}
+
/**
* Compile agent file to .md
* @param {string} yamlPath - Path to agent YAML file
- * @param {Object} options - { answers: {}, outputPath: string }
+ * @param {Object} options - { answers: {}, outputPath: string, enableAgentVibes: boolean }
* @returns {Object} Compilation result
*/
function compileAgentFile(yamlPath, options = {}) {
@@ -501,13 +530,24 @@ function compileAgentFile(yamlPath, options = {}) {
outputPath = path.join(dir, `${basename}.md`);
}
+ // Process TTS injection points if enableAgentVibes option is provided
+ let xml = result.xml;
+ let ttsInjected = false;
+ if (options.enableAgentVibes !== undefined) {
+ const ttsResult = processTTSInjectionPoints(xml, options.enableAgentVibes);
+ xml = ttsResult.content;
+ ttsInjected = ttsResult.hadInjection;
+ }
+
// Write compiled XML
- fs.writeFileSync(outputPath, result.xml, 'utf8');
+ fs.writeFileSync(outputPath, xml, 'utf8');
return {
...result,
+ xml,
outputPath,
sourcePath: yamlPath,
+ ttsInjected,
};
}
diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js
index 32e8dfc0..4c5b3379 100644
--- a/tools/cli/lib/ui.js
+++ b/tools/cli/lib/ui.js
@@ -363,11 +363,60 @@ class UI {
`๐ง Tools Configured: ${result.ides?.length > 0 ? result.ides.join(', ') : 'none'}`,
];
+ // Add AgentVibes TTS info if enabled
+ if (result.agentVibesEnabled) {
+ summary.push(`๐ค AgentVibes TTS: Enabled`);
+ }
+
CLIUtils.displayBox(summary.join('\n\n'), {
borderColor: 'green',
borderStyle: 'round',
});
+ // Display TTS injection details if present
+ if (result.ttsInjectedFiles && result.ttsInjectedFiles.length > 0) {
+ console.log('\n' + chalk.cyan.bold('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'));
+ console.log(chalk.cyan.bold(' AgentVibes TTS Injection Summary'));
+ console.log(chalk.cyan.bold('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n'));
+
+ // Explain what TTS injection is
+ console.log(chalk.white.bold('What is TTS Injection?\n'));
+ console.log(chalk.dim(' TTS (Text-to-Speech) injection adds voice instructions to BMAD agents,'));
+ console.log(chalk.dim(' enabling them to speak their responses aloud using AgentVibes.\n'));
+ console.log(chalk.dim(' Example: When you activate the PM agent, it will greet you with'));
+ console.log(chalk.dim(' spoken audio like "Hey! I\'m your Project Manager. How can I help?"\n'));
+
+ console.log(chalk.green(`โ
TTS injection applied to ${result.ttsInjectedFiles.length} file(s):\n`));
+
+ // Group by type
+ const partyModeFiles = result.ttsInjectedFiles.filter((f) => f.type === 'party-mode');
+ const agentTTSFiles = result.ttsInjectedFiles.filter((f) => f.type === 'agent-tts');
+
+ if (partyModeFiles.length > 0) {
+ console.log(chalk.yellow(' Party Mode (multi-agent conversations):'));
+ for (const file of partyModeFiles) {
+ console.log(chalk.dim(` โข ${file.path}`));
+ }
+ }
+
+ if (agentTTSFiles.length > 0) {
+ console.log(chalk.yellow(' Agent TTS (individual agent voices):'));
+ for (const file of agentTTSFiles) {
+ console.log(chalk.dim(` โข ${file.path}`));
+ }
+ }
+
+ // Show backup info and restore command
+ console.log('\n' + chalk.white.bold('Backups & Recovery:\n'));
+ console.log(chalk.dim(' Pre-injection backups are stored in:'));
+ console.log(chalk.cyan(' ~/.bmad-tts-backups/\n'));
+ console.log(chalk.dim(' To restore original files (removes TTS instructions):'));
+ console.log(chalk.cyan(` bmad-tts-injector.sh --restore ${result.path}\n`));
+
+ console.log(chalk.cyan('๐ก BMAD agents will now speak when activated!'));
+ console.log(chalk.dim(' Ensure AgentVibes is installed: https://agentvibes.org'));
+ }
+
console.log('\n' + chalk.green.bold('โจ BMAD is ready to use!'));
}