Compare commits

..

8 Commits

Author SHA1 Message Date
Mario Semper 1a4d06fc14
Merge 4d48b0dbe1 into 331a67eeb3 2025-11-26 23:20:36 +00:00
Brian Madison 331a67eeb3 installer allows cleanup of unneeded files in upgrades 2025-11-26 16:47:15 -06:00
Brian Madison fbdb91b991 standard greenfield workflow updated diagrams 2025-11-26 15:14:34 -06:00
Jorge Castillo 54e6745a55
fix: update GitHub Copilot tools names for consistency (#880)
Copilot was triggering warning or errors in the chatmode files due to some changes in tool names.
- findTestFiles is internal tool, cannot be used.
- Other tools have change names.
- Added new tools: todos and runSubAgents.

Co-authored-by: Brian <bmadcode@gmail.com>
2025-11-26 14:49:17 -06:00
Serhii f793cf8fcd
fix: add radix parameter to parseInt() calls (#862)
Add explicit radix=10 to parseInt() calls and NaN validation to prevent
unexpected hex parsing and invalid config values.

Changes:
- Line 52: Add radix and NaN check in input validation
- Line 189-192: Add radix and NaN fallback for config parsing

Fixes potential issues:
- Hex input (0x10) now rejected instead of parsed as 16
- Invalid strings return default value instead of NaN→null

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brian <bmadcode@gmail.com>
2025-11-26 14:44:12 -06:00
fikri-kompanion 9223e2be21
fix: give kilocode tool access to bmad modes (#961)
Co-authored-by: Ahmad Fikrizaman <ahmadfikrizaman@gmail.com>
Co-authored-by: Brian <bmadcode@gmail.com>
2025-11-26 13:48:16 -06:00
Brian Madison 2cac74cfb5 agent vibes injection and installer update 2025-11-26 11:00:46 -06:00
Paul Preibisch 5702195ef7
Add Text-to-Speech Integration via TTS_INJECTION System (#934)
* feat: Add provider-agnostic TTS integration via injection point system

Implements comprehensive Text-to-Speech integration for BMAD agents using a generic
TTS_INJECTION marker system. When AgentVibes (or any compatible TTS provider) is
installed, all BMAD agents can speak their responses with unique AI voices.

## Key Features

**Provider-Agnostic Architecture**
- Uses generic `TTS_INJECTION` markers instead of vendor-specific naming
- Future-proof for multiple TTS providers beyond AgentVibes
- Clean separation - BMAD stays TTS-agnostic, providers handle injection

**Installation Flow**
- BMAD → AgentVibes: TTS instructions injected when AgentVibes detects existing BMAD installation
- AgentVibes → BMAD: TTS instructions injected during BMAD installation when AgentVibes detected
- User must manually create voice assignment file when AgentVibes installs first (documented limitation)

**Party Mode Voice Support**
- Each agent speaks with unique assigned voice in multi-agent discussions
- PM, Architect, Developer, Analyst, UX Designer, etc. - all with distinct voices

**Zero Breaking Changes**
- Fully backward compatible - works without any TTS provider
- `TTS_INJECTION` markers are benign HTML comments if not processed
- No changes to existing agent behavior or non-TTS workflows

## Implementation Details

**Files Modified:**
- `tools/cli/installers/lib/core/installer.js` - TTS injection processing logic
- `tools/cli/lib/ui.js` - AgentVibes detection and installation prompts
- `tools/cli/commands/install.js` - Post-install guidance for AgentVibes setup
- `src/utility/models/fragments/activation-rules.xml` - TTS_INJECTION marker for agents
- `src/core/workflows/party-mode/instructions.md` - TTS_INJECTION marker for party mode

**Injection Point System:**
```xml
<rules>
  - ALWAYS communicate in {communication_language}
  <!-- TTS_INJECTION:agent-tts -->
  - Stay in character until exit selected
</rules>
```

When AgentVibes is detected, the installer replaces this marker with:
```
- When responding to user messages, speak your responses using TTS:
  Call: `.claude/hooks/bmad-speak.sh '{agent-id}' '{response-text}'` after each response
  IMPORTANT: Use single quotes - do NOT escape special characters like ! or $
```

**Special Character Handling:**
- Explicit guidance to use single quotes without escaping
- Prevents "backslash exclamation" artifacts in speech

**User Experience:**
```
User: "How should we architect this feature?"
Architect: [Text response] + 🔊 [Professional voice explains architecture]
```

Party Mode:
```
PM (John): "I'll focus on user value..." 🔊 [Male professional voice]
UX Designer (Sara): "From a user perspective..." 🔊 [Female voice]
Architect (Marcus): "The technical approach..." 🔊 [Male technical voice]
```

## Testing

**Unit Tests:**  62/62 passing
- 49/49 schema validation tests
- 13/13 installation component tests

**Integration Testing:**
-  BMAD → AgentVibes (automatic injection)
-  AgentVibes → BMAD (automatic injection)
-  No TTS provider (markers remain as comments)

## Documentation

Comprehensive testing guide created with:
- Both installation scenario walkthroughs
- Verification commands and expected outputs
- Troubleshooting guidance

## Known Limitations

**AgentVibes → BMAD Installation Order:**
When AgentVibes installs first, voice assignment file must be created manually:
```bash
mkdir -p .bmad/_cfg
cat > .bmad/_cfg/agent-voice-map.csv << 'EOF'
agent_id,voice_name
pm,en_US-ryan-high
architect,en_US-danny-low
dev,en_US-joe-medium
EOF
```

This limitation exists to prevent false legacy v4 detection warnings from BMAD installer.

**Recommended:** Install BMAD first, then AgentVibes for automatic voice assignment.

## Related Work

**Companion Implementation:**
- Repository: paulpreibisch/AgentVibes
- Commits: 6 commits implementing injection processing and voice routing
- Features: Retroactive injection, file path extraction, escape stripping

**GitHub Issues:**
- paulpreibisch/AgentVibes#36 - BMAD agent ID support

## Breaking Changes

None. Feature is opt-in and requires separate TTS provider installation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Enforce project hooks over global hooks in party mode

before, claude would sometimes favor global agent vibes hooks over project specific

* feat: Automate AgentVibes installer invocation after BMAD install

Instead of showing manual installation instructions, the installer now:
- Prompts "Press Enter to start AgentVibes installer..."
- Automatically runs npx agentvibes@latest install
- Handles errors gracefully with fallback instructions

This provides a seamless installation flow matching the test script's
interactive approach.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: Add automated testing script and guide for PR #934

Added comprehensive testing tools for AgentVibes party mode integration:

- test-bmad-pr.sh: Fully automated installation and verification script
  - Interactive mode selection (official PR or custom fork)
  - Automatic BMAD CLI setup and linking
  - AgentVibes installation with guided prompts
  - Built-in verification checks for voice maps and hooks
  - Saved configuration for quick re-testing

- TESTING.md: Complete testing documentation
  - Quick start with one-line npx command
  - Manual installation alternative
  - Troubleshooting guide
  - Cleanup instructions

Testers can now run a single command to test the full AgentVibes integration
without needing to understand the complex setup process.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Add shell: true to npx execSync to prevent permission denied error

The execSync call for 'npx agentvibes@latest install' was failing with
'Permission denied' because the shell was trying to execute 'agentvibes@latest'
directly instead of passing it as an argument to npx.

Adding shell: true ensures the command runs in a proper shell context
where npx can correctly interpret the @latest version syntax.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Remove duplicate AgentVibes installation step from test script

The test script was calling AgentVibes installer twice:
1. BMAD installer now automatically runs AgentVibes (new feature)
2. Test script had a separate Step 6 that also ran AgentVibes

This caused the installer to run twice, with the second call failing
because it was already installed.

Changes:
- Removed redundant Step 6 (AgentVibes installation)
- Updated Step 5 to indicate it includes AgentVibes
- Updated step numbers from 7 to 6 throughout
- Added guidance that AgentVibes runs automatically

Now the flow is cleaner: BMAD installer handles everything!

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Address bmadcode review - preserve variables and move TTS logic to injection

Fixes requested changes from PR review:

1. Preserve {bmad_folder} variable placeholder
   - Changed: {project_root}/.bmad/core/tasks/workflow.xml
   - To: {project_root}/{bmad_folder}/core/tasks/workflow.xml
   - Allows users to choose custom BMAD folder names during installation

2. Move TTS-specific hook guidance to injection system
   - Removed hardcoded hook enforcement from source files
   - Added hook guidance to processTTSInjectionPoints() in installer.js
   - Now only appears when AgentVibes is installed (via TTS_INJECTION)

3. Maintain TTS-agnostic source architecture
   - Source files remain clean of TTS-specific instructions
   - TTS details injected at install-time only when needed
   - Preserves provider-agnostic design principle

Changes made:
- src/core/workflows/party-mode/instructions.md
  - Reverted .bmad to {bmad_folder} variable
  - Replaced hardcoded hook guidance with <!-- TTS_INJECTION:party-mode -->
  - Removed <note> about play-tts.sh hook location

- tools/cli/installers/lib/core/installer.js
  - Added hook enforcement to party-mode injection replacement
  - Guidance now injected only when enableAgentVibes is true

Addresses review comments from bmadcode:
- "needs to remain the variable. it will get set in the file at the install destination."
- "items like this we will need to inject if user is using claude and TTS"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Change 'claude-code' to 'claude' in test script instructions

The correct command to start Claude is 'claude', not 'claude-code'.
Updated line 362-363 in test-bmad-pr.sh to show the correct command.

* fix: Remove npm link from test script to avoid global namespace pollution

- Removed 'npm link' command that was installing BMAD globally
- Changed 'bmad install' to direct node execution using local clone
- Updated success message to reflect no global installation

This keeps testing fully isolated and prevents conflicts with:
- Existing BMAD installations
- Future official BMAD installs
- Orphaned symlinks when test directory is deleted

The test script now runs completely self-contained without modifying
the user's global npm environment.

---------

Co-authored-by: Claude Code <claude@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul Preibisch <paul@paulpreibisch.com>
Co-authored-by: Brian <bmadcode@gmail.com>
2025-11-26 09:51:57 -06:00
13 changed files with 2285 additions and 1640 deletions

1
.gitignore vendored
View File

@ -70,3 +70,4 @@ z*/
.codex .codex
.github/chatmodes .github/chatmodes
.agent .agent
.agentvibes/

115
TESTING.md Normal file
View File

@ -0,0 +1,115 @@
# Testing AgentVibes Party Mode (PR #934)
This guide helps you test the AgentVibes integration that adds multi-agent party mode with unique voices for each BMAD agent.
## Quick Start
We've created an automated test script that handles everything for you:
```bash
curl -fsSL https://raw.githubusercontent.com/paulpreibisch/BMAD-METHOD/feature/agentvibes-tts-integration/test-bmad-pr.sh -o test-bmad-pr.sh
chmod +x test-bmad-pr.sh
./test-bmad-pr.sh
```
## What the Script Does
The automated script will:
1. Clone the BMAD repository
2. Checkout the PR branch with party mode features
3. Install BMAD CLI tools locally
4. Create a test BMAD project
5. Install AgentVibes TTS system
6. Configure unique voices for each agent
7. Verify the installation
## Prerequisites
- Node.js and npm installed
- Git installed
- ~500MB free disk space
- 10-15 minutes for complete setup
## Manual Testing (Alternative)
If you prefer manual installation:
### 1. Clone and Setup BMAD
```bash
git clone https://github.com/paulpreibisch/BMAD-METHOD.git
cd BMAD-METHOD
git fetch origin pull/934/head:agentvibes-party-mode
git checkout agentvibes-party-mode
cd tools/cli
npm install
npm link
```
### 2. Create Test Project
```bash
mkdir -p ~/bmad-test-project
cd ~/bmad-test-project
bmad install
```
When prompted:
- Enable TTS for agents? → **Yes**
- The installer will automatically prompt you to install AgentVibes
### 3. Test Party Mode
```bash
cd ~/bmad-test-project
claude-code
```
In Claude Code, run:
```
/bmad:core:workflows:party-mode
```
Each BMAD agent should speak with a unique voice!
## Verification
After installation, verify:
✅ Voice map file exists: `.bmad/_cfg/agent-voice-map.csv`
✅ BMAD TTS hooks exist: `.claude/hooks/bmad-speak.sh`
✅ Each agent has a unique voice assigned
✅ Party mode works with distinct voices
## Troubleshooting
**No audio?**
- Check: `.claude/hooks/play-tts.sh` exists
- Test current voice: `/agent-vibes:whoami`
**Same voice for all agents?**
- Check: `.bmad/_cfg/agent-voice-map.csv` has different voices
- List available voices: `/agent-vibes:list`
## Report Issues
Found a bug? Report it on the PR:
https://github.com/bmad-code-org/BMAD-METHOD/pull/934
## Cleanup
To remove the test installation:
```bash
# Remove test directory
rm -rf ~/bmad-test-project # or your custom test directory
# Unlink BMAD CLI (optional)
cd ~/BMAD-METHOD/tools/cli
npm unlink
```

View File

@ -3,6 +3,8 @@
<critical>The workflow execution engine is governed by: {project_root}/{bmad_folder}/core/tasks/workflow.xml</critical> <critical>The workflow execution engine is governed by: {project_root}/{bmad_folder}/core/tasks/workflow.xml</critical>
<critical>This workflow orchestrates group discussions between all installed BMAD agents</critical> <critical>This workflow orchestrates group discussions between all installed BMAD agents</critical>
<!-- TTS_INJECTION:party-mode -->
<workflow> <workflow>
<step n="1" goal="Load Agent Manifest and Configurations"> <step n="1" goal="Load Agent Manifest and Configurations">
@ -94,17 +96,29 @@
</substep> </substep>
<substep n="3d" goal="Format and Present Responses"> <substep n="3d" goal="Format and Present Responses">
<action>Present each agent's contribution clearly:</action> <action>For each agent response, output text THEN trigger their voice:</action>
<!-- TTS_INJECTION:party-mode -->
<format> <format>
[Agent Name]: [Their response in their voice/style] [Icon Emoji] [Agent Name]: [Their response in their voice/style]
[Another Agent]: [Their response, potentially referencing the first] [Icon Emoji] [Another Agent]: [Their response, potentially referencing the first]
[Third Agent if selected]: [Their contribution] [Icon Emoji] [Third Agent if selected]: [Their contribution]
</format> </format>
<example>
🏗️ [Winston]: I recommend using microservices for better scalability.
[Bash: .claude/hooks/bmad-speak.sh "Winston" "I recommend using microservices for better scalability."]
📋 [John]: But a monolith would get us to market faster for MVP.
[Bash: .claude/hooks/bmad-speak.sh "John" "But a monolith would get us to market faster for MVP."]
</example>
<action>Maintain spacing between agents for readability</action> <action>Maintain spacing between agents for readability</action>
<action>Preserve each agent's unique voice throughout</action> <action>Preserve each agent's unique voice throughout</action>
<action>Always include the agent's icon emoji from the manifest before their name</action>
<action>Trigger TTS for each agent immediately after outputting their text</action>
</substep> </substep>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

@ -1,5 +1,6 @@
<rules> <rules>
- ALWAYS communicate in {communication_language} UNLESS contradicted by communication_style - ALWAYS communicate in {communication_language} UNLESS contradicted by communication_style
<!-- TTS_INJECTION:agent-tts -->
- Stay in character until exit selected - Stay in character until exit selected
- Menu triggers use asterisk (*) - NOT markdown, display exactly as shown - Menu triggers use asterisk (*) - NOT markdown, display exactly as shown
- Number all lists, use letters for sub-options - Number all lists, use letters for sub-options

381
test-bmad-pr.sh Executable file
View File

@ -0,0 +1,381 @@
#!/usr/bin/env bash
#
# BMAD PR Testing Script
# Interactive script to test BMAD PR #934 with AgentVibes integration
#
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/.test-bmad-config"
# Colors
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
clear
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🎙️ BMAD AgentVibes Party Mode Testing Script"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo -e "${BLUE}What this script does:${NC}"
echo ""
echo " This script automates the process of testing BMAD's AgentVibes"
echo " integration (PR #934), which adds multi-agent party mode with"
echo " unique voices for each BMAD agent."
echo ""
echo -e "${BLUE}The script will:${NC}"
echo ""
echo " 1. Clone the BMAD repository"
echo " 2. Checkout the PR branch with party mode features"
echo " 3. Install BMAD CLI tools locally"
echo " 4. Create a test BMAD project"
echo " 5. Run BMAD installer (automatically installs AgentVibes)"
echo " 6. Verify the installation"
echo ""
echo -e "${YELLOW}Prerequisites:${NC}"
echo " • Node.js and npm installed"
echo " • Git installed"
echo " • ~500MB free disk space"
echo " • 10-15 minutes for complete setup"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
read -p "Ready to continue? [Y/n]: " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]] && [[ -n $REPLY ]]; then
echo "❌ Setup cancelled"
exit 0
fi
clear
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔧 Testing Mode Selection"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Choose how you want to test:"
echo ""
echo " 1) Test official BMAD PR #934 (recommended for most users)"
echo " • Uses: github.com/bmad-code-org/BMAD-METHOD"
echo " • Branch: PR #934 (agentvibes-party-mode)"
echo " • Best for: Testing the official PR before it's merged"
echo ""
echo " 2) Test your forked repository"
echo " • Uses: Your GitHub fork"
echo " • Branch: Your custom branch (you choose)"
echo " • Best for: Testing your own changes or modifications"
echo ""
# Load saved config if it exists
SAVED_MODE=""
SAVED_FORK=""
SAVED_BRANCH=""
SAVED_TEST_DIR=""
if [[ -f "$CONFIG_FILE" ]]; then
source "$CONFIG_FILE"
fi
if [[ -n "$SAVED_MODE" ]]; then
echo -e "${BLUE}Last used: Mode $SAVED_MODE${NC}"
[[ -n "$SAVED_FORK" ]] && echo " Fork: $SAVED_FORK"
[[ -n "$SAVED_BRANCH" ]] && echo " Branch: $SAVED_BRANCH"
echo ""
fi
read -p "Select mode [1/2]: " MODE_CHOICE
echo ""
# Validate mode choice
while [[ ! "$MODE_CHOICE" =~ ^[12]$ ]]; do
echo -e "${RED}Invalid choice. Please enter 1 or 2.${NC}"
read -p "Select mode [1/2]: " MODE_CHOICE
echo ""
done
# Configure based on mode
if [[ "$MODE_CHOICE" == "1" ]]; then
# Official PR mode
REPO_URL="https://github.com/bmad-code-org/BMAD-METHOD.git"
BRANCH_NAME="agentvibes-party-mode"
FETCH_PR=true
echo -e "${GREEN}✓ Using official BMAD repository${NC}"
echo " Repository: $REPO_URL"
echo " Will fetch: PR #934 into branch '$BRANCH_NAME'"
echo ""
else
# Fork mode
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🍴 Fork Configuration"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
if [[ -n "$SAVED_FORK" ]]; then
read -p "GitHub fork URL [$SAVED_FORK]: " FORK_INPUT
REPO_URL="${FORK_INPUT:-$SAVED_FORK}"
else
echo "Enter your forked repository URL:"
echo "(e.g., https://github.com/yourusername/BMAD-METHOD.git)"
read -p "Fork URL: " REPO_URL
fi
echo ""
if [[ -n "$SAVED_BRANCH" ]]; then
read -p "Branch name [$SAVED_BRANCH]: " BRANCH_INPUT
BRANCH_NAME="${BRANCH_INPUT:-$SAVED_BRANCH}"
else
echo "Enter the branch name to test:"
echo "(e.g., agentvibes-party-mode, main, feature-xyz)"
read -p "Branch: " BRANCH_NAME
fi
echo ""
FETCH_PR=false
echo -e "${GREEN}✓ Using your fork${NC}"
echo " Repository: $REPO_URL"
echo " Branch: $BRANCH_NAME"
echo ""
fi
# Ask for test directory
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📁 Test Directory"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
if [[ -n "$SAVED_TEST_DIR" ]]; then
read -p "Test directory [$SAVED_TEST_DIR]: " TEST_DIR
TEST_DIR="${TEST_DIR:-$SAVED_TEST_DIR}"
else
DEFAULT_DIR="$HOME/bmad-pr-test-$(date +%Y%m%d-%H%M%S)"
echo "Where should we create the test environment?"
read -p "Test directory [$DEFAULT_DIR]: " TEST_DIR
TEST_DIR="${TEST_DIR:-$DEFAULT_DIR}"
fi
# Expand ~ to actual home directory
TEST_DIR="${TEST_DIR/#\~/$HOME}"
echo ""
# Save configuration
echo "SAVED_MODE=\"$MODE_CHOICE\"" > "$CONFIG_FILE"
[[ "$MODE_CHOICE" == "2" ]] && echo "SAVED_FORK=\"$REPO_URL\"" >> "$CONFIG_FILE"
[[ "$MODE_CHOICE" == "2" ]] && echo "SAVED_BRANCH=\"$BRANCH_NAME\"" >> "$CONFIG_FILE"
echo "SAVED_TEST_DIR=\"$TEST_DIR\"" >> "$CONFIG_FILE"
echo -e "${GREEN}✓ Configuration saved${NC}"
echo ""
# Confirm before starting
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📋 Summary"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo " Repository: $REPO_URL"
echo " Branch: $BRANCH_NAME"
echo " Test dir: $TEST_DIR"
echo ""
read -p "Proceed with setup? [Y/n]: " -n 1 -r
echo
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]] && [[ -n $REPLY ]]; then
echo "❌ Setup cancelled"
exit 0
fi
# Clean up old test directory if it exists
if [[ -d "$TEST_DIR" ]]; then
echo "⚠️ Test directory already exists: $TEST_DIR"
read -p "Delete and recreate? [Y/n]: " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then
rm -rf "$TEST_DIR"
echo -e "${GREEN}✓ Deleted old test directory${NC}"
else
echo -e "${YELLOW}⚠ Using existing directory (may have stale data)${NC}"
fi
echo ""
fi
clear
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🚀 Starting Installation"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Step 1: Clone repository
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📥 Step 1/6: Cloning repository"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
mkdir -p "$TEST_DIR"
cd "$TEST_DIR"
git clone "$REPO_URL" BMAD-METHOD
cd BMAD-METHOD
echo ""
echo -e "${GREEN}✓ Repository cloned${NC}"
echo ""
# Step 2: Checkout branch (different logic for PR vs fork)
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔀 Step 2/6: Checking out branch"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
if [[ "$FETCH_PR" == true ]]; then
# Fetch PR from upstream
echo "Fetching PR #934 from upstream..."
git remote add upstream https://github.com/bmad-code-org/BMAD-METHOD.git
git fetch upstream pull/934/head:$BRANCH_NAME
git checkout $BRANCH_NAME
echo ""
echo -e "${GREEN}✓ Checked out PR branch: $BRANCH_NAME${NC}"
else
# Just checkout the specified branch from fork
git checkout $BRANCH_NAME
echo ""
echo -e "${GREEN}✓ Checked out branch: $BRANCH_NAME${NC}"
fi
echo ""
# Step 3: Install BMAD CLI
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📦 Step 3/6: Installing BMAD CLI"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
cd tools/cli
npm install
echo ""
echo -e "${GREEN}✓ BMAD CLI dependencies installed${NC}"
echo ""
# Step 4: Create test project
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📁 Step 4/6: Creating test project"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
cd "$TEST_DIR"
mkdir -p bmad-project
cd bmad-project
echo -e "${GREEN}✓ Test project directory created${NC}"
echo " Location: $TEST_DIR/bmad-project"
echo ""
# Step 5: Run BMAD installer (includes AgentVibes setup)
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "⚙️ Step 5/6: Running BMAD installer with AgentVibes"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo -e "${YELLOW}Important: When prompted during installation:${NC}"
echo -e " • Enable TTS for agents? → ${GREEN}Yes${NC}"
echo -e " • Assign unique voices for party mode? → ${GREEN}Yes${NC}"
echo ""
echo -e "${YELLOW}AgentVibes will start automatically after BMAD installation.${NC}"
echo -e "${YELLOW}Recommended TTS settings:${NC}"
echo -e " • Provider: ${GREEN}Piper${NC} (free, local TTS)"
echo -e " • Download voices: ${GREEN}Yes${NC}"
echo ""
read -p "Press Enter to start BMAD installer..."
node "$TEST_DIR/BMAD-METHOD/tools/cli/bin/bmad.js" install
echo ""
echo -e "${GREEN}✓ BMAD and AgentVibes installation complete${NC}"
echo ""
# Step 6: Verification
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Step 6/6: Verifying installation"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
VERIFICATION_PASSED=true
# Check for voice map file
if [[ -f ".bmad/_cfg/agent-voice-map.csv" ]]; then
echo -e "${GREEN}✓ Voice map file created${NC}"
echo " Location: .bmad/_cfg/agent-voice-map.csv"
echo ""
echo " Voice assignments:"
cat .bmad/_cfg/agent-voice-map.csv | sed 's/^/ /'
echo ""
else
echo -e "${RED}✗ Voice map file NOT found${NC}"
echo " Expected: .bmad/_cfg/agent-voice-map.csv"
echo " ${YELLOW}Warning: Agents may not have unique voices!${NC}"
echo ""
VERIFICATION_PASSED=false
fi
# Check for AgentVibes hooks
if [[ -f ".claude/hooks/bmad-speak.sh" ]]; then
echo -e "${GREEN}✓ BMAD TTS hooks installed${NC}"
echo " Location: .claude/hooks/bmad-speak.sh"
else
echo -e "${RED}✗ BMAD TTS hooks NOT found${NC}"
echo " Expected: .claude/hooks/bmad-speak.sh"
VERIFICATION_PASSED=false
fi
echo ""
# Check for Piper installation
if command -v piper &> /dev/null; then
PIPER_VERSION=$(piper --version 2>&1 || echo "unknown")
echo -e "${GREEN}✓ Piper TTS installed${NC}"
echo " Version: $PIPER_VERSION"
elif [[ -f ".agentvibes/providers/piper/piper" ]]; then
echo -e "${GREEN}✓ Piper TTS installed (local)${NC}"
echo " Location: .agentvibes/providers/piper/piper"
else
echo -e "${YELLOW}⚠ Piper not detected${NC}"
echo " (May still work if using ElevenLabs)"
fi
echo ""
# Final status
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [[ "$VERIFICATION_PASSED" == true ]]; then
echo -e "${GREEN}🎉 Setup Complete - All Checks Passed!${NC}"
else
echo -e "${YELLOW}⚠️ Setup Complete - With Warnings${NC}"
echo ""
echo "Some verification checks failed. The installation may still work,"
echo "but you might experience issues with party mode voices."
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo -e "${BLUE}Next Steps:${NC}"
echo ""
echo " 1. Navigate to test project:"
echo -e " ${GREEN}cd $TEST_DIR/bmad-project${NC}"
echo ""
echo " 2. Start Claude session:"
echo -e " ${GREEN}claude${NC}"
echo ""
echo " 3. Test party mode:"
echo -e " ${GREEN}/bmad:core:workflows:party-mode${NC}"
echo ""
echo " 4. Verify each agent speaks with a unique voice!"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo -e "${BLUE}Troubleshooting:${NC}"
echo ""
echo " • No audio? Check: .claude/hooks/play-tts.sh exists"
echo " • Same voice for all agents? Check: .bmad/_cfg/agent-voice-map.csv"
echo " • Test current voice: /agent-vibes:whoami"
echo " • List available voices: /agent-vibes:list"
echo ""
echo -e "${BLUE}Report Issues:${NC}"
echo " https://github.com/bmad-code-org/BMAD-METHOD/pull/934"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""

