Compare commits

...

8 Commits

Author SHA1 Message Date
Ziyu Huang 6fb60debfe
Merge e9b75bd3cb into 5ee1551b5b 2025-12-06 06:13:24 -05: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
Murat K Ozcan f99e192e74
fix: tea ci nvmrc (#1036) 2025-12-05 12:30:20 -06:00
Ziyu Huang e9b75bd3cb Enhance build command to scan all .bmad/ modules
- Update buildAllAgents() to scan all directories in .bmad/
- Update checkBuildStatus() to check all modules
- Update listAvailableAgents() to list all modules
- Auto-detect .bmad or bmad folder name
- Skip special directories (_cfg, docs)

This makes 'bmad build --all' discover custom modules (custom/, hde/, etc.)
and builds their agents automatically, matching the behavior of the fixed
install/compile workflow.

Related to #990
2025-11-28 07:06:09 +08:00
Ziyu Huang 1af87338ae Fix #990: Enable custom module discovery in installer
- Fix 'installedModules is not defined' error in compileAgents()
- Add manifest regeneration during compilation to discover custom content
- Change manifest generator to scan all directories dynamically
- Fix ManifestGenerator import to use destructuring
- Handle custom modules without installer source gracefully

This enables custom workflows/agents/tasks in .bmad/custom/ to be
automatically discovered and generate proper .claude/commands/ files.

Changes:
- installer.js: Add manifest regeneration, fix imports, handle custom modules
- manifest-generator.js: Scan all directories instead of hardcoded list

Fixes #990
2025-11-28 06:45:38 +08:00
22 changed files with 920 additions and 294 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) - `create-prd` - Create PRD for Level 2-4 projects (creates FRs/NFRs only)
- `tech-spec` - Quick spec for Level 0-1 projects - `tech-spec` - Quick spec for Level 0-1 projects
- `create-epics-and-stories` - Break PRD into implementable pieces (runs AFTER architecture) - `create-epics-and-stories` - Break PRD into implementable pieces (runs AFTER architecture)
- `validate-prd` - Validate PRD completeness - `implementation-readiness` - Validate PRD + Architecture + Epics + UX (optional)
- `validate-tech-spec` - Validate Technical Specification
- `correct-course` - Handle mid-project changes - `correct-course` - Handle mid-project changes
- `workflow-init` - Initialize workflow tracking - `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 - `workflow-status` - Check what to do next
- `create-architecture` - Produce a Scale Adaptive Architecture - `create-architecture` - Produce a Scale Adaptive Architecture
- `validate-architecture` - Validate architecture document - `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. **Communication Style:** Comprehensive yet pragmatic. Uses architectural metaphors. Balances technical depth with accessibility. Connects decisions to business value.
@ -643,9 +642,8 @@ Some workflows are available to multiple agents:
Many workflows have optional validation workflows that perform independent review: Many workflows have optional validation workflows that perform independent review:
| Validation | Agent | Validates | | Validation | Agent | Validates |
| ----------------------- | ----------- | -------------------------------- | | -------------------------- | ----------- | ------------------------------------------ |
| `validate-prd` | PM | PRD completeness (FRs/NFRs only) | | `implementation-readiness` | Architect | PRD + Architecture + Epics + UX (optional) |
| `validate-tech-spec` | PM | Technical specification quality |
| `validate-architecture` | Architect | Architecture document | | `validate-architecture` | Architect | Architecture document |
| `validate-design` | UX Designer | UX specification and artifacts | | `validate-design` | UX Designer | UX specification and artifacts |
| `validate-create-story` | SM | Story draft | | `validate-create-story` | SM | Story draft |
@ -945,9 +943,8 @@ Agent analyzes project state → recommends next workflow
``` ```
Each phase has validation gates: Each phase has validation gates:
- Phase 2 to 3: validate-prd, validate-tech-spec - Phase 3 to 4: implementation-readiness (validates PRD + Architecture + Epics + UX (optional))
- Phase 3 to 4: implementation-readiness Run validation before advancing to implementation
Run validation before advancing
``` ```
**Course correction:** **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? ### 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? ### 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 ### 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) ### 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", "type": "arrow",
"id": "arrow-discovery-no" "id": "arrow-discovery-no"
}, },
{
"type": "arrow",
"id": "arrow-prd-validate"
},
{ {
"id": "arrow-phase1-to-phase2", "id": "arrow-phase1-to-phase2",
"type": "arrow" "type": "arrow"
@ -1055,17 +1051,21 @@
{ {
"id": "arrow-has-ui-no", "id": "arrow-has-ui-no",
"type": "arrow" "type": "arrow"
},
{
"id": "arrow-prd-hasui",
"type": "arrow"
} }
], ],
"locked": false, "locked": false,
"version": 107, "version": 108,
"versionNonce": 930129274, "versionNonce": 930129275,
"index": "aN", "index": "aN",
"isDeleted": false, "isDeleted": false,
"strokeStyle": "solid", "strokeStyle": "solid",
"seed": 1, "seed": 1,
"frameId": null, "frameId": null,
"updated": 1764191563350, "updated": 1764952855000,
"link": null "link": null
}, },
{ {
@ -1107,197 +1107,6 @@
"autoResize": true, "autoResize": true,
"lineHeight": 1.25 "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", "id": "decision-has-ui",
"type": "diamond", "type": "diamond",
@ -1322,7 +1131,7 @@
}, },
{ {
"type": "arrow", "type": "arrow",
"id": "arrow-validate-prd-hasui" "id": "arrow-prd-hasui"
}, },
{ {
"type": "arrow", "type": "arrow",
@ -1334,15 +1143,15 @@
} }
], ],
"locked": false, "locked": false,
"version": 2, "version": 3,
"versionNonce": 1003877915, "versionNonce": 1003877916,
"index": "aT", "index": "aT",
"isDeleted": false, "isDeleted": false,
"strokeStyle": "solid", "strokeStyle": "solid",
"seed": 1, "seed": 1,
"frameId": null, "frameId": null,
"roundness": null, "roundness": null,
"updated": 1763522171080, "updated": 1764952855000,
"link": null "link": null
}, },
{ {
@ -5162,6 +4971,57 @@
"startArrowhead": null, "startArrowhead": null,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"elbowed": false "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": { "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 # Senior Developer Review - Validation Checklist
- [ ] Story file loaded from `{{story_path}}` - [ ] 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}}) - [ ] Epic and Story IDs resolved ({{epic_num}}.{{story_num}})
- [ ] Story Context located or warning recorded - [ ] Story Context located or warning recorded
- [ ] Epic Tech Spec located or warning recorded - [ ] Epic Tech Spec located or warning recorded
@ -17,6 +17,7 @@
- [ ] Review notes appended under "Senior Developer Review (AI)" - [ ] Review notes appended under "Senior Developer Review (AI)"
- [ ] Change Log updated with review entry - [ ] Change Log updated with review entry
- [ ] Status updated according to settings (if enabled) - [ ] Status updated according to settings (if enabled)
- [ ] Sprint status synced (if sprint tracking enabled)
- [ ] Story saved successfully - [ ] Story saved successfully
_Reviewer: {{user_name}} on {{date}}_ _Reviewer: {{user_name}} on {{date}}_

