Compare commits

..

1 Commits

Author SHA1 Message Date
Ziyu Huang 3b36fa9d8e
Merge e9b75bd3cb into 41f9cc1913 2025-12-03 22:37:50 +02:00
30 changed files with 273 additions and 661 deletions

View File

@ -1,15 +0,0 @@
#!/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"
}

View File

@ -1,286 +1,16 @@
name: Discord Notification name: Discord Notification
on: "on": [pull_request, release, create, delete, issue_comment, pull_request_review, pull_request_review_comment]
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: jobs:
pull_request: notify:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - name: Notify Discord
uses: sarisia/actions-status-discord@v1
if: always()
with: with:
ref: ${{ github.event.repository.default_branch }} webhook: ${{ secrets.DISCORD_WEBHOOK }}
sparse-checkout: .github/scripts status: ${{ job.status }}
sparse-checkout-cone-mode: false title: "Triggered by ${{ github.event_name }}"
- name: Notify Discord color: 0x5865F2
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
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 @-

1
.gitignore vendored
View File

@ -72,4 +72,3 @@ z*/
.agent .agent
.agentvibes/ .agentvibes/
.kiro/ .kiro/
.roo

View File

@ -1,5 +1,5 @@
--- ---
name: brainstorming-session name: Brainstorming Session
description: Facilitate interactive brainstorming sessions using diverse creative techniques and ideation methods description: Facilitate interactive brainstorming sessions using diverse creative techniques and ideation methods
context_file: '' # Optional context file path for project-specific guidance context_file: '' # Optional context file path for project-specific guidance
--- ---

View File

@ -1,5 +1,5 @@
--- ---
name: party-mode name: Party Mode
description: Orchestrates group discussions between all installed BMAD agents, enabling natural multi-agent conversations description: Orchestrates group discussions between all installed BMAD agents, enabling natural multi-agent conversations
--- ---

View File

@ -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 description: Interactive workflow to build BMAD Core compliant agents with optional brainstorming, persona development, and command structure
web_bundle: true web_bundle: true
--- ---

View File

@ -1,5 +1,5 @@
--- ---
name: create-workflow name: Create Workflow
description: Create structured standalone workflows using markdown-based step architecture description: Create structured standalone workflows using markdown-based step architecture
web_bundle: true web_bundle: true
--- ---

View File

@ -1,5 +1,5 @@
--- ---
name: edit-agent name: Edit Agent
description: Edit existing BMAD agents while following all best practices and conventions description: Edit existing BMAD agents while following all best practices and conventions
web_bundle: false web_bundle: false
--- ---

View File

@ -1,5 +1,5 @@
--- ---
name: edit-workflow name: Edit Workflow
description: Intelligent workflow editor that helps modify existing workflows while following best practices description: Intelligent workflow editor that helps modify existing workflows while following best practices
web_bundle: true web_bundle: true
--- ---

View File

@ -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 description: Systematic validation of workflows against BMAD standards with adversarial analysis and detailed reporting
web_bundle: false web_bundle: false
--- ---

View File

@ -1,5 +1,5 @@
--- ---
name: create-product-brief name: Product Brief Workflow
description: Create comprehensive product briefs through collaborative step-by-step discovery as creative Business Analyst working with the user as peers. description: Create comprehensive product briefs through collaborative step-by-step discovery as creative Business Analyst working with the user as peers.
web_bundle: true web_bundle: true
--- ---

View File

@ -1,7 +1,6 @@
--- ---
name: research name: Research Workflow
description: Conduct comprehensive research across multiple domains using current web data and verified sources - Market, Technical, Domain and other research types. 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 # Research Workflow

View File

@ -1,9 +1,3 @@
---
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 # 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. **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.

View File

@ -1,7 +1,7 @@
--- ---
name: create-prd name: PRD Workflow
description: Creates a comprehensive PRDs through collaborative step-by-step discovery between two product managers working as peers. 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 web_bundle: true
--- ---

View File

@ -1,7 +1,6 @@
--- ---
name: create-architecture name: Architecture Workflow
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. 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 # Architecture Workflow

View File