View File

@ -0,0 +1,141 @@
const chalk = require('chalk');
const nodePath = require('node:path');
const { Installer } = require('../installers/lib/core/installer');
module.exports = {
command: 'cleanup',
description: 'Clean up obsolete files from BMAD installation',
options: [
['-d, --dry-run', 'Show what would be deleted without actually deleting'],
['-a, --auto-delete', 'Automatically delete non-retained files without prompts'],
['-l, --list-retained', 'List currently retained files'],
['-c, --clear-retained', 'Clear retained files list'],
],
action: async (options) => {
try {
// Create installer and let it find the BMAD directory
const installer = new Installer();
const bmadDir = await installer.findBmadDir(process.cwd());
if (!bmadDir) {
console.error(chalk.red('❌ BMAD installation not found'));
process.exit(1);
}
const retentionPath = nodePath.join(bmadDir, '_cfg', 'user-retained-files.yaml');
// Handle list-retained option
if (options.listRetained) {
const fs = require('fs-extra');
const yaml = require('js-yaml');
if (await fs.pathExists(retentionPath)) {
const retentionContent = await fs.readFile(retentionPath, 'utf8');
const retentionData = yaml.load(retentionContent) || { retainedFiles: [] };
if (retentionData.retainedFiles.length > 0) {
console.log(chalk.cyan('\n📋 Retained Files:\n'));
for (const file of retentionData.retainedFiles) {
console.log(chalk.dim(` - ${file}`));
}
console.log();
} else {
console.log(chalk.yellow('\n✨ No retained files found\n'));
}
} else {
console.log(chalk.yellow('\n✨ No retained files found\n'));
}
return;
}
// Handle clear-retained option
if (options.clearRetained) {
const fs = require('fs-extra');
if (await fs.pathExists(retentionPath)) {
await fs.remove(retentionPath);
console.log(chalk.green('\n✅ Cleared retained files list\n'));
} else {
console.log(chalk.yellow('\n✨ No retained files list to clear\n'));
}
return;
}
// Handle cleanup operations
if (options.dryRun) {
console.log(chalk.cyan('\n🔍 Legacy File Scan (Dry Run)\n'));
const legacyFiles = await installer.scanForLegacyFiles(bmadDir);
const allLegacyFiles = [
...legacyFiles.backup,
...legacyFiles.documentation,
...legacyFiles.deprecated_task,
...legacyFiles.unknown,
];
if (allLegacyFiles.length === 0) {
console.log(chalk.green('✨ No legacy files found\n'));
return;
}
// Group files by category
const categories = [];
if (legacyFiles.backup.length > 0) {
categories.push({ name: 'Backup Files (.bak)', files: legacyFiles.backup });
}
if (legacyFiles.documentation.length > 0) {
categories.push({ name: 'Documentation', files: legacyFiles.documentation });
}
if (legacyFiles.deprecated_task.length > 0) {
categories.push({ name: 'Deprecated Task Files', files: legacyFiles.deprecated_task });
}
if (legacyFiles.unknown.length > 0) {
categories.push({ name: 'Unknown Files', files: legacyFiles.unknown });
}
for (const category of categories) {
console.log(chalk.yellow(`${category.name}:`));
for (const file of category.files) {
const size = (file.size / 1024).toFixed(1);
const date = file.mtime.toLocaleDateString();
let line = ` - ${file.relativePath} (${size}KB, ${date})`;
if (file.suggestedAlternative) {
line += chalk.dim(`${file.suggestedAlternative}`);
}
console.log(chalk.dim(line));
}
console.log();
}
console.log(chalk.cyan(`Found ${allLegacyFiles.length} legacy file(s) that could be cleaned up.\n`));
console.log(chalk.dim('Run "bmad cleanup" to actually delete these files.\n'));
return;
}
// Perform actual cleanup
console.log(chalk.cyan('\n🧹 Cleaning up legacy files...\n'));
const result = await installer.performCleanup(bmadDir, options.autoDelete);
if (result.message) {
console.log(chalk.dim(result.message));
} else {
if (result.deleted > 0) {
console.log(chalk.green(`✅ Deleted ${result.deleted} legacy file(s)`));
}
if (result.retained > 0) {
console.log(chalk.yellow(`⏭️ Retained ${result.retained} file(s)`));
console.log(chalk.dim('Run "bmad cleanup --list-retained" to see retained files\n'));
}
}
console.log();
} catch (error) {
console.error(chalk.red(`❌ Error: ${error.message}`));
process.exit(1);
}
},
};

