Compare commits

...

8 Commits

Author SHA1 Message Date
Serhii 817cc5585e
Merge 9e2595503f into 5ee1551b5b 2025-12-06 05:49:12 +02:00
Alex Verkhovsky 5ee1551b5b
fix(bmm): remove stale validate-prd references (fixes #1030) (#1038)
- Remove validate-prd workflow references from all workflow path YAML files
- Update Excalidraw diagram: remove Validate PRD box and zombie JSON elements
- Re-export SVG at 1x scale
- Standardize implementation-readiness descriptions across all docs
- Add validation script (validate-svg-changes.sh) and README for SVG export process
- Correct Excalidraw timestamps

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

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brian <bmadcode@gmail.com>
2025-12-05 21:35:46 -06:00
Alex Verkhovsky c95b65f462
fix(bmm): correct code-review workflow status logic and checklist (#1015) (#1028)
- Fix checklist to only accept 'review' status (not 'ready-for-review')
- Include MEDIUM issues in done/in-progress status determination
- Initialize and track fixed_count/action_count variables for summary
- Add sprint-status.yaml sync when story status changes

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-05 21:27:11 -06:00
Nguyen Quang Huy 72ef9e9722
fix: use backticks for quoted phrase in code-review description (#1025)
Replace 'looks good' with `looks good` to avoid nested single quote
issues when IDEs generate command files from workflow YAML.

Co-authored-by: Brian <bmadcode@gmail.com>
2025-12-05 21:26:04 -06:00
Paul Preibisch 8265bbf295
feat(installer): Enhanced TTS injection summary with tracking and documentation (#1037)
## Summary
- Track all files with TTS injection applied during installation
- Display informative summary explaining what TTS injection does
- Show backup location and restore command for recovery

## What is TTS Injection?
TTS (Text-to-Speech) injection adds voice instructions to BMAD agents,
enabling them to speak their responses aloud using AgentVibes.

Example: When you activate the PM agent, it will greet you with
spoken audio like "Hey! I'm your Project Manager. How can I help?"

## Changes
- **installer.js**: Track files in `processAgentFiles()`, `buildStandaloneAgents()`,
  and `rebuildAgentFiles()` when TTS markers are processed
- **compiler.js**: Add TTS injection support for custom agent compilation
- **ui.js**: Enhanced installation summary showing:
  - Explanation of what TTS injection is with example
  - List of all files with TTS injection applied (grouped by type)
  - Backup location (~/.bmad-tts-backups/)
  - Restore command for recovery

## Example Output
```
═══════════════════════════════════════════════════
            AgentVibes TTS Injection Summary
═══════════════════════════════════════════════════

What is TTS Injection?

  TTS (Text-to-Speech) injection adds voice instructions to BMAD agents,
  enabling them to speak their responses aloud using AgentVibes.

  Example: When you activate the PM agent, it will greet you with
  spoken audio like "Hey! I'm your Project Manager. How can I help?"

 TTS injection applied to 11 file(s):

  Party Mode (multi-agent conversations):
    • .bmad/core/workflows/party-mode/instructions.md
  Agent TTS (individual agent voices):
    • .bmad/bmm/agents/analyst.md
    • .bmad/bmm/agents/architect.md
    ...

Backups & Recovery:

  Pre-injection backups are stored in:
    ~/.bmad-tts-backups/

  To restore original files (removes TTS instructions):
    bmad-tts-injector.sh --restore /path/to/.bmad
```

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

Co-authored-by: Paul Preibisch <paul@paulpreibisch.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-05 18:54:03 -06:00
Brian 9e2595503f
Merge branch 'main' into refactor/use-path-basename-consistently 2025-11-19 20:53:58 -06:00
Serhii 28ecbfe9b5
Merge branch 'main' into refactor/use-path-basename-consistently 2025-11-06 21:30:36 +02:00
Serhii 033aa717f4
refactor: use path.basename() consistently for file extensions
Replace .replace('.agent.yaml', '') with path.basename(file, '.agent.yaml')
for consistency and correct handling of edge cases.

Changes:
- buildAllAgents(): line 238
- checkBuildStatus(): line 333
- listAvailableAgents(): line 452

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 06:21:16 +02:00
18 changed files with 650 additions and 259 deletions

View File

@ -76,8 +76,7 @@ The BMad Method Module (BMM) provides a comprehensive team of specialized AI age
- `create-prd` - Create PRD for Level 2-4 projects (creates FRs/NFRs only)
- `tech-spec` - Quick spec for Level 0-1 projects
- `create-epics-and-stories` - Break PRD into implementable pieces (runs AFTER architecture)
- `validate-prd` - Validate PRD completeness
- `validate-tech-spec` - Validate Technical Specification
- `implementation-readiness` - Validate PRD + Architecture + Epics + UX (optional)
- `correct-course` - Handle mid-project changes
- `workflow-init` - Initialize workflow tracking
@ -146,7 +145,7 @@ The BMad Method Module (BMM) provides a comprehensive team of specialized AI age
- `workflow-status` - Check what to do next
- `create-architecture` - Produce a Scale Adaptive Architecture
- `validate-architecture` - Validate architecture document
- `implementation-readiness` - Validate readiness for Phase 4
- `implementation-readiness` - Validate PRD + Architecture + Epics + UX (optional)
**Communication Style:** Comprehensive yet pragmatic. Uses architectural metaphors. Balances technical depth with accessibility. Connects decisions to business value.
@ -642,13 +641,12 @@ Some workflows are available to multiple agents:
Many workflows have optional validation workflows that perform independent review:
| Validation | Agent | Validates |
| ----------------------- | ----------- | -------------------------------- |
| `validate-prd` | PM | PRD completeness (FRs/NFRs only) |
| `validate-tech-spec` | PM | Technical specification quality |
| `validate-architecture` | Architect | Architecture document |
| `validate-design` | UX Designer | UX specification and artifacts |
| `validate-create-story` | SM | Story draft |
| Validation | Agent | Validates |
| -------------------------- | ----------- | ------------------------------------------ |
| `implementation-readiness` | Architect | PRD + Architecture + Epics + UX (optional) |
| `validate-architecture` | Architect | Architecture document |
| `validate-design` | UX Designer | UX specification and artifacts |
| `validate-create-story` | SM | Story draft |
**When to use validation:**
@ -945,9 +943,8 @@ Agent analyzes project state → recommends next workflow
```
Each phase has validation gates:
- Phase 2 to 3: validate-prd, validate-tech-spec
- Phase 3 to 4: implementation-readiness
Run validation before advancing
- Phase 3 to 4: implementation-readiness (validates PRD + Architecture + Epics + UX (optional))
Run validation before advancing to implementation
```
**Course correction:**

View File

@ -147,7 +147,7 @@ If status file exists, use workflow-status. If not, use workflow-init.
### Q: How do I know when Phase 3 is complete and I can start Phase 4?
**A:** For Level 3-4, run the implementation-readiness workflow. It validates that PRD (FRs/NFRs), architecture, epics+stories, and UX (if applicable) are cohesive before implementation. Pass the gate check = ready for Phase 4.
**A:** For Level 3-4, run the implementation-readiness workflow. It validates PRD + Architecture + Epics + UX (optional) are aligned before implementation. Pass the gate check = ready for Phase 4.
### Q: Can I run workflows in parallel or do they have to be sequential?

View File

@ -246,7 +246,7 @@ Workflow that initializes Phase 4 implementation by creating sprint-status.yaml,
### Gate Check
Validation workflow (implementation-readiness) run before Phase 4 to ensure PRD, architecture, and UX documents are cohesive with no gaps or contradictions. Required for BMad Method and Enterprise Method tracks.
Validation workflow (implementation-readiness) run before Phase 4 to ensure PRD + Architecture + Epics + UX (optional) are aligned with no gaps or contradictions. Required for BMad Method and Enterprise Method tracks.
### DoD (Definition of Done)

View File

@ -0,0 +1,37 @@
# Workflow Diagram Maintenance
## Regenerating SVG from Excalidraw
When you edit `workflow-method-greenfield.excalidraw`, regenerate the SVG:
1. Open https://excalidraw.com/
2. Load the `.excalidraw` file
3. Click menu (☰) → Export image → SVG
4. **Set "Scale" to 1x** (default is 2x)
5. Click "Export"
6. Save as `workflow-method-greenfield.svg`
7. **Validate the changes** (see below)
8. Commit both files together
**Important:**
- Always use **1x scale** to maintain consistent dimensions
- Automated export tools (`excalidraw-to-svg`) are broken - use manual export only
## Visual Validation
After regenerating the SVG, validate that it renders correctly:
```bash
./tools/validate-svg-changes.sh src/modules/bmm/docs/images/workflow-method-greenfield.svg
```
This script:
- Checks for required dependencies (Playwright, ImageMagick)
- Installs Playwright locally if needed (no package.json pollution)
- Renders old vs new SVG using browser-accurate rendering
- Compares pixel-by-pixel and generates a diff image
- Outputs a prompt for AI visual analysis (paste into Gemini/Claude)
**Threshold**: <0.01% difference is acceptable (anti-aliasing variations)

View File

@ -1036,10 +1036,6 @@
"type": "arrow",
"id": "arrow-discovery-no"
},
{
"type": "arrow",
"id": "arrow-prd-validate"
},
{
"id": "arrow-phase1-to-phase2",
"type": "arrow"
@ -1055,17 +1051,21 @@
{
"id": "arrow-has-ui-no",
"type": "arrow"
},
{
"id": "arrow-prd-hasui",
"type": "arrow"
}
],
"locked": false,
"version": 107,
"versionNonce": 930129274,
"version": 108,
"versionNonce": 930129275,
"index": "aN",
"isDeleted": false,
"strokeStyle": "solid",
"seed": 1,
"frameId": null,
"updated": 1764191563350,
"updated": 1764952855000,
"link": null
},
{
@ -1107,197 +1107,6 @@
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "arrow-prd-validate",
"type": "arrow",
"x": 439.4640518625828,
"y": 331.0450590268819,
"width": 0.17283039375342923,
"height": 28.50332681186643,
"angle": 0,
"strokeColor": "#1976d2",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"roughness": 0,
"opacity": 100,
"groupIds": [],
"startBinding": {
"elementId": "proc-prd",
"focus": 0,
"gap": 1
},
"endBinding": {
"elementId": "proc-validate-prd",
"focus": 0,
"gap": 1
},
"points": [
[
0,
0
],
[
0.17283039375342923,
28.50332681186643
]
],
"lastCommittedPoint": null,
"version": 102,
"versionNonce": 1274591910,
"index": "aP",
"isDeleted": false,
"strokeStyle": "solid",
"seed": 1,
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1764191023838,
"link": null,
"locked": false,
"startArrowhead": null,
"endArrowhead": "arrow"
},
{
"id": "proc-validate-prd",
"type": "rectangle",
"x": 360,
"y": 360,
"width": 160,
"height": 80,
"angle": 0,
"strokeColor": "#43a047",
"backgroundColor": "#c8e6c9",
"fillStyle": "solid",
"strokeWidth": 2,
"roughness": 0,
"opacity": 100,
"roundness": {
"type": 3,
"value": 8
},
"groupIds": [
"proc-validate-prd-group"
],
"boundElements": [
{
"type": "text",
"id": "proc-validate-prd-text"
},
{
"type": "arrow",
"id": "arrow-prd-validate"
},
{
"type": "arrow",
"id": "arrow-validate-prd-hasui"
},
{
"id": "jv0rnlK2D9JKIGTO7pUtT",
"type": "arrow"
}
],
"locked": false,
"version": 3,
"versionNonce": 894806650,
"index": "aQ",
"isDeleted": false,
"strokeStyle": "solid",
"seed": 1,
"frameId": null,
"updated": 1764191341774,
"link": null
},
{
"id": "proc-validate-prd-text",
"type": "text",
"x": 370,
"y": 375,
"width": 140,
"height": 50,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"roughness": 0,
"opacity": 100,
"groupIds": [
"proc-validate-prd-group"
],
"fontSize": 14,
"fontFamily": 1,
"text": "Validate PRD\n<<optional>>",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "proc-validate-prd",
"locked": false,
"version": 2,
"versionNonce": 944332155,
"index": "aR",
"isDeleted": false,
"strokeStyle": "solid",
"seed": 1,
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1763522171080,
"link": null,
"originalText": "Validate PRD\n<<optional>>",
"autoResize": true,
"lineHeight": 1.7857142857142858
},
{
"id": "arrow-validate-prd-hasui",
"type": "arrow",
"x": 440,
"y": 440,
"width": 0,
"height": 30,
"angle": 0,
"strokeColor": "#1976d2",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"roughness": 0,
"opacity": 100,
"groupIds": [],
"startBinding": {
"elementId": "proc-validate-prd",
"focus": 0,
"gap": 1
},
"endBinding": {
"elementId": "decision-has-ui",
"focus": 0,
"gap": 1
},
"points": [
[
0,
0
],
[
0,
30
]
],
"lastCommittedPoint": null,
"version": 2,
"versionNonce": 1369541557,
"index": "aS",
"isDeleted": false,
"strokeStyle": "solid",
"seed": 1,
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1763522171080,
"link": null,
"locked": false,
"startArrowhead": null,
"endArrowhead": "arrow"
},
{
"id": "decision-has-ui",
"type": "diamond",
@ -1322,7 +1131,7 @@
},
{
"type": "arrow",
"id": "arrow-validate-prd-hasui"
"id": "arrow-prd-hasui"
},
{
"type": "arrow",
@ -1334,15 +1143,15 @@
}
],
"locked": false,
"version": 2,
"versionNonce": 1003877915,
"version": 3,
"versionNonce": 1003877916,
"index": "aT",
"isDeleted": false,
"strokeStyle": "solid",
"seed": 1,
"frameId": null,
"roundness": null,
"updated": 1763522171080,
"updated": 1764952855000,
"link": null
},
{
@ -5162,6 +4971,57 @@
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": false
},
{
"id": "arrow-prd-hasui",
"type": "arrow",
"x": 440,
"y": 330,
"width": 0,
"height": 140,
"angle": 0,
"strokeColor": "#1976d2",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"roughness": 0,
"opacity": 100,
"groupIds": [],
"startBinding": {
"elementId": "proc-prd",
"focus": 0,
"gap": 1
},
"endBinding": {
"elementId": "decision-has-ui",
"focus": 0,
"gap": 1
},
"points": [
[
0,
0
],
[
0,
140
]
],
"lastCommittedPoint": null,
"version": 1,
"versionNonce": 1,
"index": "b1J",
"isDeleted": false,
"strokeStyle": "solid",
"seed": 1,
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1764952855000,
"link": null,
"locked": false,
"startArrowhead": null,
"endArrowhead": "arrow"
}
],
"appState": {

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View File

@ -1,7 +1,7 @@
# Senior Developer Review - Validation Checklist
- [ ] Story file loaded from `{{story_path}}`
- [ ] Story Status verified as one of: {{allow_status_values}}
- [ ] Story Status verified as reviewable (review)
- [ ] Epic and Story IDs resolved ({{epic_num}}.{{story_num}})
- [ ] Story Context located or warning recorded
- [ ] Epic Tech Spec located or warning recorded
@ -17,6 +17,7 @@
- [ ] Review notes appended under "Senior Developer Review (AI)"
- [ ] Change Log updated with review entry
- [ ] Status updated according to settings (if enabled)
- [ ] Sprint status synced (if sprint tracking enabled)
- [ ] Story saved successfully
_Reviewer: {{user_name}} on {{date}}_

View File

@ -16,6 +16,7 @@
<step n="1" goal="Load story and discover changes">
<action>Use provided {{story_path}} or ask user which story file to review</action>
<action>Read COMPLETE story file</action>
<action>Set {{story_key}} = extracted key from filename (e.g., "1-2-user-authentication.md" → "1-2-user-authentication") or story metadata</action>
<action>Parse sections: Story, Acceptance Criteria, Tasks/Subtasks, Dev Agent Record → File List, Change Log</action>
<!-- Discover actual changes via git -->
@ -106,6 +107,8 @@
<step n="4" goal="Present findings and fix them">
<action>Categorize findings: HIGH (must fix), MEDIUM (should fix), LOW (nice to fix)</action>
<action>Set {{fixed_count}} = 0</action>
<action>Set {{action_count}} = 0</action>
<output>**🔥 CODE REVIEW FINDINGS, {user_name}!**
@ -145,11 +148,15 @@
<action>Add/update tests as needed</action>
<action>Update File List in story if files changed</action>
<action>Update story Dev Agent Record with fixes applied</action>
<action>Set {{fixed_count}} = number of HIGH and MEDIUM issues fixed</action>
<action>Set {{action_count}} = 0</action>
</check>
<check if="user chooses 2">
<action>Add "Review Follow-ups (AI)" subsection to Tasks/Subtasks</action>
<action>For each issue: `- [ ] [AI-Review][Severity] Description [file:line]`</action>
<action>Set {{action_count}} = number of action items created</action>
<action>Set {{fixed_count}} = 0</action>
</check>
<check if="user chooses 3">
@ -158,11 +165,52 @@
</check>
</step>
<step n="5" goal="Update story status">
<action>If all HIGH issues fixed and ACs implemented → Update story Status to "done"</action>
<action>If issues remain → Update story Status to "in-progress"</action>
<step n="5" goal="Update story status and sync sprint tracking">
<!-- Determine new status based on review outcome -->
<check if="all HIGH and MEDIUM issues fixed AND all ACs implemented">
<action>Set {{new_status}} = "done"</action>
<action>Update story Status field to "done"</action>
</check>
<check if="HIGH or MEDIUM issues remain OR ACs not fully implemented">
<action>Set {{new_status}} = "in-progress"</action>
<action>Update story Status field to "in-progress"</action>
</check>
<action>Save story file</action>
<!-- Determine sprint tracking status -->
<check if="{sprint_status} file exists">
<action>Set {{current_sprint_status}} = "enabled"</action>
</check>
<check if="{sprint_status} file does NOT exist">
<action>Set {{current_sprint_status}} = "no-sprint-tracking"</action>
</check>
<!-- Sync sprint-status.yaml when story status changes (only if sprint tracking enabled) -->
<check if="{{current_sprint_status}} != 'no-sprint-tracking'">
<action>Load the FULL file: {sprint_status}</action>
<action>Find development_status key matching {{story_key}}</action>
<check if="{{new_status}} == 'done'">
<action>Update development_status[{{story_key}}] = "done"</action>
<action>Save file, preserving ALL comments and structure</action>
<output>✅ Sprint status synced: {{story_key}} → done</output>
</check>
<check if="{{new_status}} == 'in-progress'">
<action>Update development_status[{{story_key}}] = "in-progress"</action>
<action>Save file, preserving ALL comments and structure</action>
<output>🔄 Sprint status synced: {{story_key}} → in-progress</output>
</check>
<check if="story key not found in sprint status">
<output>⚠️ Story file updated, but sprint-status sync failed: {{story_key}} not found in sprint-status.yaml</output>
</check>
</check>
<check if="{{current_sprint_status}} == 'no-sprint-tracking'">
<output> Story status updated (no sprint tracking configured)</output>
</check>
<output>**✅ Review Complete!**
**Story Status:** {{new_status}}

View File

@ -1,6 +1,6 @@
# Review Story Workflow
name: code-review
description: "Perform an ADVERSARIAL Senior Developer code review that finds 3-10 specific problems in every story. Challenges everything: code quality, test coverage, architecture compliance, security, performance. NEVER accepts 'looks good' - must find minimum issues and can auto-fix with user approval."
description: "Perform an ADVERSARIAL Senior Developer code review that finds 3-10 specific problems in every story. Challenges everything: code quality, test coverage, architecture compliance, security, performance. NEVER accepts `looks good` - must find minimum issues and can auto-fix with user approval."
author: "BMad"
# Critical variables from config

View File

@ -56,11 +56,6 @@ phases:
output: "Enterprise PRD with compliance requirements"
note: "Must address existing system constraints and migration strategy"
- id: "validate-prd"
recommended: true
agent: "pm"
command: "validate-prd"
- id: "create-ux-design"
recommended: true
agent: "ux-designer"
@ -114,7 +109,7 @@ phases:
required: true
agent: "architect"
command: "implementation-readiness"
note: "Critical gate - validates all planning + Epics before touching production system"
note: "Validates PRD + Architecture + Epics + UX (optional)"
- phase: 3
name: "Implementation"

View File

@ -44,11 +44,6 @@ phases:
output: "Comprehensive Product Requirements Document"
note: "Enterprise-level requirements with compliance considerations"
- id: "validate-prd"
recommended: true
agent: "pm"
command: "validate-prd"
- id: "create-ux-design"
recommended: true
agent: "ux-designer"
@ -102,7 +97,7 @@ phases:
required: true
agent: "architect"
command: "implementation-readiness"
note: "Validates all planning artifacts + Epics + testability align before implementation"
note: "Validates PRD + Architecture + Epics + UX (optional)"
- phase: 3
name: "Implementation"

View File

@ -55,11 +55,6 @@ phases:
output: "PRD focused on new features/changes"
note: "Must consider existing system constraints"
- id: "validate-prd"
optional: true
agent: "pm"
command: "validate-prd"
- id: "create-ux-design"
conditional: "if_has_ui"
agent: "ux-designer"
@ -98,7 +93,7 @@ phases:
required: true
agent: "architect"
command: "implementation-readiness"
note: "Validates PRD + UX + Architecture + Epics cohesion before implementation"
note: "Validates PRD + Architecture + Epics + UX (optional)"
- phase: 3
name: "Implementation"

View File

@ -43,12 +43,6 @@ phases:
command: "prd"
output: "Product Requirements Document with FRs and NFRs"
- id: "validate-prd"
optional: true
agent: "pm"
command: "validate-prd"
note: "Quality check for PRD completeness"
- id: "create-ux-design"
conditional: "if_has_ui"
agent: "ux-designer"
@ -89,7 +83,7 @@ phases:
required: true
agent: "architect"
command: "implementation-readiness"
note: "Validates PRD + UX + Architecture + Epics + Testability cohesion before implementation"
note: "Validates PRD + Architecture + Epics + UX (optional)"
- phase: 3
name: "Implementation"

View File

@ -235,7 +235,7 @@ async function buildAllAgents(projectDir, force = false) {
continue;
}
const agentName = file.replace('.agent.yaml', '');
const agentName = path.basename(file, '.agent.yaml');
const agentYamlPath = path.join(agentsDir, file);
const outputPath = path.join(agentsDir, `${agentName}.md`);
@ -330,7 +330,7 @@ async function checkBuildStatus(projectDir) {
continue;
}
const agentName = file.replace('.agent.yaml', '');
const agentName = path.basename(file, '.agent.yaml');
const agentYamlPath = path.join(agentsDir, file);
const outputPath = path.join(agentsDir, `${agentName}.md`);
@ -449,7 +449,7 @@ async function listAvailableAgents(projectDir) {
for (const file of files) {
if (file.endsWith('.agent.yaml')) {
const agentName = file.replace('.agent.yaml', '');
const agentName = path.basename(file, '.agent.yaml');
console.log(chalk.dim(` - ${agentName} (${module})`));
}
}

View File

@ -51,6 +51,7 @@ class Installer {
this.configCollector = new ConfigCollector();
this.ideConfigManager = new IdeConfigManager();
this.installedFiles = []; // Track all installed files
this.ttsInjectedFiles = []; // Track files with TTS injection applied
}
/**
@ -146,8 +147,8 @@ class Installer {
content = content.replaceAll('{*bmad_folder*}', '{bmad_folder}');
}
// Process AgentVibes injection points
content = this.processTTSInjectionPoints(content);
// Process AgentVibes injection points (pass targetPath for tracking)
content = this.processTTSInjectionPoints(content, targetPath);
// Write to target with replaced content
await fs.ensureDir(path.dirname(targetPath));
@ -226,10 +227,14 @@ class Installer {
* - src/modules/bmm/agents/*.md (rules sections)
* - TTS Hook: .claude/hooks/bmad-speak.sh (in AgentVibes repo)
*/
processTTSInjectionPoints(content) {
processTTSInjectionPoints(content, targetPath = null) {
// Check if AgentVibes is enabled (set during installation configuration)
const enableAgentVibes = this.enableAgentVibes || false;
// Check if content contains any TTS injection markers
const hasPartyMode = content.includes('<!-- TTS_INJECTION:party-mode -->');
const hasAgentTTS = content.includes('<!-- TTS_INJECTION:agent-tts -->');
if (enableAgentVibes) {
// Replace party-mode injection marker with actual TTS call
// Use single quotes to prevent shell expansion of special chars like !
@ -253,6 +258,12 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
IMPORTANT: Use single quotes as shown - do NOT escape special characters like ! or $ inside single quotes
Run in background (&) to avoid blocking`,
);
// Track files that had TTS injection applied
if (targetPath && (hasPartyMode || hasAgentTTS)) {
const injectionType = hasPartyMode ? 'party-mode' : 'agent-tts';
this.ttsInjectedFiles.push({ path: targetPath, type: injectionType });
}
} else {
// Strip injection markers cleanly when AgentVibes is disabled
content = content.replaceAll(/<!-- TTS_INJECTION:party-mode -->\n?/g, '');
@ -1021,6 +1032,8 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
modules: config.modules,
ides: config.ides,
customFiles: customFiles.length > 0 ? customFiles : undefined,
ttsInjectedFiles: this.enableAgentVibes && this.ttsInjectedFiles.length > 0 ? this.ttsInjectedFiles : undefined,
agentVibesEnabled: this.enableAgentVibes || false,
});
// Offer cleanup for legacy files (only for updates, not fresh installs, and only if not skipped)
@ -1526,13 +1539,16 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Build YAML + customize to .md
const customizeExists = await fs.pathExists(customizePath);
const xmlContent = await this.xmlHandler.buildFromYaml(yamlPath, customizeExists ? customizePath : null, {
let xmlContent = await this.xmlHandler.buildFromYaml(yamlPath, customizeExists ? customizePath : null, {
includeMetadata: true,
});
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
// Process TTS injection points (pass targetPath for tracking)
xmlContent = this.processTTSInjectionPoints(xmlContent, mdPath);
// Write the built .md file to bmad/{module}/agents/ with POSIX-compliant final newline
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
await fs.writeFile(mdPath, content, 'utf8');
@ -1628,13 +1644,16 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
// Build YAML to XML .md
const xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, {
let xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, {
includeMetadata: true,
});
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
// Process TTS injection points (pass targetPath for tracking)
xmlContent = this.processTTSInjectionPoints(xmlContent, targetMdPath);
// Write the built .md file with POSIX-compliant final newline
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
await fs.writeFile(targetMdPath, content, 'utf8');
@ -1722,13 +1741,16 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
// Build YAML + customize to .md
const xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, {
let xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, {
includeMetadata: true,
});
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
// Process TTS injection points (pass targetPath for tracking)
xmlContent = this.processTTSInjectionPoints(xmlContent, targetMdPath);
// Write the rebuilt .md file with POSIX-compliant final newline
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
await fs.writeFile(targetMdPath, content, 'utf8');

View File

@ -482,10 +482,39 @@ function compileAgent(yamlContent, answers = {}, agentName = '', targetPath = ''
};
}
/**
* Process TTS injection markers in content
* @param {string} content - Content to process
* @param {boolean} enableAgentVibes - Whether AgentVibes is enabled
* @returns {Object} { content: string, hadInjection: boolean }
*/
function processTTSInjectionPoints(content, enableAgentVibes) {
const hasAgentTTS = content.includes('<!-- TTS_INJECTION:agent-tts -->');
if (enableAgentVibes && hasAgentTTS) {
// Replace agent-tts injection marker with TTS rule
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`,
);
return { content, hadInjection: true };
} else if (!enableAgentVibes && hasAgentTTS) {
// Strip injection markers when disabled
content = content.replaceAll(/<!-- TTS_INJECTION:agent-tts -->\n?/g, '');
}
return { content, hadInjection: false };
}
/**
* Compile agent file to .md
* @param {string} yamlPath - Path to agent YAML file
* @param {Object} options - { answers: {}, outputPath: string }
* @param {Object} options - { answers: {}, outputPath: string, enableAgentVibes: boolean }
* @returns {Object} Compilation result
*/
function compileAgentFile(yamlPath, options = {}) {
@ -501,13 +530,24 @@ function compileAgentFile(yamlPath, options = {}) {
outputPath = path.join(dir, `${basename}.md`);
}
// Process TTS injection points if enableAgentVibes option is provided
let xml = result.xml;
let ttsInjected = false;
if (options.enableAgentVibes !== undefined) {
const ttsResult = processTTSInjectionPoints(xml, options.enableAgentVibes);
xml = ttsResult.content;
ttsInjected = ttsResult.hadInjection;
}
// Write compiled XML
fs.writeFileSync(outputPath, result.xml, 'utf8');
fs.writeFileSync(outputPath, xml, 'utf8');
return {
...result,
xml,
outputPath,
sourcePath: yamlPath,
ttsInjected,
};
}

View File

@ -363,11 +363,60 @@ class UI {
`🔧 Tools Configured: ${result.ides?.length > 0 ? result.ides.join(', ') : 'none'}`,
];
// Add AgentVibes TTS info if enabled
if (result.agentVibesEnabled) {
summary.push(`🎤 AgentVibes TTS: Enabled`);
}
CLIUtils.displayBox(summary.join('\n\n'), {
borderColor: 'green',
borderStyle: 'round',
});
// Display TTS injection details if present
if (result.ttsInjectedFiles && result.ttsInjectedFiles.length > 0) {
console.log('\n' + chalk.cyan.bold('═══════════════════════════════════════════════════'));
console.log(chalk.cyan.bold(' AgentVibes TTS Injection Summary'));
console.log(chalk.cyan.bold('═══════════════════════════════════════════════════\n'));
// Explain what TTS injection is
console.log(chalk.white.bold('What is TTS Injection?\n'));
console.log(chalk.dim(' TTS (Text-to-Speech) injection adds voice instructions to BMAD agents,'));
console.log(chalk.dim(' enabling them to speak their responses aloud using AgentVibes.\n'));
console.log(chalk.dim(' Example: When you activate the PM agent, it will greet you with'));
console.log(chalk.dim(' spoken audio like "Hey! I\'m your Project Manager. How can I help?"\n'));
console.log(chalk.green(`✅ TTS injection applied to ${result.ttsInjectedFiles.length} file(s):\n`));
// Group by type
const partyModeFiles = result.ttsInjectedFiles.filter((f) => f.type === 'party-mode');
const agentTTSFiles = result.ttsInjectedFiles.filter((f) => f.type === 'agent-tts');
if (partyModeFiles.length > 0) {
console.log(chalk.yellow(' Party Mode (multi-agent conversations):'));
for (const file of partyModeFiles) {
console.log(chalk.dim(`${file.path}`));
}
}
if (agentTTSFiles.length > 0) {
console.log(chalk.yellow(' Agent TTS (individual agent voices):'));
for (const file of agentTTSFiles) {
console.log(chalk.dim(`${file.path}`));
}
}
// Show backup info and restore command
console.log('\n' + chalk.white.bold('Backups & Recovery:\n'));
console.log(chalk.dim(' Pre-injection backups are stored in:'));
console.log(chalk.cyan(' ~/.bmad-tts-backups/\n'));
console.log(chalk.dim(' To restore original files (removes TTS instructions):'));
console.log(chalk.cyan(` bmad-tts-injector.sh --restore ${result.path}\n`));
console.log(chalk.cyan('💡 BMAD agents will now speak when activated!'));
console.log(chalk.dim(' Ensure AgentVibes is installed: https://agentvibes.org'));
}
console.log('\n' + chalk.green.bold('✨ BMAD is ready to use!'));
}

356
tools/validate-svg-changes.sh Executable file
View File

@ -0,0 +1,356 @@
#!/bin/bash
#
# Visual SVG Validation Script
#
# Compares old vs new SVG files using browser-accurate rendering (Playwright)
# and pixel-level comparison (ImageMagick), then generates a prompt for AI analysis.
#
# Usage: ./tools/validate-svg-changes.sh <path-to-svg>
#
set -e
SVG_FILE="${1:-src/modules/bmm/docs/images/workflow-method-greenfield.svg}"
TMP_DIR="/tmp/svg-validation-$$"
echo "🎨 Visual SVG Validation"
echo ""
# Check if file exists
if [ ! -f "$SVG_FILE" ]; then
echo "❌ Error: SVG file not found: $SVG_FILE"
exit 1
fi
# Check for ImageMagick
if ! command -v magick &> /dev/null; then
echo "❌ ImageMagick not found"
echo ""
echo "Install with:"
echo " brew install imagemagick"
echo ""
exit 1
fi
echo "✓ ImageMagick found"
# Check for Node.js
if ! command -v node &> /dev/null; then
echo "❌ Node.js not found"
exit 1
fi
echo "✓ Node.js found ($(node -v))"
# Check for Playwright (local install)
if [ ! -d "node_modules/playwright" ]; then
echo ""
echo "📦 Playwright not found locally"
echo "Installing Playwright (local to this project, no package.json changes)..."
echo ""
npm install --no-save playwright
echo ""
echo "✓ Playwright installed"
else
echo "✓ Playwright found"
fi
echo ""
echo "🔄 Rendering SVGs to PNG..."
echo ""
# Create temp directory
mkdir -p "$TMP_DIR"
# Extract old SVG from git
git show HEAD:"$SVG_FILE" > "$TMP_DIR/old.svg" 2>/dev/null || {
echo "❌ Could not extract old SVG from git HEAD"
echo " Make sure you have uncommitted changes to compare"
exit 1
}
# Copy new SVG
cp "$SVG_FILE" "$TMP_DIR/new.svg"
# Create Node.js renderer script in project directory (so it can find node_modules)
cat > "tools/render-svg-temp.js" << 'EOJS'
const { chromium } = require('playwright');
const fs = require('fs');
async function renderSVG(svgPath, pngPath) {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const svgContent = fs.readFileSync(svgPath, 'utf8');
const widthMatch = svgContent.match(/width="([^"]+)"/);
const heightMatch = svgContent.match(/height="([^"]+)"/);
const width = Math.ceil(parseFloat(widthMatch[1]));
const height = Math.ceil(parseFloat(heightMatch[1]));
const html = `
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; padding: 0; background: white; }
svg { display: block; }
</style>
</head>
<body>${svgContent}</body>
</html>
`;
await page.setContent(html);
await page.setViewportSize({ width, height });
await page.waitForTimeout(1000);
await page.screenshot({ path: pngPath, fullPage: true });
await browser.close();
console.log(`✓ Rendered ${pngPath}`);
}
(async () => {
await renderSVG(process.argv[2], process.argv[3]);
await renderSVG(process.argv[4], process.argv[5]);
})();
EOJS
# Render both SVGs (run from project dir so node_modules is accessible)
node tools/render-svg-temp.js \
"$TMP_DIR/old.svg" "$TMP_DIR/old.png" \
"$TMP_DIR/new.svg" "$TMP_DIR/new.png"
# Clean up temp script
rm tools/render-svg-temp.js
echo ""
echo "🔍 Comparing pixels..."
echo ""
# Compare using ImageMagick
DIFF_OUTPUT=$(magick compare -metric AE "$TMP_DIR/old.png" "$TMP_DIR/new.png" "$TMP_DIR/diff.png" 2>&1 || true)
DIFF_PIXELS=$(echo "$DIFF_OUTPUT" | awk '{print $1}')
# Get image dimensions
DIMENSIONS=$(magick identify -format "%wx%h" "$TMP_DIR/old.png")
WIDTH=$(echo "$DIMENSIONS" | cut -d'x' -f1)
HEIGHT=$(echo "$DIMENSIONS" | cut -d'x' -f2)
TOTAL_PIXELS=$((WIDTH * HEIGHT))
# Calculate percentage
DIFF_PERCENT=$(echo "scale=4; $DIFF_PIXELS / $TOTAL_PIXELS * 100" | bc)
echo "📊 Results:"
echo " Dimensions: ${WIDTH} × ${HEIGHT}"
echo " Total pixels: $(printf "%'d" $TOTAL_PIXELS)"
echo " Different pixels: $(printf "%'d" $DIFF_PIXELS)"
echo " Difference: ${DIFF_PERCENT}%"
echo ""
if (( $(echo "$DIFF_PERCENT < 0.01" | bc -l) )); then
echo "✅ ESSENTIALLY IDENTICAL (< 0.01% difference)"
VERDICT="essentially identical"
elif (( $(echo "$DIFF_PERCENT < 0.1" | bc -l) )); then
echo "⚠️ MINOR DIFFERENCES (< 0.1%)"
VERDICT="minor differences detected"
else
echo "❌ SIGNIFICANT DIFFERENCES (≥ 0.1%)"
VERDICT="significant differences detected"
fi
echo ""
echo "📁 Output files:"
echo " Old render: $TMP_DIR/old.png"
echo " New render: $TMP_DIR/new.png"
echo " Diff image: $TMP_DIR/diff.png"
echo ""
# Generate HTML comparison page
cat > "$TMP_DIR/comparison.html" << 'EOHTML'
<!DOCTYPE html>
<html>
<head>
<title>SVG Comparison</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.header {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 { margin-bottom: 10px; color: #333; }
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
margin-top: 15px;
}
.stat {
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
}
.stat-label { font-size: 12px; color: #666; text-transform: uppercase; }
.stat-value { font-size: 18px; font-weight: 600; color: #333; margin-top: 4px; }
.container {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.panel {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h2 {
margin: 0 0 15px 0;
color: #333;
font-size: 18px;
border-bottom: 2px solid #e0e0e0;
padding-bottom: 10px;
}
.image-container {
border: 1px solid #ddd;
background: white;
overflow: auto;
max-height: 600px;
}
img {
display: block;
max-width: 100%;
height: auto;
}
.verdict {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
}
.verdict.good { background: #d4edda; color: #155724; }
.verdict.warning { background: #fff3cd; color: #856404; }
.verdict.bad { background: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<div class="header">
<h1>🎨 SVG Visual Comparison</h1>
<p><strong>File:</strong> FILENAME_PLACEHOLDER</p>
<div class="stats">
<div class="stat">
<div class="stat-label">Dimensions</div>
<div class="stat-value">DIMENSIONS_PLACEHOLDER</div>
</div>
<div class="stat">
<div class="stat-label">Different Pixels</div>
<div class="stat-value">DIFF_PIXELS_PLACEHOLDER</div>
</div>
<div class="stat">
<div class="stat-label">Difference</div>
<div class="stat-value">DIFF_PERCENT_PLACEHOLDER%</div>
</div>
<div class="stat">
<div class="stat-label">Verdict</div>
<div class="stat-value"><span class="verdict VERDICT_CLASS_PLACEHOLDER">VERDICT_PLACEHOLDER</span></div>
</div>
</div>
</div>
<div class="container">
<div class="panel">
<h2>📄 Old (HEAD)</h2>
<div class="image-container">
<img src="old.png" alt="Old SVG">
</div>
</div>
<div class="panel">
<h2>📝 New (Working)</h2>
<div class="image-container">
<img src="new.png" alt="New SVG">
</div>
</div>
<div class="panel">
<h2>🔍 Diff (Red = Changes)</h2>
<div class="image-container">
<img src="diff.png" alt="Diff">
</div>
</div>
</div>
</body>
</html>
EOHTML
# Determine verdict class for styling
if (( $(echo "$DIFF_PERCENT < 0.01" | bc -l) )); then
VERDICT_CLASS="good"
elif (( $(echo "$DIFF_PERCENT < 0.1" | bc -l) )); then
VERDICT_CLASS="warning"
else
VERDICT_CLASS="bad"
fi
# Replace placeholders in HTML
sed -i '' "s|FILENAME_PLACEHOLDER|$SVG_FILE|g" "$TMP_DIR/comparison.html"
sed -i '' "s|DIMENSIONS_PLACEHOLDER|${WIDTH} × ${HEIGHT}|g" "$TMP_DIR/comparison.html"
sed -i '' "s|DIFF_PIXELS_PLACEHOLDER|$(printf "%'d" $DIFF_PIXELS) / $(printf "%'d" $TOTAL_PIXELS)|g" "$TMP_DIR/comparison.html"
sed -i '' "s|DIFF_PERCENT_PLACEHOLDER|$DIFF_PERCENT|g" "$TMP_DIR/comparison.html"
sed -i '' "s|VERDICT_PLACEHOLDER|$VERDICT|g" "$TMP_DIR/comparison.html"
sed -i '' "s|VERDICT_CLASS_PLACEHOLDER|$VERDICT_CLASS|g" "$TMP_DIR/comparison.html"
echo "✓ Generated comparison page: $TMP_DIR/comparison.html"
echo ""
echo "🌐 Opening comparison in browser..."
open "$TMP_DIR/comparison.html"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "🤖 AI VISUAL ANALYSIS PROMPT"
echo ""
echo "Copy and paste this into Gemini/Claude with the diff image attached:"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cat << PROMPT
I've made changes to an Excalidraw diagram SVG file. Please analyze the visual differences between the old and new versions.
**Automated Analysis:**
- Dimensions: ${WIDTH} × ${HEIGHT} pixels
- Different pixels: $(printf "%'d" $DIFF_PIXELS) out of $(printf "%'d" $TOTAL_PIXELS)
- Difference: ${DIFF_PERCENT}%
- Verdict: ${VERDICT}
**Attached Image:**
The attached image shows the pixel-level diff (red = differences).
**Questions:**
1. Are the differences purely anti-aliasing/rendering artifacts, or are there actual content changes?
2. If there are content changes, what specifically changed?
3. Do the changes align with the intent to remove zombie Excalidraw elements (elements marked as deleted but left in the JSON)?
4. Is this safe to commit?
**Context:**
- File: $SVG_FILE
- Changes: Removed 191 lines of zombie JSON from Excalidraw source
- Expected: Visual output should be identical (zombie elements were already marked as deleted)
PROMPT
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "📎 Attach this file to your AI prompt:"
echo " $TMP_DIR/diff.png"
echo ""
echo "💡 To open the diff image:"
echo " open $TMP_DIR/diff.png"
echo ""