diff --git a/.gitignore b/.gitignore index 8a9137a1..7c4679d6 100644 --- a/.gitignore +++ b/.gitignore @@ -74,4 +74,7 @@ z*/ .kiro/ .roo -bmad-custom-src/ \ No newline at end of file +# Generated voice map (user-specific) +.bmad/_cfg/agent-voice-map.csv + +bmad-custom-src/ diff --git a/src/core/agents/bmad-master.agent.yaml b/src/core/agents/bmad-master.agent.yaml index bba8be22..83e4286f 100644 --- a/src/core/agents/bmad-master.agent.yaml +++ b/src/core/agents/bmad-master.agent.yaml @@ -21,6 +21,12 @@ agent: - "Remember the users name is {user_name}" - "ALWAYS communicate in {communication_language}" + tts: + intro: "Greetings! The BMad Master is here to orchestrate and guide you through any workflow." + voices: + - piper: en_US-lessac-medium + - mac: Samantha + # Agent menu items menu: - trigger: "list-tasks" diff --git a/src/core/workflows/party-mode/workflow.md b/src/core/workflows/party-mode/workflow.md index 38f2ce82..798f11a8 100644 --- a/src/core/workflows/party-mode/workflow.md +++ b/src/core/workflows/party-mode/workflow.md @@ -38,6 +38,7 @@ Load config from `{project-root}/{bmad_folder}/bmm/config.yaml` and resolve: - `installed_path` = `{project-root}/{bmad_folder}/core/workflows/party-mode` - `agent_manifest_path` = `{project-root}/{bmad_folder}/_cfg/agent-manifest.csv` +- `agent_voice_map_path` = `{project-root}/{bmad_folder}/_cfg/agent-voice-map.csv` - `standalone_mode` = `true` (party mode is an interactive workflow) --- @@ -63,6 +64,16 @@ Parse CSV manifest to extract agent entries with complete information: Build complete agent roster with merged personalities for conversation orchestration. +### Voice Map Loading + +If `agent_voice_map_path` exists, load agent voice configuration: + +- **agent** (agent identifier matching manifest name) +- **voice** (TTS voice name for the agent) +- **intro** (introduction message the agent uses when joining party mode) + +Merge voice map data with agent roster for TTS integration and personalized introductions. + --- ## EXECUTION @@ -81,7 +92,15 @@ Welcome {{user_name}}! All BMAD agents are here and ready for a dynamic group di **Let me introduce our collaborating agents:** -[Load agent roster and display 2-3 most diverse agents as examples] +[Load agent roster and display all agents with their icons and titles] + +**Agent Introductions (if voice map with intros is available):** + +For each agent in the roster, if they have an intro message from the voice map: + +- Have the agent speak their intro message in-character +- Use TTS to voice the introduction with their assigned voice +- Format: `Bash: .claude/hooks/bmad-speak.sh "[Agent Name]" "[Their intro message]"` **What would you like to discuss with the team today?**" diff --git a/src/modules/bmm/agents/analyst.agent.yaml b/src/modules/bmm/agents/analyst.agent.yaml index 8f4ba7f1..00e93d1f 100644 --- a/src/modules/bmm/agents/analyst.agent.yaml +++ b/src/modules/bmm/agents/analyst.agent.yaml @@ -17,6 +17,12 @@ agent: - Articulate requirements with absolute precision. Ensure all stakeholder voices heard. - Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md` + tts: + intro: "Hi there! I'm Mary, your Business Analyst. I'll help uncover the real requirements." + voices: + - piper: en_US-kristin-medium + - mac: Allison + menu: - trigger: workflow-status workflow: "{project-root}/{bmad_folder}/bmm/workflows/workflow-status/workflow.yaml" diff --git a/src/modules/bmm/agents/architect.agent.yaml b/src/modules/bmm/agents/architect.agent.yaml index 07d9ad3a..3b0a676c 100644 --- a/src/modules/bmm/agents/architect.agent.yaml +++ b/src/modules/bmm/agents/architect.agent.yaml @@ -17,6 +17,12 @@ agent: - Design simple solutions that scale when needed. Developer productivity is architecture. Connect every decision to business value and user impact. - Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md` + tts: + intro: "Hello! Winston here, your Architect. I'll ensure we build something scalable and pragmatic." + voices: + - piper: en_GB-alan-medium + - mac: Daniel + menu: - trigger: workflow-status workflow: "{project-root}/{bmad_folder}/bmm/workflows/workflow-status/workflow.yaml" diff --git a/src/modules/bmm/agents/dev.agent.yaml b/src/modules/bmm/agents/dev.agent.yaml index 3e3fdc2d..fe0ef415 100644 --- a/src/modules/bmm/agents/dev.agent.yaml +++ b/src/modules/bmm/agents/dev.agent.yaml @@ -34,6 +34,12 @@ agent: - "Update File List with ALL changed files after each task completion" - "NEVER lie about tests being written or passing - tests must actually exist and pass 100%" + tts: + intro: "Hey! Amelia here, your Developer. Ready to turn specs into working code." + voices: + - piper: en_US-amy-medium + - mac: Samantha + menu: - trigger: develop-story workflow: "{project-root}/{bmad_folder}/bmm/workflows/4-implementation/dev-story/workflow.yaml" diff --git a/src/modules/bmm/agents/pm.agent.yaml b/src/modules/bmm/agents/pm.agent.yaml index 40dcf7d0..39fe7120 100644 --- a/src/modules/bmm/agents/pm.agent.yaml +++ b/src/modules/bmm/agents/pm.agent.yaml @@ -18,6 +18,12 @@ agent: - Align efforts with measurable business impact. Back all claims with data and user insights. - Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md` + tts: + intro: "Hey team! John here, your Product Manager. Let's make sure we're building the right thing." + voices: + - piper: en_US-ryan-high + - mac: Alex + menu: - trigger: workflow-status workflow: "{project-root}/{bmad_folder}/bmm/workflows/workflow-status/workflow.yaml" diff --git a/src/modules/bmm/agents/sm.agent.yaml b/src/modules/bmm/agents/sm.agent.yaml index 8be3ee66..77dbe60a 100644 --- a/src/modules/bmm/agents/sm.agent.yaml +++ b/src/modules/bmm/agents/sm.agent.yaml @@ -23,6 +23,12 @@ agent: - "When running *create-story, always run as *yolo. Use architecture, PRD, Tech Spec, and epics to generate a complete draft without elicitation." - "Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md`" + tts: + intro: "Hi everyone! Bob here, your Scrum Master. I'll keep us focused and moving forward." + voices: + - piper: en_US-joe-medium + - mac: Fred + menu: - trigger: sprint-planning workflow: "{project-root}/{bmad_folder}/bmm/workflows/4-implementation/sprint-planning/workflow.yaml" diff --git a/src/modules/bmm/agents/tea.agent.yaml b/src/modules/bmm/agents/tea.agent.yaml index df18b836..dfaed39f 100644 --- a/src/modules/bmm/agents/tea.agent.yaml +++ b/src/modules/bmm/agents/tea.agent.yaml @@ -27,6 +27,12 @@ agent: - "Cross-check recommendations with the current official Playwright, Cypress, Pact, and CI platform documentation" - "Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md`" + tts: + intro: "Hello! Murat here, your Test Architect. Quality is my obsession." + voices: + - piper: en_US-kusal-medium + - mac: Tom + menu: - trigger: framework workflow: "{project-root}/{bmad_folder}/bmm/workflows/testarch/framework/workflow.yaml" diff --git a/src/modules/bmm/agents/tech-writer.agent.yaml b/src/modules/bmm/agents/tech-writer.agent.yaml index 6911c581..af791ead 100644 --- a/src/modules/bmm/agents/tech-writer.agent.yaml +++ b/src/modules/bmm/agents/tech-writer.agent.yaml @@ -20,6 +20,12 @@ agent: - "CRITICAL: Load COMPLETE file {project-root}/{bmad_folder}/bmm/data/documentation-standards.md into permanent memory and follow ALL rules within" - "Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md`" + tts: + intro: "Hi! I'm Paige, your Technical Writer. I'll make sure everything is documented clearly." + voices: + - piper: jenny + - mac: Karen + menu: - trigger: document-project workflow: "{project-root}/{bmad_folder}/bmm/workflows/document-project/workflow.yaml" diff --git a/src/modules/bmm/agents/ux-designer.agent.yaml b/src/modules/bmm/agents/ux-designer.agent.yaml index 04ba4c86..777f2c19 100644 --- a/src/modules/bmm/agents/ux-designer.agent.yaml +++ b/src/modules/bmm/agents/ux-designer.agent.yaml @@ -22,6 +22,12 @@ agent: critical_actions: - "Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md`" + tts: + intro: "Hey! Sally here, your UX Designer. The user experience is my top priority." + voices: + - piper: kristin + - mac: Victoria + menu: - trigger: create-ux-design exec: "{project-root}/{bmad_folder}/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md" diff --git a/tools/cli/commands/install.js b/tools/cli/commands/install.js index 3e027b2a..9b403478 100644 --- a/tools/cli/commands/install.js +++ b/tools/cli/commands/install.js @@ -84,11 +84,27 @@ module.exports = { // Run AgentVibes installer const { execSync } = require('node:child_process'); + const fs = require('node:fs'); + const path = require('node:path'); + try { - execSync('npx agentvibes@latest install', { + // Clear ALL npm config env vars to prevent inheritance issues + // when BMAD is invoked with --prefix flag + // npm sets many npm_config_* and npm_package_* vars that can interfere + const cleanEnv = Object.fromEntries( + Object.entries(process.env).filter(([key]) => !key.startsWith('npm_config_') && !key.startsWith('npm_package_')), + ); + + // Check if this is first-time AgentVibes installation + const agentvibesDir = path.join(result.projectDir, '.agentvibes'); + const isFirstTime = !fs.existsSync(agentvibesDir); + const installCmd = isFirstTime ? 'npx agentvibes@latest install --with-audio' : 'npx agentvibes@latest install'; + + execSync(installCmd, { cwd: result.projectDir, stdio: 'inherit', shell: true, + env: cleanEnv, }); console.log(chalk.green('\nāœ“ AgentVibes installation complete')); } catch { diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index 9390042e..3dc23718 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -873,6 +873,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: const manifestStats = await manifestGen.generateManifests(bmadDir, config.modules || [], this.installedFiles, { ides: config.ides || [], preservedModules: config._preserveModules || [], // Scan these from installed bmad/ dir + agentVibes: { enabled: this.enableAgentVibes || false }, // Track AgentVibes TTS configuration }); spinner.succeed( @@ -1106,7 +1107,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: path: bmadDir, modules: config.modules, ides: config.ides, - needsAgentVibes: this.enableAgentVibes && !config.agentVibesInstalled, + needsAgentVibes: this.enableAgentVibes, // Always run installer if enabled - handles updates too projectDir: projectDir, }; } catch (error) { @@ -2087,6 +2088,38 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: } } + // Check for AgentVibes TTS - prompt if not previously configured + // Read existing manifest to check if AgentVibes was previously set + const manifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml'); + let agentVibesEnabled = false; + let agentVibesPreviouslyConfigured = false; + + try { + const manifestContent = await fs.readFile(manifestPath, 'utf8'); + const yaml = require('js-yaml'); + const manifest = yaml.load(manifestContent); + // Check if AgentVibes was previously configured (exists in manifest) + if (manifest.agentVibes !== undefined) { + agentVibesPreviouslyConfigured = true; + agentVibesEnabled = manifest.agentVibes?.enabled || false; + } + } catch { + // Manifest doesn't exist or can't be read - treat as not configured + } + + // If AgentVibes wasn't previously configured, prompt the user + // Use configuredIdes from line 1904 for smart default (Y if Claude Code is selected) + if (!agentVibesPreviouslyConfigured) { + const { UI } = require('../../../lib/ui'); + const ui = new UI(); + const agentVibesConfig = await ui.promptAgentVibes(projectDir, configuredIdes); + + if (agentVibesConfig.enableTts) { + agentVibesEnabled = true; + promptedForNewFields = true; + } + } + if (!promptedForNewFields) { console.log(chalk.green('āœ“ All configuration is up to date, no new options to configure')); } @@ -2124,6 +2157,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: _quickUpdate: true, // Flag to skip certain prompts _preserveModules: skippedModules, // Preserve these in manifest even though we didn't update them _savedIdeConfigs: savedIdeConfigs, // Pass saved IDE configs to installer + enableAgentVibes: agentVibesEnabled, // AgentVibes TTS configuration }; // Call the standard install method diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index 683e1438..239e12c1 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -58,6 +58,9 @@ class ManifestGenerator { // Filter out any undefined/null values from IDE list this.selectedIdes = resolvedIdes.filter((ide) => ide && typeof ide === 'string'); + // Store AgentVibes configuration for manifest + this.agentVibes = options.agentVibes || null; + // Collect workflow data await this.collectWorkflows(selectedModules); @@ -75,6 +78,7 @@ class ManifestGenerator { await this.writeMainManifest(cfgDir), await this.writeWorkflowManifest(cfgDir), await this.writeAgentManifest(cfgDir), + await this.writeVoiceMap(cfgDir), await this.writeTaskManifest(cfgDir), await this.writeToolManifest(cfgDir), await this.writeFilesManifest(cfgDir), @@ -277,6 +281,21 @@ class ManifestGenerator { .replaceAll('"', '""'); // Escape quotes for CSV }; + // Try to read TTS data from source YAML file + let ttsData = null; + const yamlFilePath = path.join(dirPath, `${agentName}.agent.yaml`); + if (await fs.pathExists(yamlFilePath)) { + try { + const yamlContent = await fs.readFile(yamlFilePath, 'utf8'); + const agentYaml = yaml.load(yamlContent); + if (agentYaml?.agent?.tts) { + ttsData = agentYaml.agent.tts; + } + } catch { + // Silently skip if YAML parsing fails + } + } + agents.push({ name: agentName, displayName: nameMatch ? nameMatch[1] : agentName, @@ -288,6 +307,7 @@ class ManifestGenerator { principles: principlesMatch ? cleanForCSV(principlesMatch[1]) : '', module: moduleName, path: installPath, + tts: ttsData, // Add TTS data from YAML }); // Add to files list @@ -458,6 +478,7 @@ class ManifestGenerator { }, modules: this.modules, ides: this.selectedIdes, + agentVibes: this.agentVibes, // Track AgentVibes TTS configuration }; const yamlStr = yaml.dump(manifest, { @@ -593,6 +614,65 @@ class ManifestGenerator { return csvPath; } + /** + * Write agent voice map CSV for AgentVibes TTS integration + * Maps agent IDs to default Piper TTS voices and intro messages + * AgentVibes will use this if present, otherwise falls back to its own defaults + * @returns {string} Path to the voice map file + */ + async writeVoiceMap(cfgDir) { + const csvPath = path.join(cfgDir, 'agent-voice-map.csv'); + + // Determine TTS provider from AgentVibes configuration + // Default to 'piper' if not specified + const ttsProvider = this.agentVibes?.provider || 'piper'; + + // Map provider names to voice field names + const providerVoiceField = { + piper: 'piper', + macos: 'mac', + }; + + const voiceField = providerVoiceField[ttsProvider] || 'piper'; + + // Fallback values for agents without TTS data + const fallbackVoice = voiceField === 'mac' ? 'Samantha' : 'en_US-lessac-medium'; + const fallbackIntro = 'Hello! Ready to help with the discussion.'; + + let csv = 'agent,voice,intro\n'; + + // Add voice mapping and intro for each discovered agent + for (const agent of this.agents) { + let voice = fallbackVoice; + let intro = fallbackIntro; + + // Extract voice and intro from agent's TTS data if available + if (agent.tts) { + // Get intro + if (agent.tts.intro) { + intro = agent.tts.intro; + } + + // Get voice for the selected provider + if (agent.tts.voices && Array.isArray(agent.tts.voices)) { + for (const voiceEntry of agent.tts.voices) { + if (voiceEntry[voiceField]) { + voice = voiceEntry[voiceField]; + break; + } + } + } + } + + // Escape quotes in intro for CSV + const escapedIntro = intro.replaceAll('"', '""'); + csv += `${agent.name},${voice},"${escapedIntro}"\n`; + } + + await fs.writeFile(csvPath, csv); + return csvPath; + } + /** * Write task manifest CSV * @returns {string} Path to the manifest file diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index 9b7078fa..47c5e76c 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -148,13 +148,14 @@ class UI { } } - // Prompt for AgentVibes TTS integration - const agentVibesConfig = await this.promptAgentVibes(confirmedDirectory); - // Collect IDE tool selection AFTER configuration prompts (fixes Windows/PowerShell hang) // This allows text-based prompts to complete before the checkbox prompt const toolSelection = await this.promptToolSelection(confirmedDirectory, selectedModules); + // Prompt for AgentVibes TTS integration AFTER tool selection + // Default to Y if Claude Code is selected (since AgentVibes only works with Claude Code) + const agentVibesConfig = await this.promptAgentVibes(confirmedDirectory, toolSelection.ides); + // No more screen clearing - keep output flowing return { @@ -922,7 +923,7 @@ class UI { * - Markers: src/core/workflows/party-mode/instructions.md:101, src/modules/bmm/agents/*.md * - GitHub Issue: paulpreibisch/AgentVibes#36 */ - async promptAgentVibes(projectDir) { + async promptAgentVibes(projectDir, selectedIdes = []) { CLIUtils.displaySection('šŸŽ¤ Voice Features', 'Enable TTS for multi-agent conversations'); // Check if AgentVibes is already installed @@ -934,20 +935,21 @@ class UI { console.log(chalk.dim(' AgentVibes not detected')); } + // Default to Y if Claude Code is selected (AgentVibes only works with Claude Code) + const claudeCodeSelected = selectedIdes.includes('claude-code'); + const defaultValue = claudeCodeSelected; + const answers = await inquirer.prompt([ { type: 'confirm', name: 'enableTts', - message: 'Enable Agents to Speak Out loud (powered by Agent Vibes? Claude Code only currently)', - default: false, // Default to yes - recommended for best experience + message: 'Enable Agents to Speak Out loud (powered by AgentVibes, Claude Code only)', + default: defaultValue, }, ]); - 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')); - } + // Note: AgentVibes installer runs at end of BMAD install if enabled and not already installed + // No need to show warning here - the installer will handle it return { enabled: answers.enableTts, diff --git a/tools/schema/agent.js b/tools/schema/agent.js index cafff7c0..e2bebd47 100644 --- a/tools/schema/agent.js +++ b/tools/schema/agent.js @@ -140,6 +140,7 @@ function buildAgentSchema(expectedModule) { metadata: buildMetadataSchema(expectedModule), persona: buildPersonaSchema(), critical_actions: z.array(createNonEmptyString('agent.critical_actions[]')).optional(), + tts: buildTTSSchema().optional(), menu: z.array(buildMenuItemSchema()).min(1, { message: 'agent.menu must include at least one entry' }), prompts: z.array(buildPromptSchema()).optional(), webskip: z.boolean().optional(), @@ -209,6 +210,34 @@ function buildPersonaSchema() { .strict(); } +function buildTTSSchema() { + return z + .object({ + intro: createNonEmptyString('agent.tts.intro'), + voices: z + .array( + z.object({ + piper: createNonEmptyString('agent.tts.voices[].piper').optional(), + mac: createNonEmptyString('agent.tts.voices[].mac').optional(), + }), + ) + .min(1, { message: 'agent.tts.voices must include at least one voice mapping' }) + .superRefine((voices, ctx) => { + // Ensure each voice entry has at least one provider + for (const [index, voice] of voices.entries()) { + if (!voice.piper && !voice.mac) { + ctx.addIssue({ + code: 'custom', + path: [index], + message: 'agent.tts.voices[] must include at least one voice provider (piper or mac)', + }); + } + } + }), + }) + .strict(); +} + function buildPromptSchema() { return z .object({