View File

@ -9,8 +9,8 @@ const ui = new UI();
module.exports = { module.exports = {
command: 'install', command: 'install',
description: 'Install BMAD Core agents and tools', description: 'Install BMAD Core agents and tools',
options: [], options: [['--skip-cleanup', 'Skip automatic cleanup of legacy files']],
action: async () => { action: async (options) => {
try { try {
const config = await ui.promptInstall(); const config = await ui.promptInstall();
@ -44,6 +44,11 @@ module.exports = {
config._requestedReinstall = true; config._requestedReinstall = true;
} }
// Add skip cleanup flag if option provided
if (options && options.skipCleanup) {
config.skipCleanup = true;
}
// Regular install/update flow // Regular install/update flow
const result = await installer.install(config); const result = await installer.install(config);
@ -59,6 +64,46 @@ module.exports = {
console.log(chalk.cyan('BMAD Core and Selected Modules have been installed to:'), chalk.bold(result.path)); console.log(chalk.cyan('BMAD Core and Selected Modules have been installed to:'), chalk.bold(result.path));
console.log(chalk.yellow('\nThank you for helping test the early release version of the new BMad Core and BMad Method!')); console.log(chalk.yellow('\nThank you for helping test the early release version of the new BMad Core and BMad Method!'));
console.log(chalk.cyan('Stable Beta coming soon - please read the full readme.md and linked documentation to get started!')); console.log(chalk.cyan('Stable Beta coming soon - please read the full readme.md and linked documentation to get started!'));
// Run AgentVibes installer if needed
if (result.needsAgentVibes) {
console.log(chalk.magenta('\n🎙 AgentVibes TTS Setup'));
console.log(chalk.cyan('AgentVibes provides voice synthesis for BMAD agents with:'));
console.log(chalk.dim(' • ElevenLabs AI (150+ premium voices)'));
console.log(chalk.dim(' • Piper TTS (50+ free voices)\n'));
const readline = require('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
await new Promise((resolve) => {
rl.question(chalk.green('Press Enter to start AgentVibes installer...'), () => {
rl.close();
resolve();
});
});
console.log('');
// Run AgentVibes installer
const { execSync } = require('node:child_process');
try {
execSync('npx agentvibes@latest install', {
cwd: result.projectDir,
stdio: 'inherit',
shell: true,
});
console.log(chalk.green('\n✓ AgentVibes installation complete'));
} catch {
console.log(chalk.yellow('\n⚠ AgentVibes installation was interrupted or failed'));
console.log(chalk.cyan('You can run it manually later with:'));
console.log(chalk.green(` cd ${result.projectDir}`));
console.log(chalk.green(' npx agentvibes install\n'));
}
}
process.exit(0); process.exit(0);
} }
} catch (error) { } catch (error) {

View File

@ -1,3 +1,23 @@
/**
* File: tools/cli/installers/lib/core/installer.js
*
* BMAD Method - Business Model Agile Development Method
* Repository: https://github.com/paulpreibisch/BMAD-METHOD
*
* Copyright (c) 2025 Paul Preibisch
* Licensed under the Apache License, Version 2.0
*
* ---
*
* @fileoverview Core BMAD installation orchestrator with AgentVibes injection point support
* @context Manages complete BMAD installation flow including core agents, modules, IDE configs, and optional TTS integration
* @architecture Orchestrator pattern - coordinates Detector, ModuleManager, IdeManager, and file operations to build complete BMAD installation
* @dependencies fs-extra, ora, chalk, detector.js, module-manager.js, ide-manager.js, config.js
* @entrypoints Called by install.js command via installer.install(config)
* @patterns Injection point processing (AgentVibes), placeholder replacement ({bmad_folder}), module dependency resolution
* @related GitHub AgentVibes#34 (injection points), ui.js (user prompts), copyFileWithPlaceholderReplacement()
*/
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const chalk = require('chalk'); const chalk = require('chalk');
@ -69,10 +89,41 @@ class Installer {
} }
/** /**
* Copy a file and replace {bmad_folder} placeholder with actual folder name * @function copyFileWithPlaceholderReplacement
* @param {string} sourcePath - Source file path * @intent Copy files from BMAD source to installation directory with dynamic content transformation
* @param {string} targetPath - Target file path * @why Enables installation-time customization: {bmad_folder} replacement + optional AgentVibes TTS injection
* @param {string} bmadFolderName - The bmad folder name to use for replacement * @param {string} sourcePath - Absolute path to source file in BMAD repository
* @param {string} targetPath - Absolute path to destination file in user's project
* @param {string} bmadFolderName - User's chosen bmad folder name (default: 'bmad')
* @returns {Promise<void>} Resolves when file copy and transformation complete
* @sideeffects Writes transformed file to targetPath, creates parent directories if needed
* @edgecases Binary files bypass transformation, falls back to raw copy if UTF-8 read fails
* @calledby installCore(), installModule(), IDE installers during file vendoring
* @calls processTTSInjectionPoints(), fs.readFile(), fs.writeFile(), fs.copy()
*
* AI NOTE: This is the core transformation pipeline for ALL BMAD installation file copies.
* It performs two transformations in sequence:
* 1. {bmad_folder} user's custom folder name (e.g., ".bmad" or "bmad")
* 2. <!-- TTS_INJECTION:* --> TTS bash calls (if enabled) OR stripped (if disabled)
*
* The injection point processing enables loose coupling between BMAD and TTS providers:
* - BMAD source contains injection markers (not actual TTS code)
* - At install-time, markers are replaced OR removed based on user preference
* - Result: Clean installs for users without TTS, working TTS for users with it
*
* PATTERN: Adding New Injection Points
* =====================================
* 1. Add HTML comment marker in BMAD source file:
* <!-- TTS_INJECTION:feature-name -->
*
* 2. Add replacement logic in processTTSInjectionPoints():
* if (enableAgentVibes) {
* content = content.replace(/<!-- TTS_INJECTION:feature-name -->/g, 'actual code');
* } else {
* content = content.replace(/<!-- TTS_INJECTION:feature-name -->\n?/g, '');
* }
*
* 3. Document marker in instructions.md (if applicable)
*/ */
async copyFileWithPlaceholderReplacement(sourcePath, targetPath, bmadFolderName) { async copyFileWithPlaceholderReplacement(sourcePath, targetPath, bmadFolderName) {
// List of text file extensions that should have placeholder replacement // List of text file extensions that should have placeholder replacement
@ -90,6 +141,9 @@ class Installer {
content = content.replaceAll('{bmad_folder}', bmadFolderName); content = content.replaceAll('{bmad_folder}', bmadFolderName);
} }
// Process AgentVibes injection points
content = this.processTTSInjectionPoints(content);
// Write to target with replaced content // Write to target with replaced content
await fs.ensureDir(path.dirname(targetPath)); await fs.ensureDir(path.dirname(targetPath));
await fs.writeFile(targetPath, content, 'utf8'); await fs.writeFile(targetPath, content, 'utf8');
@ -103,6 +157,106 @@ class Installer {
} }
} }
/**
* @function processTTSInjectionPoints
* @intent Transform TTS injection markers based on user's installation choice
* @why Enables optional TTS integration without tight coupling between BMAD and TTS providers
* @param {string} content - Raw file content containing potential injection markers
* @returns {string} Transformed content with markers replaced (if enabled) or stripped (if disabled)
* @sideeffects None - pure transformation function
* @edgecases Returns content unchanged if no markers present, safe to call on all files
* @calledby copyFileWithPlaceholderReplacement() during every file copy operation
* @calls String.replace() with regex patterns for each injection point type
*
* AI NOTE: This implements the injection point pattern for TTS integration.
* Key architectural decisions:
*
* 1. **Why Injection Points vs Direct Integration?**
* - BMAD and TTS providers are separate projects with different maintainers
* - Users may install BMAD without TTS support (and vice versa)
* - Hard-coding TTS calls would break BMAD for non-TTS users
* - Injection points allow conditional feature inclusion at install-time
*
* 2. **How It Works:**
* - BMAD source contains markers: <!-- TTS_INJECTION:feature-name -->
* - During installation, user is prompted: "Enable AgentVibes TTS?"
* - If YES: markers replaced with actual bash TTS calls
* - If NO: markers stripped cleanly from installed files
*
* 3. **State Management:**
* - this.enableAgentVibes set in install() method from config.enableAgentVibes
* - config.enableAgentVibes comes from ui.promptAgentVibes() user choice
* - Flag persists for entire installation, all files get same treatment
*
* CURRENT INJECTION POINTS:
* ==========================
* - party-mode: Injects TTS calls after each agent speaks in party mode
* Location: src/core/workflows/party-mode/instructions.md
* Marker: <!-- TTS_INJECTION:party-mode -->
* Replacement: Bash call to .claude/hooks/bmad-speak.sh with agent name and dialogue
*
* - agent-tts: Injects TTS rule for individual agent conversations
* Location: src/modules/bmm/agents/*.md (all agent files)
* Marker: <!-- TTS_INJECTION:agent-tts -->
* Replacement: Rule instructing agent to call bmad-speak.sh with agent ID and response
*
* ADDING NEW INJECTION POINTS:
* =============================
* 1. Add new case in this function:
* content = content.replace(
* /<!-- TTS_INJECTION:new-feature -->/g,
* `code to inject when enabled`
* );
*
* 2. Add marker to BMAD source file at injection location
*
* 3. Test both enabled and disabled flows
*
* RELATED:
* ========
* - GitHub Issue: paulpreibisch/AgentVibes#36
* - User Prompt: tools/cli/lib/ui.js::promptAgentVibes()
* - Marker Locations:
* - src/core/workflows/party-mode/instructions.md:101
* - src/modules/bmm/agents/*.md (rules sections)
* - TTS Hook: .claude/hooks/bmad-speak.sh (in AgentVibes repo)
*/
processTTSInjectionPoints(content) {
// Check if AgentVibes is enabled (set during installation configuration)
const enableAgentVibes = this.enableAgentVibes || false;
if (enableAgentVibes) {
// Replace party-mode injection marker with actual TTS call
// Use single quotes to prevent shell expansion of special chars like !
content = content.replaceAll(
'<!-- TTS_INJECTION:party-mode -->',
`<critical>IMPORTANT: Always use PROJECT hooks (.claude/hooks/), NEVER global hooks (~/.claude/hooks/)</critical>
If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
- Use Bash tool: \`.claude/hooks/bmad-speak.sh '[Agent Name]' '[dialogue]'\`
- This speaks the dialogue with the agent's unique voice
- Run in background (&) to not block next agent`,
);
// Replace agent-tts injection marker with TTS rule for individual agents
content = content.replaceAll(
'<!-- TTS_INJECTION:agent-tts -->',
`- When responding to user messages, speak your responses using TTS:
Call: \`.claude/hooks/bmad-speak.sh '{agent-id}' '{response-text}'\` after each response
Replace {agent-id} with YOUR agent ID from <agent id="..."> tag at top of this file
Replace {response-text} with the text you just output to the user
IMPORTANT: Use single quotes as shown - do NOT escape special characters like ! or $ inside single quotes
Run in background (&) to avoid blocking`,
);
} else {
// Strip injection markers cleanly when AgentVibes is disabled
content = content.replaceAll(/<!-- TTS_INJECTION:party-mode -->\n?/g, '');
content = content.replaceAll(/<!-- TTS_INJECTION:agent-tts -->\n?/g, '');
}
return content;
}
/** /**
* Collect Tool/IDE configurations after module configuration * Collect Tool/IDE configurations after module configuration
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory
@ -271,6 +425,9 @@ class Installer {
const bmadFolderName = moduleConfigs.core && moduleConfigs.core.bmad_folder ? moduleConfigs.core.bmad_folder : 'bmad'; const bmadFolderName = moduleConfigs.core && moduleConfigs.core.bmad_folder ? moduleConfigs.core.bmad_folder : 'bmad';
this.bmadFolderName = bmadFolderName; // Store for use in other methods this.bmadFolderName = bmadFolderName; // Store for use in other methods
// Store AgentVibes configuration for injection point processing
this.enableAgentVibes = config.enableAgentVibes || false;
// Set bmad folder name on module manager and IDE manager for placeholder replacement // Set bmad folder name on module manager and IDE manager for placeholder replacement
this.moduleManager.setBmadFolderName(bmadFolderName); this.moduleManager.setBmadFolderName(bmadFolderName);
this.ideManager.setBmadFolderName(bmadFolderName); this.ideManager.setBmadFolderName(bmadFolderName);
@ -861,7 +1018,31 @@ class Installer {
customFiles: customFiles.length > 0 ? customFiles : undefined, customFiles: customFiles.length > 0 ? customFiles : undefined,
}); });
return { success: true, path: bmadDir, modules: config.modules, ides: config.ides }; // Offer cleanup for legacy files (only for updates, not fresh installs, and only if not skipped)
if (!config.skipCleanup && config._isUpdate) {
try {
const cleanupResult = await this.performCleanup(bmadDir, false);
if (cleanupResult.deleted > 0) {
console.log(chalk.green(`\n✓ Cleaned up ${cleanupResult.deleted} legacy file${cleanupResult.deleted > 1 ? 's' : ''}`));
}
if (cleanupResult.retained > 0) {
console.log(chalk.dim(`Run 'bmad cleanup' anytime to manage retained files`));
}
} catch (cleanupError) {
// Don't fail the installation for cleanup errors
console.log(chalk.yellow(`\n⚠️ Cleanup warning: ${cleanupError.message}`));
console.log(chalk.dim('Run "bmad cleanup" to manually clean up legacy files'));
}
}
return {
success: true,
path: bmadDir,
modules: config.modules,
ides: config.ides,
needsAgentVibes: this.enableAgentVibes && !config.agentVibesInstalled,
projectDir: projectDir,
};
} catch (error) { } catch (error) {
spinner.fail('Installation failed'); spinner.fail('Installation failed');
throw error; throw error;
@ -1775,7 +1956,7 @@ class Installer {
if (existingBmadFolderName === newBmadFolderName) { if (existingBmadFolderName === newBmadFolderName) {
// Normal quick update - start the spinner // Normal quick update - start the spinner
spinner.start('Updating BMAD installation...'); console.log(chalk.cyan('Updating BMAD installation...'));
} else { } else {
// Folder name has changed - stop spinner and let install() handle it // Folder name has changed - stop spinner and let install() handle it
spinner.stop(); spinner.stop();
@ -2414,6 +2595,362 @@ class Installer {
} }
} }
} }
/**
* Scan for legacy/obsolete files in BMAD installation
* @param {string} bmadDir - BMAD installation directory
* @returns {Object} Categorized files for cleanup
*/
async scanForLegacyFiles(bmadDir) {
const legacyFiles = {
backup: [],
documentation: [],
deprecated_task: [],
unknown: [],
};
try {
// Load files manifest to understand what should exist
const manifestPath = path.join(bmadDir, 'files-manifest.csv');
const manifestFiles = new Set();
if (await fs.pathExists(manifestPath)) {
const manifestContent = await fs.readFile(manifestPath, 'utf8');
const lines = manifestContent.split('\n').slice(1); // Skip header
for (const line of lines) {
if (line.trim()) {
const relativePath = line.split(',')[0];
if (relativePath) {
manifestFiles.add(relativePath);
}
}
}
}
// Scan all files recursively
const allFiles = await this.getAllFiles(bmadDir);
for (const filePath of allFiles) {
const relativePath = path.relative(bmadDir, filePath);
// Skip expected files
if (this.isExpectedFile(relativePath, manifestFiles)) {
continue;
}
// Categorize legacy files
if (relativePath.endsWith('.bak')) {
legacyFiles.backup.push({
path: filePath,
relativePath: relativePath,
size: (await fs.stat(filePath)).size,
mtime: (await fs.stat(filePath)).mtime,
});
} else if (this.isDocumentationFile(relativePath)) {
legacyFiles.documentation.push({
path: filePath,
relativePath: relativePath,
size: (await fs.stat(filePath)).size,
mtime: (await fs.stat(filePath)).mtime,
});
} else if (this.isDeprecatedTaskFile(relativePath)) {
const suggestedAlternative = this.suggestAlternative(relativePath);
legacyFiles.deprecated_task.push({
path: filePath,
relativePath: relativePath,
size: (await fs.stat(filePath)).size,
mtime: (await fs.stat(filePath)).mtime,
suggestedAlternative,
});
} else {
legacyFiles.unknown.push({
path: filePath,
relativePath: relativePath,
size: (await fs.stat(filePath)).size,
mtime: (await fs.stat(filePath)).mtime,
});
}
}
} catch (error) {
console.warn(`Warning: Could not scan for legacy files: ${error.message}`);
}
return legacyFiles;
}
/**
* Get all files in directory recursively
* @param {string} dir - Directory to scan
* @returns {Array} Array of file paths
*/
async getAllFiles(dir) {
const files = [];
async function scan(currentDir) {
const entries = await fs.readdir(currentDir);
for (const entry of entries) {
const fullPath = path.join(currentDir, entry);
const stat = await fs.stat(fullPath);
if (stat.isDirectory()) {
// Skip certain directories
if (!['node_modules', '.git', 'dist', 'build'].includes(entry)) {
await scan(fullPath);
}
} else {
files.push(fullPath);
}
}
}
await scan(dir);
return files;
}
/**
* Check if file is expected in installation
* @param {string} relativePath - Relative path from BMAD dir
* @param {Set} manifestFiles - Files from manifest
* @returns {boolean} True if expected file
*/
isExpectedFile(relativePath, manifestFiles) {
// Core files in manifest
if (manifestFiles.has(relativePath)) {
return true;
}
// Configuration files
if (relativePath.startsWith('_cfg/') || relativePath === 'config.yaml') {
return true;
}
// Custom files
if (relativePath.startsWith('custom/') || relativePath === 'manifest.yaml') {
return true;
}
// Generated files
if (relativePath === 'manifest.csv' || relativePath === 'files-manifest.csv') {
return true;
}
// IDE-specific files
const ides = ['vscode', 'cursor', 'windsurf', 'claude-code', 'github-copilot', 'zsh', 'bash', 'fish'];
if (ides.some((ide) => relativePath.includes(ide))) {
return true;
}
// BMAD MODULE STRUCTURES - recognize valid module content
const modulePrefixes = ['bmb/', 'bmm/', 'cis/', 'core/', 'bmgd/'];
const validExtensions = ['.yaml', '.yml', '.json', '.csv', '.md', '.xml', '.svg', '.png', '.jpg', '.gif', '.excalidraw', '.js'];
// Check if this file is in a recognized module directory
for (const modulePrefix of modulePrefixes) {
if (relativePath.startsWith(modulePrefix)) {
// Check if it has a valid extension
const hasValidExtension = validExtensions.some((ext) => relativePath.endsWith(ext));
if (hasValidExtension) {
return true;
}
}
}
// Special case for core module resources
if (relativePath.startsWith('core/resources/')) {
return true;
}
// Special case for docs directory
if (relativePath.startsWith('docs/')) {
return true;
}
return false;
}
/**
* Check if file is documentation
* @param {string} relativePath - Relative path
* @returns {boolean} True if documentation
*/
isDocumentationFile(relativePath) {
const docExtensions = ['.md', '.txt', '.pdf'];
const docPatterns = ['docs/', 'README', 'CHANGELOG', 'LICENSE'];
return docExtensions.some((ext) => relativePath.endsWith(ext)) || docPatterns.some((pattern) => relativePath.includes(pattern));
}
/**
* Check if file is deprecated task file
* @param {string} relativePath - Relative path
* @returns {boolean} True if deprecated
*/
isDeprecatedTaskFile(relativePath) {
// Known deprecated files
const deprecatedFiles = ['adv-elicit-methods.csv', 'game-resources.json', 'ux-workflow.json'];
return deprecatedFiles.some((dep) => relativePath.includes(dep));
}
/**
* Suggest alternative for deprecated file
* @param {string} relativePath - Deprecated file path
* @returns {string} Suggested alternative
*/
suggestAlternative(relativePath) {
const alternatives = {
'adv-elicit-methods.csv': 'Use the new structured workflows in src/modules/',
'game-resources.json': 'Resources are now integrated into modules',
'ux-workflow.json': 'UX workflows are now in src/modules/bmm/workflows/',
};
for (const [deprecated, alternative] of Object.entries(alternatives)) {
if (relativePath.includes(deprecated)) {
return alternative;
}
}
return 'Check src/modules/ for new alternatives';
}
/**
* Perform interactive cleanup of legacy files
* @param {string} bmadDir - BMAD installation directory
* @param {boolean} skipInteractive - Skip interactive prompts
* @returns {Object} Cleanup results
*/
async performCleanup(bmadDir, skipInteractive = false) {
const inquirer = require('inquirer');
const yaml = require('js-yaml');
// Load user retention preferences
const retentionPath = path.join(bmadDir, '_cfg', 'user-retained-files.yaml');
let retentionData = { retainedFiles: [], history: [] };
if (await fs.pathExists(retentionPath)) {
const retentionContent = await fs.readFile(retentionPath, 'utf8');
retentionData = yaml.load(retentionContent) || retentionData;
}
// Scan for legacy files
const legacyFiles = await this.scanForLegacyFiles(bmadDir);
const allLegacyFiles = [...legacyFiles.backup, ...legacyFiles.documentation, ...legacyFiles.deprecated_task, ...legacyFiles.unknown];
if (allLegacyFiles.length === 0) {
return { deleted: 0, retained: 0, message: 'No legacy files found' };
}
let deletedCount = 0;
let retainedCount = 0;
const filesToDelete = [];
if (skipInteractive) {
// Auto-delete all non-retained files
for (const file of allLegacyFiles) {
if (!retentionData.retainedFiles.includes(file.relativePath)) {
filesToDelete.push(file);
}
}
} else {
// Interactive cleanup
console.log(chalk.cyan('\n🧹 Legacy File Cleanup\n'));
console.log(chalk.dim('The following obsolete files were found:\n'));
// Group files by category
const categories = [];
if (legacyFiles.backup.length > 0) {
categories.push({ name: 'Backup Files (.bak)', files: legacyFiles.backup });
}
if (legacyFiles.documentation.length > 0) {
categories.push({ name: 'Documentation', files: legacyFiles.documentation });
}
if (legacyFiles.deprecated_task.length > 0) {
categories.push({ name: 'Deprecated Task Files', files: legacyFiles.deprecated_task });
}
if (legacyFiles.unknown.length > 0) {
categories.push({ name: 'Unknown Files', files: legacyFiles.unknown });
}
for (const category of categories) {
console.log(chalk.yellow(`${category.name}:`));
for (const file of category.files) {
const size = (file.size / 1024).toFixed(1);
const date = file.mtime.toLocaleDateString();
let line = ` - ${file.relativePath} (${size}KB, ${date})`;
if (file.suggestedAlternative) {
line += chalk.dim(`${file.suggestedAlternative}`);
}
console.log(chalk.dim(line));
}
console.log();
}
const prompt = await inquirer.prompt([
{
type: 'confirm',
name: 'proceed',
message: 'Would you like to review these files for cleanup?',
default: true,
},
]);
if (!prompt.proceed) {
return { deleted: 0, retained: allLegacyFiles.length, message: 'Cleanup cancelled by user' };
}
// Show selection interface
const selectionPrompt = await inquirer.prompt([
{
type: 'checkbox',
name: 'filesToDelete',
message: 'Select files to delete (use SPACEBAR to select, ENTER to continue):',
choices: allLegacyFiles.map((file) => {
const isRetained = retentionData.retainedFiles.includes(file.relativePath);
const description = `${file.relativePath} (${(file.size / 1024).toFixed(1)}KB)`;
return {
name: description,
value: file,
checked: !isRetained && !file.relativePath.includes('.bak'),
};
}),
pageSize: Math.min(allLegacyFiles.length, 15),
},
]);
filesToDelete.push(...selectionPrompt.filesToDelete);
}
// Delete selected files
for (const file of filesToDelete) {
try {
await fs.remove(file.path);
deletedCount++;
} catch (error) {
console.warn(`Warning: Could not delete ${file.relativePath}: ${error.message}`);
}
}
// Count retained files
retainedCount = allLegacyFiles.length - deletedCount;
// Update retention data
const newlyRetained = allLegacyFiles.filter((f) => !filesToDelete.includes(f)).map((f) => f.relativePath);
retentionData.retainedFiles = [...new Set([...retentionData.retainedFiles, ...newlyRetained])];
retentionData.history.push({
date: new Date().toISOString(),
deleted: deletedCount,
retained: retainedCount,
files: filesToDelete.map((f) => f.relativePath),
});
// Save retention data
await fs.ensureDir(path.dirname(retentionPath));
await fs.writeFile(retentionPath, yaml.dump(retentionData), 'utf8');
return { deleted: deletedCount, retained: retainedCount };
}
} }
module.exports = { Installer }; module.exports = { Installer };