View File

@ -16,6 +16,7 @@
<step n="1" goal="Load story and discover changes"> <step n="1" goal="Load story and discover changes">
<action>Use provided {{story_path}} or ask user which story file to review</action> <action>Use provided {{story_path}} or ask user which story file to review</action>
<action>Read COMPLETE story file</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> <action>Parse sections: Story, Acceptance Criteria, Tasks/Subtasks, Dev Agent Record → File List, Change Log</action>
<!-- Discover actual changes via git --> <!-- Discover actual changes via git -->
@ -106,6 +107,8 @@
<step n="4" goal="Present findings and fix them"> <step n="4" goal="Present findings and fix them">
<action>Categorize findings: HIGH (must fix), MEDIUM (should fix), LOW (nice to fix)</action> <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}!** <output>**🔥 CODE REVIEW FINDINGS, {user_name}!**
@ -145,11 +148,15 @@
<action>Add/update tests as needed</action> <action>Add/update tests as needed</action>
<action>Update File List in story if files changed</action> <action>Update File List in story if files changed</action>
<action>Update story Dev Agent Record with fixes applied</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>
<check if="user chooses 2"> <check if="user chooses 2">
<action>Add "Review Follow-ups (AI)" subsection to Tasks/Subtasks</action> <action>Add "Review Follow-ups (AI)" subsection to Tasks/Subtasks</action>
<action>For each issue: `- [ ] [AI-Review][Severity] Description [file:line]`</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>
<check if="user chooses 3"> <check if="user chooses 3">
@ -158,11 +165,52 @@
</check> </check>
</step> </step>
<step n="5" goal="Update story status"> <step n="5" goal="Update story status and sync sprint tracking">
<action>If all HIGH issues fixed and ACs implemented → Update story Status to "done"</action> <!-- Determine new status based on review outcome -->
<action>If issues remain → Update story Status to "in-progress"</action> <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> <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!** <output>**✅ Review Complete!**
**Story Status:** {{new_status}} **Story Status:** {{new_status}}