@ -1,5 +1,5 @@
--- ---
name: create-epics-stories name: 'Create Epics and 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.' 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 web_bundle: true
--- ---

View File

@ -1,5 +1,5 @@
--- ---
name: check-implementation-readiness name: '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.' 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 web_bundle: false
--- ---

View File

@ -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. 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.
--- ---

View File

@ -109,7 +109,7 @@ class ManifestGenerator {
} }
/** /**
* Recursively find and parse workflow.yaml and workflow.md files * Recursively find and parse workflow.yaml files
*/ */
async getWorkflowsFromPath(basePath, moduleName) { async getWorkflowsFromPath(basePath, moduleName) {
const workflows = []; const workflows = [];
@ -130,23 +130,11 @@ class ManifestGenerator {
// Recurse into subdirectories // Recurse into subdirectories
const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name; const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
await findWorkflows(fullPath, newRelativePath); await findWorkflows(fullPath, newRelativePath);
} else if (entry.name === 'workflow.yaml' || entry.name === 'workflow.md') { } else if (entry.name === 'workflow.yaml') {
// Parse workflow file (both YAML and MD formats) // Parse workflow file
try { try {
const content = await fs.readFile(fullPath, 'utf8'); 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) // Skip template workflows (those with placeholder values)
if (workflow.name && workflow.name.includes('{') && workflow.name.includes('}')) { if (workflow.name && workflow.name.includes('{') && workflow.name.includes('}')) {
@ -157,15 +145,18 @@ class ManifestGenerator {
// Build relative path for installation // Build relative path for installation
const installPath = const installPath =
moduleName === 'core' moduleName === 'core'
? `${this.bmadFolderName}/core/workflows/${relativePath}/${entry.name}` ? `${this.bmadFolderName}/core/workflows/${relativePath}/workflow.yaml`
: `${this.bmadFolderName}/${moduleName}/workflows/${relativePath}/${entry.name}`; : `${this.bmadFolderName}/${moduleName}/workflows/${relativePath}/workflow.yaml`;
// Check for standalone property (default: false)
const standalone = workflow.standalone === true;
// ALL workflows now generate commands - no standalone property needed
workflows.push({ workflows.push({
name: workflow.name, name: workflow.name,
description: workflow.description.replaceAll('"', '""'), // Escape quotes for CSV description: workflow.description.replaceAll('"', '""'), // Escape quotes for CSV
module: moduleName, module: moduleName,
path: installPath, path: installPath,
standalone: standalone,
}); });
// Add to files list // Add to files list
@ -572,12 +563,12 @@ class ManifestGenerator {
async writeWorkflowManifest(cfgDir) { async writeWorkflowManifest(cfgDir) {
const csvPath = path.join(cfgDir, 'workflow-manifest.csv'); const csvPath = path.join(cfgDir, 'workflow-manifest.csv');
// Create CSV header - removed standalone column as ALL workflows now generate commands // Create CSV header with standalone column
let csv = 'name,description,module,path\n'; let csv = 'name,description,module,path,standalone\n';
// Add all workflows - no standalone property needed anymore // Add all workflows
for (const workflow of this.workflows) { for (const workflow of this.workflows) {
csv += `"${workflow.name}","${workflow.description}","${workflow.module}","${workflow.path}"\n`; csv += `"${workflow.name}","${workflow.description}","${workflow.module}","${workflow.path}","${workflow.standalone}"\n`;
} }
await fs.writeFile(csvPath, csv); await fs.writeFile(csvPath, csv);

View File

@ -3,7 +3,6 @@ const fs = require('fs-extra');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk'); const chalk = require('chalk');
const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
/** /**
* Auggie CLI setup handler * Auggie CLI setup handler
@ -34,23 +33,10 @@ class AuggieSetup extends BaseIdeSetup {
const agentGen = new AgentCommandGenerator(this.bmadFolderName); const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
// Get tasks, tools, and workflows (ALL workflows now generate commands) // Get tasks, tools, and workflows (standalone only)
const tasks = await this.getTasks(bmadDir, true); const tasks = await this.getTasks(bmadDir, true);
const tools = await this.getTools(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 bmadCommandsDir = path.join(location, 'bmad');
const agentsDir = path.join(bmadCommandsDir, 'agents'); const agentsDir = path.join(bmadCommandsDir, 'agents');
@ -87,11 +73,13 @@ class AuggieSetup extends BaseIdeSetup {
await this.writeFile(targetPath, commandContent); await this.writeFile(targetPath, commandContent);
} }
// Install workflows (already generated commands) // Install workflows
for (const workflow of workflows) { for (const workflow of workflows) {
// Use the pre-generated workflow command content const content = await this.readFile(workflow.path);
const commandContent = this.createWorkflowCommand(workflow, content);
const targetPath = path.join(workflowsDir, `${workflow.module}-${workflow.name}.md`); const targetPath = path.join(workflowsDir, `${workflow.module}-${workflow.name}.md`);
await this.writeFile(targetPath, workflow.content); await this.writeFile(targetPath, commandContent);
} }
const totalInstalled = agentArtifacts.length + tasks.length + tools.length + workflows.length; const totalInstalled = agentArtifacts.length + tasks.length + tools.length + workflows.length;

View File

@ -3,7 +3,6 @@ const fs = require('fs-extra');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk'); const chalk = require('chalk');
const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
/** /**
* Crush IDE setup handler * Crush IDE setup handler
@ -35,23 +34,10 @@ class CrushSetup extends BaseIdeSetup {
const agentGen = new AgentCommandGenerator(this.bmadFolderName); const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
// Get tasks, tools, and workflows (ALL workflows now generate commands) // Get tasks, tools, and workflows (standalone only)
const tasks = await this.getTasks(bmadDir, true); const tasks = await this.getTasks(bmadDir, true);
const tools = await this.getTools(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 // Organize by module
const agentCount = await this.organizeByModule(commandsDir, agentArtifacts, tasks, tools, workflows, projectDir); const agentCount = await this.organizeByModule(commandsDir, agentArtifacts, tasks, tools, workflows, projectDir);
@ -127,12 +113,13 @@ class CrushSetup extends BaseIdeSetup {
toolCount++; toolCount++;
} }
// Copy module-specific workflow commands (already generated) // Copy module-specific workflows
const moduleWorkflows = workflows.filter((w) => w.module === module); const moduleWorkflows = workflows.filter((w) => w.module === module);
for (const workflow of moduleWorkflows) { for (const workflow of moduleWorkflows) {
// Use the pre-generated workflow command content const content = await this.readFile(workflow.path);
const commandContent = this.createWorkflowCommand(workflow, content);
const targetPath = path.join(moduleWorkflowsDir, `${workflow.name}.md`); const targetPath = path.join(moduleWorkflowsDir, `${workflow.name}.md`);
await this.writeFile(targetPath, workflow.content); await this.writeFile(targetPath, commandContent);
workflowCount++; workflowCount++;
} }
} }

View File

@ -2,7 +2,6 @@ const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk'); const chalk = require('chalk');
const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
/** /**
* Cursor IDE setup handler * Cursor IDE setup handler
@ -54,22 +53,10 @@ class CursorSetup extends BaseIdeSetup {
// Convert artifacts to agent format for index creation // Convert artifacts to agent format for index creation
const agents = agentArtifacts.map((a) => ({ module: a.module, name: a.name })); const agents = agentArtifacts.map((a) => ({ module: a.module, name: a.name }));
// Get tasks, tools, and workflows (ALL workflows now generate commands) // Get tasks, tools, and workflows (standalone only)
const tasks = await this.getTasks(bmadDir, true); const tasks = await this.getTasks(bmadDir, true);
const tools = await this.getTools(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 // Create directories for each module
const modules = new Set(); const modules = new Set();
@ -126,21 +113,18 @@ class CursorSetup extends BaseIdeSetup {
toolCount++; toolCount++;
} }
// Process and copy workflow commands (generated, not raw workflows) // Process and copy workflows
let workflowCount = 0; let workflowCount = 0;
for (const artifact of workflowArtifacts) { for (const workflow of workflows) {
if (artifact.type === 'workflow-command') { const content = await this.readAndProcess(workflow.path, {
// Add MDC metadata header to workflow command module: workflow.module,
const content = this.wrapLauncherWithMDC(artifact.content, { name: workflow.name,
module: artifact.module, });
name: path.basename(artifact.relativePath, '.md'),
});
const targetPath = path.join(bmadRulesDir, artifact.module, 'workflows', `${path.basename(artifact.relativePath, '.md')}.mdc`); const targetPath = path.join(bmadRulesDir, workflow.module, 'workflows', `${workflow.name}.mdc`);
await this.writeFile(targetPath, content); await this.writeFile(targetPath, content);
workflowCount++; workflowCount++;
}
} }
// Create BMAD index file (but NOT .cursorrules - user manages that) // Create BMAD index file (but NOT .cursorrules - user manages that)

View File

@ -4,7 +4,6 @@ const yaml = require('js-yaml');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk'); const chalk = require('chalk');
const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
/** /**
* Gemini CLI setup handler * Gemini CLI setup handler
@ -69,13 +68,9 @@ class GeminiSetup extends BaseIdeSetup {
const agentGen = new AgentCommandGenerator(this.bmadFolderName); const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
// Get tasks and workflows (ALL workflows now generate commands) // Get tasks
const tasks = await this.getTasks(bmadDir); 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) // Install agents as TOML files with bmad- prefix (flat structure)
let agentCount = 0; let agentCount = 0;
for (const artifact of agentArtifacts) { for (const artifact of agentArtifacts) {
@ -103,37 +98,17 @@ class GeminiSetup extends BaseIdeSetup {
console.log(chalk.green(` ✓ Added task: /bmad:tasks:${task.module}:${task.name}`)); 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.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents configured`)); console.log(chalk.dim(` - ${agentCount} agents configured`));
console.log(chalk.dim(` - ${taskCount} tasks 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(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
console.log(chalk.dim(` - Agent activation: /bmad:agents:{agent-name}`)); console.log(chalk.dim(` - Agent activation: /bmad:agents:{agent-name}`));
console.log(chalk.dim(` - Task activation: /bmad:tasks:{task-name}`)); console.log(chalk.dim(` - Task activation: /bmad:tasks:{task-name}`));
console.log(chalk.dim(` - Workflow activation: /bmad:workflows:{workflow-name}`));
return { return {
success: true, success: true,
agents: agentCount, agents: agentCount,
tasks: taskCount, tasks: taskCount,
workflows: workflowCount,
}; };
} }
@ -204,27 +179,6 @@ ${contentWithoutFrontmatter}
return tomlContent; 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 * Cleanup Gemini configuration - surgically remove only BMAD files
*/ */

View File

@ -3,7 +3,6 @@ const fs = require('fs-extra');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk'); const chalk = require('chalk');
const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
/** /**
* iFlow CLI setup handler * iFlow CLI setup handler
@ -30,11 +29,9 @@ class IFlowSetup extends BaseIdeSetup {
const commandsDir = path.join(iflowDir, this.commandsDir, 'bmad'); const commandsDir = path.join(iflowDir, this.commandsDir, 'bmad');
const agentsDir = path.join(commandsDir, 'agents'); const agentsDir = path.join(commandsDir, 'agents');
const tasksDir = path.join(commandsDir, 'tasks'); const tasksDir = path.join(commandsDir, 'tasks');
const workflowsDir = path.join(commandsDir, 'workflows');
await this.ensureDir(agentsDir); await this.ensureDir(agentsDir);
await this.ensureDir(tasksDir); await this.ensureDir(tasksDir);
await this.ensureDir(workflowsDir);
// Generate agent launchers // Generate agent launchers
const agentGen = new AgentCommandGenerator(this.bmadFolderName); const agentGen = new AgentCommandGenerator(this.bmadFolderName);
@ -50,13 +47,9 @@ class IFlowSetup extends BaseIdeSetup {
agentCount++; agentCount++;
} }
// Get tasks and workflows (ALL workflows now generate commands) // Get tasks
const tasks = await this.getTasks(bmadDir); 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 // Setup tasks as commands
let taskCount = 0; let taskCount = 0;
for (const task of tasks) { for (const task of tasks) {
@ -68,27 +61,15 @@ class IFlowSetup extends BaseIdeSetup {
taskCount++; 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.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agent commands created`)); console.log(chalk.dim(` - ${agentCount} agent commands created`));
console.log(chalk.dim(` - ${taskCount} task 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)}`)); console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
return { return {
success: true, success: true,
agents: agentCount, agents: agentCount,
tasks: taskCount, tasks: taskCount,
workflows: workflowCount,
}; };
} }

View File

@ -156,41 +156,16 @@ class KiroCliSetup extends BaseIdeSetup {
return; return;
} }
// Extract module from file path // Extract agent name from ID path (e.g., "{bmad_folder}/bmm/agents/analyst.md" -> "analyst")
const normalizedPath = path.normalize(agentFile); const idPath = agentData.agent.metadata.id;
const pathParts = normalizedPath.split(path.sep); const basename = path.basename(idPath, '.md');
const basename = path.basename(agentFile, '.agent.yaml'); const agentName = basename.startsWith('bmad-') ? basename : `bmad-${basename}`;
// 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 // Create JSON definition
await this.createAgentDefinitionFromYaml(agentsDir, sanitizedAgentName, agentData); await this.createAgentDefinitionFromYaml(agentsDir, agentName, agentData);
// Create prompt file // Create prompt file
await this.createAgentPromptFromYaml(agentsDir, sanitizedAgentName, agentData, projectDir); await this.createAgentPromptFromYaml(agentsDir, agentName, agentData, projectDir);
} }
/** /**

View File

@ -47,7 +47,7 @@ class OpenCodeSetup extends BaseIdeSetup {
agentCount++; agentCount++;
} }
// Install workflow commands with flat naming: bmad-{module}-{workflow-name} // Install workflow commands with flat naming: bmad-workflow-{module}-{name}.md
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
@ -55,10 +55,10 @@ class OpenCodeSetup extends BaseIdeSetup {
for (const artifact of workflowArtifacts) { for (const artifact of workflowArtifacts) {
if (artifact.type === 'workflow-command') { if (artifact.type === 'workflow-command') {
const commandContent = artifact.content; const commandContent = artifact.content;
// Flat structure: bmad-{module}-{workflow-name}.md // Flat structure: bmad-workflow-{module}-{name}.md
// artifact.relativePath is like: bmm/workflows/plan-project.md // artifact.relativePath is like: bmm/workflows/plan-project.md
const workflowName = path.basename(artifact.relativePath, '.md'); const workflowName = path.basename(artifact.relativePath, '.md');
const targetPath = path.join(commandsBaseDir, `bmad-${artifact.module}-${workflowName}.md`); const targetPath = path.join(commandsBaseDir, `bmad-workflow-${artifact.module}-${workflowName}.md`);
await this.writeFile(targetPath, commandContent); await this.writeFile(targetPath, commandContent);
workflowCommandCount++; workflowCommandCount++;
} }

View File

@ -5,13 +5,34 @@ const { AgentCommandGenerator } = require('./shared/agent-command-generator');
/** /**
* Roo IDE setup handler * Roo IDE setup handler
* Creates custom commands in .roo/commands directory * Creates custom modes in .roomodes file
*/ */
class RooSetup extends BaseIdeSetup { class RooSetup extends BaseIdeSetup {
constructor() { constructor() {
super('roo', 'Roo Code'); super('roo', 'Roo Code');
this.configDir = '.roo'; this.configFile = '.roomodes';
this.commandsDir = 'commands'; 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: '.*',
},
};
} }
/** /**
@ -23,96 +44,94 @@ class RooSetup extends BaseIdeSetup {
async setup(projectDir, bmadDir, options = {}) { async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`)); console.log(chalk.cyan(`Setting up ${this.name}...`));
// Create .roo/commands directory // Check for existing .roomodes file
const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir); const roomodesPath = path.join(projectDir, this.configFile);
await this.ensureDir(rooCommandsDir); let existingModes = [];
let existingContent = '';
// Generate agent launchers 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)
const agentGen = new AgentCommandGenerator(this.bmadFolderName); const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); 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 addedCount = 0;
let skippedCount = 0; let skippedCount = 0;
for (const artifact of agentArtifacts) { for (const artifact of agentArtifacts) {
const commandName = `bmad-${artifact.module}-agent-${artifact.name}`; const slug = `bmad-${artifact.module}-${artifact.name}`;
const commandPath = path.join(rooCommandsDir, `${commandName}.md`);
// Skip if already exists // Skip if already exists
if (await this.pathExists(commandPath)) { if (existingModes.includes(slug)) {
console.log(chalk.dim(` Skipping ${commandName} - already exists`)); console.log(chalk.dim(` Skipping ${slug} - already exists`));
skippedCount++; skippedCount++;
continue; continue;
} }
// Read the actual agent file from .bmad for metadata extraction (installed agents are .md files) // Read the actual agent file from .bmad for metadata extraction
const agentPath = path.join(bmadDir, artifact.module, 'agents', `${artifact.name}.md`); const agentPath = path.join(bmadDir, artifact.module, 'agents', `${artifact.name}.md`);
const content = await this.readFile(agentPath); const content = await this.readFile(agentPath);
// Create command file that references the actual .bmad agent // Create mode entry that references the actual .bmad agent
await this.createCommandFile({ module: artifact.module, name: artifact.name, path: agentPath }, content, commandPath, projectDir); const modeEntry = await this.createModeEntry(
{ module: artifact.module, name: artifact.name, path: agentPath },
content,
permissionChoice,
projectDir,
);
newModesContent += modeEntry;
addedCount++; addedCount++;
console.log(chalk.green(` ✓ Added command: ${commandName}`)); console.log(chalk.green(` ✓ Added mode: ${slug}`));
} }
// 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.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${addedCount} commands added`)); console.log(chalk.dim(` - ${addedCount} modes added`));
if (skippedCount > 0) { if (skippedCount > 0) {
console.log(chalk.dim(` - ${skippedCount} commands skipped (already exist)`)); console.log(chalk.dim(` - ${skippedCount} modes skipped (already exist)`));
} }
console.log(chalk.dim(` - Commands directory: ${this.configDir}/${this.commandsDir}/bmad/`)); console.log(chalk.dim(` - Configuration file: ${this.configFile}`));
console.log(chalk.dim(` Commands will be available when you open this project in Roo Code`)); 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`));
return { return {
success: true, success: true,
commands: addedCount, modes: addedCount,
skipped: skippedCount, skipped: skippedCount,
}; };
} }
/** /**
* Create a unified command file for agents * Create a mode entry for an agent
* @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 createAgentCommandFile(commandPath, options) { async createModeEntry(agent, content, permissionChoice, projectDir) {
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 += `<agent-activation CRITICAL="TRUE">\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 += `</agent-activation>\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 // Extract metadata from agent content
const titleMatch = content.match(/title="([^"]+)"/); const titleMatch = content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name); const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
@ -123,16 +142,66 @@ class RooSetup extends BaseIdeSetup {
const whenToUseMatch = content.match(/whenToUse="([^"]+)"/); const whenToUseMatch = content.match(/whenToUse="([^"]+)"/);
const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`; 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 // Get relative path
const relativePath = path.relative(projectDir, agent.path).replaceAll('\\', '/'); const relativePath = path.relative(projectDir, agent.path).replaceAll('\\', '/');
// Use unified method // Determine permissions
await this.createAgentCommandFile(commandPath, { const permissions = this.getPermissionsForAgent(agent, permissionChoice);
name: title,
description: whenToUse, // Build mode entry
agentPath: relativePath, const slug = `bmad-${agent.module}-${agent.name}`;
icon: icon, 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;
} }
/** /**
@ -150,26 +219,8 @@ class RooSetup extends BaseIdeSetup {
*/ */
async cleanup(projectDir) { async cleanup(projectDir) {
const fs = require('fs-extra'); const fs = require('fs-extra');
const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir); const roomodesPath = path.join(projectDir, this.configFile);
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)) { if (await fs.pathExists(roomodesPath)) {
const content = await fs.readFile(roomodesPath, 'utf8'); const content = await fs.readFile(roomodesPath, 'utf8');
@ -194,9 +245,7 @@ class RooSetup extends BaseIdeSetup {
// Write back filtered content // Write back filtered content
await fs.writeFile(roomodesPath, filteredLines.join('\n')); await fs.writeFile(roomodesPath, filteredLines.join('\n'));
if (removedCount > 0) { console.log(chalk.dim(`Removed ${removedCount} BMAD modes from .roomodes`));
console.log(chalk.dim(`Removed ${removedCount} BMAD modes from legacy .roomodes file`));
}
} }
} }
@ -205,53 +254,68 @@ class RooSetup extends BaseIdeSetup {
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory
* @param {string} agentName - Agent name (e.g., "fred-commit-poet") * @param {string} agentName - Agent name (e.g., "fred-commit-poet")
* @param {string} agentPath - Path to compiled agent (relative to project root) * @param {string} agentPath - Path to compiled agent (relative to project root)
* @param {Object} metadata - Agent metadata (unused, kept for compatibility) * @param {Object} metadata - Agent metadata
* @returns {Object} Installation result * @returns {Object} Installation result
*/ */
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir); const roomodesPath = path.join(projectDir, this.configFile);
await this.ensureDir(rooCommandsDir); let existingContent = '';
const commandName = `bmad-custom-agent-${agentName.toLowerCase()}`; // Read existing .roomodes file
const commandPath = path.join(rooCommandsDir, `${commandName}.md`); if (await this.pathExists(roomodesPath)) {
existingContent = await this.readFile(roomodesPath);
}
// Check if command already exists // Create custom agent mode entry
if (await this.pathExists(commandPath)) { 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)) {
return { return {
ide: 'roo', ide: 'roo',
path: path.join(this.configDir, this.commandsDir, `${commandName}.md`), path: this.configFile,
command: commandName, command: agentName,
type: 'custom-agent-launcher', type: 'custom-agent-launcher',
alreadyExists: true, alreadyExists: true,
}; };
} }
// Read the custom agent file to extract metadata (same as regular agents) // Build final content
const fullAgentPath = path.join(projectDir, agentPath); let finalContent = '';
const content = await this.readFile(fullAgentPath); 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;
}
// Extract metadata from agent content // Write .roomodes file
const titleMatch = content.match(/title="([^"]+)"/); await this.writeFile(roomodesPath, finalContent);
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 { return {
ide: 'roo', ide: 'roo',
path: path.join(this.configDir, this.commandsDir, `${commandName}.md`), path: this.configFile,
command: commandName, command: slug,
type: 'custom-agent-launcher', type: 'custom-agent-launcher',
}; };
} }

View File

@ -90,11 +90,6 @@ async function getAgentsFromDir(dirPath, moduleName) {
continue; continue;
} }
// Skip README files and other non-agent files
if (file.toLowerCase() === 'readme.md' || file.toLowerCase().startsWith('readme-')) {
continue;
}
if (file.includes('.customize.')) { if (file.includes('.customize.')) {
continue; continue;
} }
@ -106,11 +101,6 @@ async function getAgentsFromDir(dirPath, moduleName) {
continue; continue;
} }
// Only include files that have agent-specific content (compiled agents have <agent> tag)
if (!content.includes('<agent')) {
continue;
}
agents.push({ agents.push({
path: filePath, path: filePath,
name: file.replace('.md', ''), name: file.replace('.md', ''),

View File

@ -25,16 +25,16 @@ class WorkflowCommandGenerator {
return { generated: 0 }; return { generated: 0 };
} }
// ALL workflows now generate commands - no standalone filtering // Filter to only standalone workflows
const allWorkflows = workflows; const standaloneWorkflows = workflows.filter((w) => w.standalone === 'true' || w.standalone === true);
// Base commands directory // Base commands directory
const baseCommandsDir = path.join(projectDir, '.claude', 'commands', 'bmad'); const baseCommandsDir = path.join(projectDir, '.claude', 'commands', 'bmad');
let generatedCount = 0; let generatedCount = 0;
// Generate a command file for each workflow, organized by module // Generate a command file for each standalone workflow, organized by module
for (const workflow of allWorkflows) { for (const workflow of standaloneWorkflows) {
const moduleWorkflowsDir = path.join(baseCommandsDir, workflow.module, 'workflows'); const moduleWorkflowsDir = path.join(baseCommandsDir, workflow.module, 'workflows');
await fs.ensureDir(moduleWorkflowsDir); await fs.ensureDir(moduleWorkflowsDir);
@ -46,7 +46,7 @@ class WorkflowCommandGenerator {
} }
// Also create a workflow launcher README in each module // Also create a workflow launcher README in each module
const groupedWorkflows = this.groupWorkflowsByModule(allWorkflows); const groupedWorkflows = this.groupWorkflowsByModule(standaloneWorkflows);
await this.createModuleWorkflowLaunchers(baseCommandsDir, groupedWorkflows); await this.createModuleWorkflowLaunchers(baseCommandsDir, groupedWorkflows);
return { generated: generatedCount }; return { generated: generatedCount };
@ -59,12 +59,12 @@ class WorkflowCommandGenerator {
return { artifacts: [], counts: { commands: 0, launchers: 0 } }; return { artifacts: [], counts: { commands: 0, launchers: 0 } };
} }
// ALL workflows now generate commands - no standalone filtering // Filter to only standalone workflows
const allWorkflows = workflows; const standaloneWorkflows = workflows.filter((w) => w.standalone === 'true' || w.standalone === true);
const artifacts = []; const artifacts = [];
for (const workflow of allWorkflows) { for (const workflow of standaloneWorkflows) {
const commandContent = await this.generateCommandContent(workflow, bmadDir); const commandContent = await this.generateCommandContent(workflow, bmadDir);
artifacts.push({ artifacts.push({
type: 'workflow-command', type: 'workflow-command',
@ -75,7 +75,7 @@ class WorkflowCommandGenerator {
}); });
} }
const groupedWorkflows = this.groupWorkflowsByModule(allWorkflows); const groupedWorkflows = this.groupWorkflowsByModule(standaloneWorkflows);
for (const [module, launcherContent] of Object.entries(this.buildModuleWorkflowLaunchers(groupedWorkflows))) { for (const [module, launcherContent] of Object.entries(this.buildModuleWorkflowLaunchers(groupedWorkflows))) {
artifacts.push({ artifacts.push({
type: 'workflow-launcher', type: 'workflow-launcher',
@ -89,7 +89,7 @@ class WorkflowCommandGenerator {
return { return {
artifacts, artifacts,
counts: { counts: {
commands: allWorkflows.length, commands: standaloneWorkflows.length,
launchers: Object.keys(groupedWorkflows).length, launchers: Object.keys(groupedWorkflows).length,
}, },
}; };
@ -99,13 +99,8 @@ class WorkflowCommandGenerator {
* Generate command content for a workflow * Generate command content for a workflow
*/ */
async generateCommandContent(workflow, bmadDir) { async generateCommandContent(workflow, bmadDir) {
// Determine template based on workflow file type // Load the template
const isMarkdownWorkflow = workflow.path.endsWith('workflow.md'); const template = await fs.readFile(this.templatePath, 'utf8');
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 // Convert source path to installed path
// From: /Users/.../src/modules/bmm/workflows/.../workflow.yaml // From: /Users/.../src/modules/bmm/workflows/.../workflow.yaml
@ -132,7 +127,9 @@ class WorkflowCommandGenerator {
.replaceAll('{{description}}', workflow.description) .replaceAll('{{description}}', workflow.description)
.replaceAll('{{workflow_path}}', workflowPath) .replaceAll('{{workflow_path}}', workflowPath)
.replaceAll('{bmad_folder}', this.bmadFolderName) .replaceAll('{bmad_folder}', this.bmadFolderName)
.replaceAll('{*bmad_folder*}', '{bmad_folder}'); .replaceAll('{*bmad_folder*}', '{bmad_folder}')
.replaceAll('{{interactive}}', workflow.interactive)
.replaceAll('{{author}}', workflow.author || 'BMAD');
} }
/** /**

View File

@ -1,5 +0,0 @@
---
description: '{{description}}'
---
IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL @{{workflow_path}}, READ its entire contents and follow its directions exactly!