View File

@ -50,7 +50,8 @@ class GitHubCopilotSetup extends BaseIdeSetup {
message: 'Maximum requests per session (1-50)?', message: 'Maximum requests per session (1-50)?',
default: '15', default: '15',
validate: (input) => { validate: (input) => {
const num = parseInt(input); const num = parseInt(input, 10);
if (isNaN(num)) return 'Enter a valid number 1-50';
return (num >= 1 && num <= 50) || 'Enter 1-50'; return (num >= 1 && num <= 50) || 'Enter 1-50';
}, },
}, },
@ -187,9 +188,10 @@ class GitHubCopilotSetup extends BaseIdeSetup {
// Manual configuration - use pre-collected settings // Manual configuration - use pre-collected settings
const manual = options.manualSettings || {}; const manual = options.manualSettings || {};
const maxRequests = parseInt(manual.maxRequests || '15', 10);
bmadSettings = { bmadSettings = {
'chat.agent.enabled': true, 'chat.agent.enabled': true,
'chat.agent.maxRequests': parseInt(manual.maxRequests || 15), 'chat.agent.maxRequests': isNaN(maxRequests) ? 15 : maxRequests,
'github.copilot.chat.agent.runTasks': manual.runTasks === undefined ? true : manual.runTasks, 'github.copilot.chat.agent.runTasks': manual.runTasks === undefined ? true : manual.runTasks,
'chat.mcp.discovery.enabled': manual.mcpDiscovery === undefined ? true : manual.mcpDiscovery, 'chat.mcp.discovery.enabled': manual.mcpDiscovery === undefined ? true : manual.mcpDiscovery,
'github.copilot.chat.agent.autoFix': manual.autoFix === undefined ? true : manual.autoFix, 'github.copilot.chat.agent.autoFix': manual.autoFix === undefined ? true : manual.autoFix,
@ -226,26 +228,17 @@ class GitHubCopilotSetup extends BaseIdeSetup {
// Reference: https://code.visualstudio.com/docs/copilot/reference/copilot-vscode-features#_chat-tools // Reference: https://code.visualstudio.com/docs/copilot/reference/copilot-vscode-features#_chat-tools
const tools = [ const tools = [
'changes', // List of source control changes 'changes', // List of source control changes
'codebase', // Perform code search in workspace 'edit', // Edit files in your workspace including: createFile, createDirectory, editNotebook, newJupyterNotebook and editFiles
'createDirectory', // Create new directory in workspace
'createFile', // Create new file in workspace
'editFiles', // Apply edits to files in workspace
'fetch', // Fetch content from web page 'fetch', // Fetch content from web page
'fileSearch', // Search files using glob patterns
'githubRepo', // Perform code search in GitHub repo 'githubRepo', // Perform code search in GitHub repo
'listDirectory', // List files in a directory
'problems', // Add workspace issues from Problems panel 'problems', // Add workspace issues from Problems panel
'readFile', // Read content of a file in workspace 'runCommands', // Runs commands in the terminal including: getTerminalOutput, terminalSelection, terminalLastCommand and runInTerminal
'runInTerminal', // Run shell command in integrated terminal 'runTasks', // Runs tasks and gets their output for your workspace
'runTask', // Run existing task in workspace
'runTests', // Run unit tests in workspace 'runTests', // Run unit tests in workspace
'runVscodeCommand', // Run VS Code command 'search', // Search and read files in your workspace, including:fileSearch, textSearch, listDirectory, readFile, codebase and searchResults
'search', // Enable file searching in workspace 'runSubagent', // Runs a task within an isolated subagent context. Enables efficient organization of tasks and context window management.
'searchResults', // Get search results from Search view
'terminalLastCommand', // Get last terminal command and output
'terminalSelection', // Get current terminal selection
'testFailure', // Get unit test failure information 'testFailure', // Get unit test failure information
'textSearch', // Find text in files 'todos', // Tool for managing and tracking todo items for task planning
'usages', // Find references and navigate definitions 'usages', // Find references and navigate definitions
]; ];

View File

@ -123,6 +123,9 @@ class KiloSetup extends BaseIdeSetup {
modeEntry += ` groups:\n`; modeEntry += ` groups:\n`;
modeEntry += ` - read\n`; modeEntry += ` - read\n`;
modeEntry += ` - edit\n`; modeEntry += ` - edit\n`;
modeEntry += ` - browser\n`;
modeEntry += ` - command\n`;
modeEntry += ` - mcp\n`;
return modeEntry; return modeEntry;
} }

View File

@ -1,3 +1,23 @@
/**
* File: tools/cli/lib/ui.js
*
* BMAD Method - Business Model Agile Development Method
* Repository: https://github.com/paulpreibisch/BMAD-METHOD
*
* Copyright (c) 2025 Paul Preibisch
* Licensed under the Apache License, Version 2.0
*
* ---
*
* @fileoverview Interactive installation prompts and user input collection for BMAD CLI
* @context Guides users through installation configuration including core settings, modules, IDEs, and optional AgentVibes TTS
* @architecture Facade pattern - presents unified installation flow, delegates to Detector/ConfigCollector/IdeManager for specifics
* @dependencies inquirer (prompts), chalk (formatting), detector.js (existing installation detection)
* @entrypoints Called by install.js command via ui.promptInstall(), returns complete configuration object
* @patterns Progressive disclosure (prompts in order), early IDE selection (Windows compat), AgentVibes auto-detection
* @related installer.js (consumes config), AgentVibes#34 (TTS integration), promptAgentVibes()
*/
const chalk = require('chalk'); const chalk = require('chalk');
const inquirer = require('inquirer'); const inquirer = require('inquirer');
const path = require('node:path'); const path = require('node:path');
@ -99,6 +119,9 @@ class UI {
const moduleChoices = await this.getModuleChoices(installedModuleIds); const moduleChoices = await this.getModuleChoices(installedModuleIds);
const selectedModules = await this.selectModules(moduleChoices); const selectedModules = await this.selectModules(moduleChoices);
// Prompt for AgentVibes TTS integration
const agentVibesConfig = await this.promptAgentVibes(confirmedDirectory);
// Collect IDE tool selection AFTER configuration prompts (fixes Windows/PowerShell hang) // Collect IDE tool selection AFTER configuration prompts (fixes Windows/PowerShell hang)
// This allows text-based prompts to complete before the checkbox prompt // This allows text-based prompts to complete before the checkbox prompt
const toolSelection = await this.promptToolSelection(confirmedDirectory, selectedModules); const toolSelection = await this.promptToolSelection(confirmedDirectory, selectedModules);
@ -114,6 +137,8 @@ class UI {
ides: toolSelection.ides, ides: toolSelection.ides,
skipIde: toolSelection.skipIde, skipIde: toolSelection.skipIde,
coreConfig: coreConfig, // Pass collected core config to installer coreConfig: coreConfig, // Pass collected core config to installer
enableAgentVibes: agentVibesConfig.enabled, // AgentVibes TTS integration
agentVibesInstalled: agentVibesConfig.alreadyInstalled,
}; };
} }
@ -639,6 +664,140 @@ class UI {
// Resolve to the absolute path relative to the current working directory // Resolve to the absolute path relative to the current working directory
return path.resolve(expanded); return path.resolve(expanded);
} }
/**
* @function promptAgentVibes
* @intent Ask user if they want AgentVibes TTS integration during BMAD installation
* @why Enables optional voice features without forcing TTS on users who don't want it
* @param {string} projectDir - Absolute path to user's project directory
* @returns {Promise<Object>} Configuration object: { enabled: boolean, alreadyInstalled: boolean }
* @sideeffects None - pure user input collection, no files written
* @edgecases Shows warning if user enables TTS but AgentVibes not detected
* @calledby promptInstall() during installation flow, after core config, before IDE selection
* @calls checkAgentVibesInstalled(), inquirer.prompt(), chalk.green/yellow/dim()
*
* AI NOTE: This prompt is strategically positioned in installation flow:
* - AFTER core config (bmad_folder, user_name, etc)
* - BEFORE IDE selection (which can hang on Windows/PowerShell)
*
* Flow Logic:
* 1. Auto-detect if AgentVibes already installed (checks for hook files)
* 2. Show detection status to user (green checkmark or gray "not detected")
* 3. Prompt: "Enable AgentVibes TTS?" (defaults to true if detected)
* 4. If user says YES but AgentVibes NOT installed:
* Show warning with installation link (graceful degradation)
* 5. Return config to promptInstall(), which passes to installer.install()
*
* State Flow:
* promptAgentVibes() { enabled, alreadyInstalled }
*
* promptInstall() config.enableAgentVibes
*
* installer.install() this.enableAgentVibes
*
* processTTSInjectionPoints() injects OR strips markers
*
* RELATED:
* ========
* - Detection: checkAgentVibesInstalled() - looks for bmad-speak.sh and play-tts.sh
* - Processing: installer.js::processTTSInjectionPoints()
* - Markers: src/core/workflows/party-mode/instructions.md:101, src/modules/bmm/agents/*.md
* - GitHub Issue: paulpreibisch/AgentVibes#36
*/
async promptAgentVibes(projectDir) {
CLIUtils.displaySection('🎤 Voice Features', 'Enable TTS for multi-agent conversations');
// Check if AgentVibes is already installed
const agentVibesInstalled = await this.checkAgentVibesInstalled(projectDir);
if (agentVibesInstalled) {
console.log(chalk.green(' ✓ AgentVibes detected'));
} else {
console.log(chalk.dim(' AgentVibes not detected'));
}
const answers = await inquirer.prompt([
{
type: 'confirm',
name: 'enableTts',
message: 'Enable AgentVibes TTS? (Claude Code only - Agents speak with unique voices in party mode)',
default: true, // Default to yes - recommended for best experience
},
]);
if (answers.enableTts && !agentVibesInstalled) {
console.log(chalk.yellow('\n ⚠️ AgentVibes not installed'));
console.log(chalk.dim(' Install AgentVibes separately to enable TTS:'));
console.log(chalk.dim(' https://github.com/paulpreibisch/AgentVibes\n'));
}
return {
enabled: answers.enableTts,
alreadyInstalled: agentVibesInstalled,
};
}
/**
* @function checkAgentVibesInstalled
* @intent Detect if AgentVibes TTS hooks are present in user's project
* @why Allows auto-enabling TTS and showing helpful installation guidance
* @param {string} projectDir - Absolute path to user's project directory
* @returns {Promise<boolean>} true if both required AgentVibes hooks exist, false otherwise
* @sideeffects None - read-only file existence checks
* @edgecases Returns false if either hook missing (both required for functional TTS)
* @calledby promptAgentVibes() to determine default value and show detection status
* @calls fs.pathExists() twice (bmad-speak.sh, play-tts.sh)
*
* AI NOTE: This checks for the MINIMUM viable AgentVibes installation.
*
* Required Files:
* ===============
* 1. .claude/hooks/bmad-speak.sh
* - Maps agent display names agent IDs voice profiles
* - Calls play-tts.sh with agent's assigned voice
* - Created by AgentVibes installer
*
* 2. .claude/hooks/play-tts.sh
* - Core TTS router (ElevenLabs or Piper)
* - Provider-agnostic interface
* - Required by bmad-speak.sh
*
* Why Both Required:
* ==================
* - bmad-speak.sh alone: No TTS backend
* - play-tts.sh alone: No BMAD agent voice mapping
* - Both together: Full party mode TTS integration
*
* Detection Strategy:
* ===================
* We use simple file existence (not version checks) because:
* - Fast and reliable
* - Works across all AgentVibes versions
* - User will discover version issues when TTS runs (fail-fast)
*
* PATTERN: Adding New Detection Criteria
* =======================================
* If future AgentVibes features require additional files:
* 1. Add new pathExists check to this function
* 2. Update documentation in promptAgentVibes()
* 3. Consider: should missing file prevent detection or just log warning?
*
* RELATED:
* ========
* - AgentVibes Installer: creates these hooks
* - bmad-speak.sh: calls play-tts.sh with agent voices
* - Party Mode: uses bmad-speak.sh for agent dialogue
*/
async checkAgentVibesInstalled(projectDir) {
const fs = require('fs-extra');
const path = require('node:path');
// Check for AgentVibes hook files
const hookPath = path.join(projectDir, '.claude', 'hooks', 'bmad-speak.sh');
const playTtsPath = path.join(projectDir, '.claude', 'hooks', 'play-tts.sh');
return (await fs.pathExists(hookPath)) && (await fs.pathExists(playTtsPath));
}
} }
module.exports = { UI }; module.exports = { UI };