View File

@ -1,6 +1,6 @@
# Review Story Workflow # Review Story Workflow
name: code-review 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" author: "BMad"
# Critical variables from config # Critical variables from config

View File

@ -27,10 +27,21 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Determine Node version
id: node-version
run: |
if [ -f .nvmrc ]; then
echo "value=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
echo "Using Node from .nvmrc"
else
echo "value=24" >> "$GITHUB_OUTPUT"
echo "Using default Node 24 (current LTS)"
fi
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version-file: ".nvmrc" node-version: ${{ steps.node-version.outputs.value }}
cache: "npm" cache: "npm"
- name: Install dependencies - name: Install dependencies
@ -54,10 +65,21 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Determine Node version
id: node-version
run: |
if [ -f .nvmrc ]; then
echo "value=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
echo "Using Node from .nvmrc"
else
echo "value=22" >> "$GITHUB_OUTPUT"
echo "Using default Node 22 (current LTS)"
fi
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version-file: ".nvmrc" node-version: ${{ steps.node-version.outputs.value }}
cache: "npm" cache: "npm"
- name: Cache Playwright browsers - name: Cache Playwright browsers
@ -99,10 +121,21 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Determine Node version
id: node-version
run: |
if [ -f .nvmrc ]; then
echo "value=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
echo "Using Node from .nvmrc"
else
echo "value=22" >> "$GITHUB_OUTPUT"
echo "Using default Node 22 (current LTS)"
fi
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version-file: ".nvmrc" node-version: ${{ steps.node-version.outputs.value }}
cache: "npm" cache: "npm"
- name: Cache Playwright browsers - name: Cache Playwright browsers

View File

@ -15,6 +15,8 @@ variables:
npm_config_cache: "$CI_PROJECT_DIR/.npm" npm_config_cache: "$CI_PROJECT_DIR/.npm"
# Playwright browser cache # Playwright browser cache
PLAYWRIGHT_BROWSERS_PATH: "$CI_PROJECT_DIR/.cache/ms-playwright" PLAYWRIGHT_BROWSERS_PATH: "$CI_PROJECT_DIR/.cache/ms-playwright"
# Default Node version when .nvmrc is missing
DEFAULT_NODE_VERSION: "24"
# Caching configuration # Caching configuration
cache: cache:
@ -29,19 +31,32 @@ cache:
# Lint stage - Code quality checks # Lint stage - Code quality checks
lint: lint:
stage: lint stage: lint
image: node:20 image: node:$DEFAULT_NODE_VERSION
script: before_script:
- |
NODE_VERSION=$(cat .nvmrc 2>/dev/null || echo "$DEFAULT_NODE_VERSION")
echo "Using Node $NODE_VERSION"
npm install -g n
n "$NODE_VERSION"
node -v
- npm ci - npm ci
script:
- npm run lint - npm run lint
timeout: 5 minutes timeout: 5 minutes
# Test stage - Parallel execution with sharding # Test stage - Parallel execution with sharding
.test-template: &test-template .test-template: &test-template
stage: test stage: test
image: node:20 image: node:$DEFAULT_NODE_VERSION
needs: needs:
- lint - lint
before_script: before_script:
- |
NODE_VERSION=$(cat .nvmrc 2>/dev/null || echo "$DEFAULT_NODE_VERSION")
echo "Using Node $NODE_VERSION"
npm install -g n
n "$NODE_VERSION"
node -v
- npm ci - npm ci
- npx playwright install --with-deps chromium - npx playwright install --with-deps chromium
artifacts: artifacts:
@ -75,7 +90,7 @@ test:shard-4:
# Burn-in stage - Flaky test detection # Burn-in stage - Flaky test detection
burn-in: burn-in:
stage: burn-in stage: burn-in
image: node:20 image: node:$DEFAULT_NODE_VERSION
needs: needs:
- test:shard-1 - test:shard-1
- test:shard-2 - test:shard-2
@ -86,6 +101,12 @@ burn-in:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_PIPELINE_SOURCE == "schedule"' - if: '$CI_PIPELINE_SOURCE == "schedule"'
before_script: before_script:
- |
NODE_VERSION=$(cat .nvmrc 2>/dev/null || echo "$DEFAULT_NODE_VERSION")
echo "Using Node $NODE_VERSION"
npm install -g n
n "$NODE_VERSION"
node -v
- npm ci - npm ci
- npx playwright install --with-deps chromium - npx playwright install --with-deps chromium
script: script:

