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} + + โŒ sprint-status.yaml not found. +Run `/bmad:bmm:workflows:sprint-planning` to generate it, then rerun sprint-status. + 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) + + + + +## ๐Ÿ“Š Sprint Status + +- Project: {{project}} ({{project_key}}) +- Tracking: {{tracking_system}} +- Status file: {sprint_status_file} + +**Stories:** backlog {{count_backlog}}, drafted {{count_drafted}}, ready-for-dev {{count_ready}}, in-progress {{count_in_progress}}, review {{count_review}}, done {{count_done}} + +**Epics:** backlog {{epic_backlog}}, contexted {{epic_contexted}} + +**Next Recommendation:** /bmad:bmm:workflows:{{next_workflow_id}} ({{next_story_id}}) + +{{#if risks}} +**Risks:** +{{#each risks}} + +- {{this}} + {{/each}} + {{/if}} + +{{#if by_epic}} +**Per Epic:** +{{#each by_epic}} + +- {{epic_id}}: context={{context_status}}, stories โ†’ backlog {{backlog}}, drafted {{drafted}}, ready {{ready_for_dev}}, in-progress {{in_progress}}, review {{review}}, done {{done}} + {{/each}} + {{/if}} + + + + + Pick an option: +1) Run recommended workflow now +2) Show all stories grouped by status +3) Show raw sprint-status.yaml +4) Exit +Choice: + + + Run `/bmad:bmm:workflows:{{next_workflow_id}}`. +If the command targets a story, set `story_key={{next_story_id}}` when prompted. + + + + +### Stories by Status +- In Progress: {{stories_in_progress}} +- Review: {{stories_in_review}} +- Ready for Dev: {{stories_ready_for_dev}} +- Drafted: {{stories_drafted}} +- Backlog: {{stories_backlog}} +- Done: {{stories_done}} + + + + + 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!