View File

@ -61,8 +61,8 @@ Scaffolds a production-ready CI/CD quality pipeline with test execution, burn-in
- Ask user if unable to auto-detect - Ask user if unable to auto-detect
5. **Read Environment Configuration** 5. **Read Environment Configuration**
- Check for `.nvmrc` to determine Node version - Use `.nvmrc` for Node version if present
- Default to Node 20 LTS if not found - If missing, default to a current LTS (Node 24) or newer instead of a fixed old version
- Read `package.json` to identify dependencies (affects caching strategy) - Read `package.json` to identify dependencies (affects caching strategy)
**Halt Condition:** If preflight checks fail, stop immediately and report which requirement failed. **Halt Condition:** If preflight checks fail, stop immediately and report which requirement failed.

View File

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

View File

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

View File

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

View File

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

View File

@ -166,8 +166,66 @@ async function buildAllAgents(projectDir, force = false) {
let builtCount = 0; let builtCount = 0;
let skippedCount = 0; let skippedCount = 0;
// First, build standalone agents in bmad/agents/ // Detect .bmad folder name (could be .bmad or bmad)
const standaloneAgentsDir = path.join(projectDir, 'bmad', 'agents'); const bmadFolder = (await fs.pathExists(path.join(projectDir, '.bmad'))) ? '.bmad' : 'bmad';
const bmadDir = path.join(projectDir, bmadFolder);
// Build agents from ALL module directories in .bmad/ (including custom, hde, etc.)
if (await fs.pathExists(bmadDir)) {
console.log(chalk.cyan('\nScanning all modules in .bmad/...'));
const moduleEntries = await fs.readdir(bmadDir, { withFileTypes: true });
for (const moduleEntry of moduleEntries) {
// Skip special directories
if (!moduleEntry.isDirectory() || moduleEntry.name === '_cfg' || moduleEntry.name === 'docs') {
continue;
}
const modulePath = path.join(bmadDir, moduleEntry.name);
const agentsPath = path.join(modulePath, 'agents');
// Check if this module has an agents/ directory
if (!(await fs.pathExists(agentsPath))) {
continue;
}
console.log(chalk.cyan(`\nBuilding agents in ${moduleEntry.name} module...`));
const agentFiles = await fs.readdir(agentsPath);
for (const file of agentFiles) {
if (!file.endsWith('.agent.yaml')) {
continue;
}
const agentName = file.replace('.agent.yaml', '');
const agentYamlPath = path.join(agentsPath, file);
const outputPath = path.join(agentsPath, `${agentName}.md`);
// Check if rebuild needed
if (!force && (await fs.pathExists(outputPath))) {
const needsRebuild = await checkIfNeedsRebuild(agentYamlPath, outputPath, projectDir, agentName);
if (!needsRebuild) {
console.log(chalk.dim(` ${agentName}: up to date`));
skippedCount++;
continue;
}
}
console.log(chalk.cyan(` Building ${agentName}...`));
const customizePath = path.join(bmadDir, '_cfg', 'agents', `${moduleEntry.name}-${agentName}.customize.yaml`);
const customizeExists = await fs.pathExists(customizePath);
await builder.buildAgent(agentYamlPath, customizeExists ? customizePath : null, outputPath, { includeMetadata: true });
console.log(chalk.green(`${agentName} (${moduleEntry.name})`));
builtCount++;
}
}
}
// Also build standalone agents in bmad/agents/ (top-level, for backward compatibility)
const standaloneAgentsDir = path.join(projectDir, bmadFolder, 'agents');
if (await fs.pathExists(standaloneAgentsDir)) { if (await fs.pathExists(standaloneAgentsDir)) {
console.log(chalk.cyan('\nBuilding standalone agents...')); console.log(chalk.cyan('\nBuilding standalone agents...'));
const agentDirs = await fs.readdir(standaloneAgentsDir); const agentDirs = await fs.readdir(standaloneAgentsDir);
@ -205,7 +263,7 @@ async function buildAllAgents(projectDir, force = false) {
console.log(chalk.cyan(` Building standalone agent ${agentName}...`)); console.log(chalk.cyan(` Building standalone agent ${agentName}...`));
const customizePath = path.join(projectDir, 'bmad', '_cfg', 'agents', `${agentName}.customize.yaml`); const customizePath = path.join(projectDir, bmadFolder, '_cfg', 'agents', `${agentName}.customize.yaml`);
const customizeExists = await fs.pathExists(customizePath); const customizeExists = await fs.pathExists(customizePath);
await builder.buildAgent(agentYamlPath, customizeExists ? customizePath : null, outputPath, { includeMetadata: true }); await builder.buildAgent(agentYamlPath, customizeExists ? customizePath : null, outputPath, { includeMetadata: true });
@ -275,8 +333,52 @@ async function checkBuildStatus(projectDir) {
const needsRebuild = []; const needsRebuild = [];
const upToDate = []; const upToDate = [];
// Check standalone agents in bmad/agents/ // Detect .bmad folder name (could be .bmad or bmad)
const standaloneAgentsDir = path.join(projectDir, 'bmad', 'agents'); const bmadFolder = (await fs.pathExists(path.join(projectDir, '.bmad'))) ? '.bmad' : 'bmad';
const bmadDir = path.join(projectDir, bmadFolder);
// Check agents in ALL module directories in .bmad/
if (await fs.pathExists(bmadDir)) {
const moduleEntries = await fs.readdir(bmadDir, { withFileTypes: true });
for (const moduleEntry of moduleEntries) {
// Skip special directories
if (!moduleEntry.isDirectory() || moduleEntry.name === '_cfg' || moduleEntry.name === 'docs') {
continue;
}
const modulePath = path.join(bmadDir, moduleEntry.name);
const agentsPath = path.join(modulePath, 'agents');
// Check if this module has an agents/ directory
if (!(await fs.pathExists(agentsPath))) {
continue;
}
const agentFiles = await fs.readdir(agentsPath);
for (const file of agentFiles) {
if (!file.endsWith('.agent.yaml')) {
continue;
}
const agentName = file.replace('.agent.yaml', '');
const agentYamlPath = path.join(agentsPath, file);
const outputPath = path.join(agentsPath, `${agentName}.md`);
if (!(await fs.pathExists(outputPath))) {
needsRebuild.push(`${agentName} (${moduleEntry.name})`);
} else if (await checkIfNeedsRebuild(agentYamlPath, outputPath, projectDir, agentName)) {
needsRebuild.push(`${agentName} (${moduleEntry.name})`);
} else {
upToDate.push(`${agentName} (${moduleEntry.name})`);
}
}
}
}
// Check standalone agents in bmad/agents/ (top-level)
const standaloneAgentsDir = path.join(projectDir, bmadFolder, 'agents');
if (await fs.pathExists(standaloneAgentsDir)) { if (await fs.pathExists(standaloneAgentsDir)) {
const agentDirs = await fs.readdir(standaloneAgentsDir); const agentDirs = await fs.readdir(standaloneAgentsDir);
@ -406,8 +508,42 @@ async function checkIfNeedsRebuild(yamlPath, outputPath, projectDir, agentName)
* List available agents * List available agents
*/ */
async function listAvailableAgents(projectDir) { async function listAvailableAgents(projectDir) {
// List standalone agents first // Detect .bmad folder name (could be .bmad or bmad)
const standaloneAgentsDir = path.join(projectDir, 'bmad', 'agents'); const bmadFolder = (await fs.pathExists(path.join(projectDir, '.bmad'))) ? '.bmad' : 'bmad';
const bmadDir = path.join(projectDir, bmadFolder);
// List agents from ALL module directories in .bmad/
if (await fs.pathExists(bmadDir)) {
console.log(chalk.dim(' Module agents:'));
const moduleEntries = await fs.readdir(bmadDir, { withFileTypes: true });
for (const moduleEntry of moduleEntries) {
// Skip special directories
if (!moduleEntry.isDirectory() || moduleEntry.name === '_cfg' || moduleEntry.name === 'docs') {
continue;
}
const modulePath = path.join(bmadDir, moduleEntry.name);
const agentsPath = path.join(modulePath, 'agents');
// Check if this module has an agents/ directory
if (!(await fs.pathExists(agentsPath))) {
continue;
}
const agentFiles = await fs.readdir(agentsPath);
for (const file of agentFiles) {
if (file.endsWith('.agent.yaml')) {
const agentName = file.replace('.agent.yaml', '');
console.log(chalk.dim(` - ${agentName} (${moduleEntry.name})`));
}
}
}
}
// List standalone agents
const standaloneAgentsDir = path.join(projectDir, bmadFolder, 'agents');
if (await fs.pathExists(standaloneAgentsDir)) { if (await fs.pathExists(standaloneAgentsDir)) {
console.log(chalk.dim(' Standalone agents:')); console.log(chalk.dim(' Standalone agents:'));
const agentDirs = await fs.readdir(standaloneAgentsDir); const agentDirs = await fs.readdir(standaloneAgentsDir);

View File

@ -51,6 +51,7 @@ class Installer {
this.configCollector = new ConfigCollector(); this.configCollector = new ConfigCollector();
this.ideConfigManager = new IdeConfigManager(); this.ideConfigManager = new IdeConfigManager();
this.installedFiles = []; // Track all installed files 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}'); content = content.replaceAll('{*bmad_folder*}', '{bmad_folder}');
} }
// Process AgentVibes injection points // Process AgentVibes injection points (pass targetPath for tracking)
content = this.processTTSInjectionPoints(content); content = this.processTTSInjectionPoints(content, targetPath);
// Write to target with replaced content // Write to target with replaced content
await fs.ensureDir(path.dirname(targetPath)); await fs.ensureDir(path.dirname(targetPath));
@ -226,10 +227,14 @@ class Installer {
* - src/modules/bmm/agents/*.md (rules sections) * - src/modules/bmm/agents/*.md (rules sections)
* - TTS Hook: .claude/hooks/bmad-speak.sh (in AgentVibes repo) * - 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) // Check if AgentVibes is enabled (set during installation configuration)
const enableAgentVibes = this.enableAgentVibes || false; 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) { if (enableAgentVibes) {
// Replace party-mode injection marker with actual TTS call // Replace party-mode injection marker with actual TTS call
// Use single quotes to prevent shell expansion of special chars like ! // 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 IMPORTANT: Use single quotes as shown - do NOT escape special characters like ! or $ inside single quotes
Run in background (&) to avoid blocking`, 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 { } else {
// Strip injection markers cleanly when AgentVibes is disabled // Strip injection markers cleanly when AgentVibes is disabled
content = content.replaceAll(/<!-- TTS_INJECTION:party-mode -->\n?/g, ''); 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, modules: config.modules,
ides: config.ides, ides: config.ides,
customFiles: customFiles.length > 0 ? customFiles : undefined, 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) // 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 // Build YAML + customize to .md
const customizeExists = await fs.pathExists(customizePath); 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, includeMetadata: true,
}); });
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime // DO NOT replace {project-root} - LLMs understand this placeholder at runtime
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir); // 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 // Write the built .md file to bmad/{module}/agents/ with POSIX-compliant final newline
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n'; const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
await fs.writeFile(mdPath, content, 'utf8'); 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 // 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, includeMetadata: true,
}); });
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime // DO NOT replace {project-root} - LLMs understand this placeholder at runtime
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir); // 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 // Write the built .md file with POSIX-compliant final newline
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n'; const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
await fs.writeFile(targetMdPath, content, 'utf8'); 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 // 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, includeMetadata: true,
}); });
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime // DO NOT replace {project-root} - LLMs understand this placeholder at runtime
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir); // 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 // Write the rebuilt .md file with POSIX-compliant final newline
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n'; const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
await fs.writeFile(targetMdPath, content, 'utf8'); await fs.writeFile(targetMdPath, content, 'utf8');
@ -1792,7 +1814,17 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Rebuild module agents from installer source // Rebuild module agents from installer source
const agentsPath = path.join(modulePath, 'agents'); const agentsPath = path.join(modulePath, 'agents');
if (await fs.pathExists(agentsPath)) { if (await fs.pathExists(agentsPath)) {
// Check if this module has source in the installer
const sourceAgentsPath =
entry.name === 'core'
? path.join(getModulePath('core'), 'agents')
: path.join(getSourcePath(`modules/${entry.name}`), 'agents');
// Only rebuild if source exists in installer, otherwise skip (for custom modules)
if (await fs.pathExists(sourceAgentsPath)) {
await this.rebuildAgentFiles(modulePath, entry.name); await this.rebuildAgentFiles(modulePath, entry.name);
}
const agentFiles = await fs.readdir(agentsPath); const agentFiles = await fs.readdir(agentsPath);
agentCount += agentFiles.filter((f) => f.endsWith('.md')).length; agentCount += agentFiles.filter((f) => f.endsWith('.md')).length;
} }
@ -1817,9 +1849,16 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
spinner.succeed('No custom agents found to rebuild'); spinner.succeed('No custom agents found to rebuild');
} }
// Skip full manifest regeneration during compileAgents to preserve custom agents // Detect installed modules for manifest regeneration and IDE configuration
// Custom agents are already added to manifests during individual installation spinner.start('Regenerating manifests...');
// Only regenerate YAML manifest for IDE updates if needed const existingInstall = await this.detector.detect(bmadDir);
const installedModules = existingInstall.modules.map((m) => m.id);
// Regenerate manifests to include all discovered content (including custom)
const { ManifestGenerator } = require('./manifest-generator');
const manifestGen = new ManifestGenerator();
// Get existing IDE list from current manifest
const existingManifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml'); const existingManifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
let existingIdes = []; let existingIdes = [];
if (await fs.pathExists(existingManifestPath)) { if (await fs.pathExists(existingManifestPath)) {
@ -1829,6 +1868,12 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
existingIdes = manifest.ides || []; existingIdes = manifest.ides || [];
} }
await manifestGen.generateManifests(bmadDir, installedModules, [], {
ides: existingIdes,
preservedModules: [],
});
spinner.succeed('Manifests regenerated');
// Update IDE configurations using the existing IDE list from manifest // Update IDE configurations using the existing IDE list from manifest
if (existingIdes && existingIdes.length > 0) { if (existingIdes && existingIdes.length > 0) {
spinner.start('Updating IDE configurations...'); spinner.start('Updating IDE configurations...');

View File

@ -87,20 +87,24 @@ class ManifestGenerator {
} }
/** /**
* Collect all workflows from core and selected modules * Collect all workflows from ALL directories in bmad installation
* Scans the INSTALLED bmad directory, not the source * Scans the INSTALLED bmad directory, not the source
*/ */
async collectWorkflows(selectedModules) { async collectWorkflows(selectedModules) {
this.workflows = []; this.workflows = [];
// Use updatedModules which already includes deduplicated 'core' + selectedModules // Scan all directories under bmad installation
for (const moduleName of this.updatedModules) { const entries = await fs.readdir(this.bmadDir, { withFileTypes: true });
const modulePath = path.join(this.bmadDir, moduleName);
if (await fs.pathExists(modulePath)) { for (const entry of entries) {
const moduleWorkflows = await this.getWorkflowsFromPath(modulePath, moduleName); // Skip special directories that don't contain modules
this.workflows.push(...moduleWorkflows); if (!entry.isDirectory() || entry.name === '_cfg' || entry.name === 'docs') {
continue;
} }
const modulePath = path.join(this.bmadDir, entry.name);
const moduleWorkflows = await this.getWorkflowsFromPath(modulePath, entry.name);
this.workflows.push(...moduleWorkflows);
} }
} }
@ -184,23 +188,32 @@ class ManifestGenerator {
} }
/** /**
* Collect all agents from core and selected modules * Collect all agents from ALL directories in bmad installation
* Scans the INSTALLED bmad directory, not the source * Scans the INSTALLED bmad directory, not the source
*/ */
async collectAgents(selectedModules) { async collectAgents(selectedModules) {
this.agents = []; this.agents = [];
// Use updatedModules which already includes deduplicated 'core' + selectedModules // Scan all directories under bmad installation
for (const moduleName of this.updatedModules) { const entries = await fs.readdir(this.bmadDir, { withFileTypes: true });
const agentsPath = path.join(this.bmadDir, moduleName, 'agents');
for (const entry of entries) {
// Skip special directories that don't contain modules
if (!entry.isDirectory() || entry.name === '_cfg' || entry.name === 'docs') {
continue;
}
const modulePath = path.join(this.bmadDir, entry.name);
// Check for agents/ subdirectory in this module
const agentsPath = path.join(modulePath, 'agents');
if (await fs.pathExists(agentsPath)) { if (await fs.pathExists(agentsPath)) {
const moduleAgents = await this.getAgentsFromDir(agentsPath, moduleName); const moduleAgents = await this.getAgentsFromDir(agentsPath, entry.name);
this.agents.push(...moduleAgents); this.agents.push(...moduleAgents);
} }
} }
// Get standalone agents from bmad/agents/ directory // Also check for standalone agents in bmad/agents/ directory (top-level)
const standaloneAgentsDir = path.join(this.bmadDir, 'agents'); const standaloneAgentsDir = path.join(this.bmadDir, 'agents');
if (await fs.pathExists(standaloneAgentsDir)) { if (await fs.pathExists(standaloneAgentsDir)) {
const agentDirs = await fs.readdir(standaloneAgentsDir, { withFileTypes: true }); const agentDirs = await fs.readdir(standaloneAgentsDir, { withFileTypes: true });
@ -292,18 +305,27 @@ class ManifestGenerator {
} }
/** /**
* Collect all tasks from core and selected modules * Collect all tasks from ALL directories in bmad installation
* Scans the INSTALLED bmad directory, not the source * Scans the INSTALLED bmad directory, not the source
*/ */
async collectTasks(selectedModules) { async collectTasks(selectedModules) {
this.tasks = []; this.tasks = [];
// Use updatedModules which already includes deduplicated 'core' + selectedModules // Scan all directories under bmad installation
for (const moduleName of this.updatedModules) { const entries = await fs.readdir(this.bmadDir, { withFileTypes: true });
const tasksPath = path.join(this.bmadDir, moduleName, 'tasks');
for (const entry of entries) {
// Skip special directories that don't contain modules
if (!entry.isDirectory() || entry.name === '_cfg' || entry.name === 'docs') {
continue;
}
const modulePath = path.join(this.bmadDir, entry.name);
// Check for tasks/ subdirectory in this module
const tasksPath = path.join(modulePath, 'tasks');
if (await fs.pathExists(tasksPath)) { if (await fs.pathExists(tasksPath)) {
const moduleTasks = await this.getTasksFromDir(tasksPath, moduleName); const moduleTasks = await this.getTasksFromDir(tasksPath, entry.name);
this.tasks.push(...moduleTasks); this.tasks.push(...moduleTasks);
} }
} }

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 * Compile agent file to .md
* @param {string} yamlPath - Path to agent YAML file * @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 * @returns {Object} Compilation result
*/ */
function compileAgentFile(yamlPath, options = {}) { function compileAgentFile(yamlPath, options = {}) {
@ -501,13 +530,24 @@ function compileAgentFile(yamlPath, options = {}) {
outputPath = path.join(dir, `${basename}.md`); 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 // Write compiled XML
fs.writeFileSync(outputPath, result.xml, 'utf8'); fs.writeFileSync(outputPath, xml, 'utf8');
return { return {
...result, ...result,
xml,
outputPath, outputPath,
sourcePath: yamlPath, sourcePath: yamlPath,
ttsInjected,
}; };
} }

View File

@ -363,11 +363,60 @@ class UI {
`🔧 Tools Configured: ${result.ides?.length > 0 ? result.ides.join(', ') : 'none'}`, `🔧 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'), { CLIUtils.displayBox(summary.join('\n\n'), {
borderColor: 'green', borderColor: 'green',
borderStyle: 'round', 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!')); 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 ""