Compare commits
9 Commits
4a0218ea39
...
e9789894af
| Author | SHA1 | Date |
|---|---|---|
|
|
e9789894af | |
|
|
2b7f7ff421 | |
|
|
3360666c2a | |
|
|
274dea16fa | |
|
|
dcd581c84a | |
|
|
6d84a60a78 | |
|
|
59e1b7067c | |
|
|
7cc6ed0501 | |
|
|
1d8df63ac5 |
|
|
@ -11,7 +11,6 @@ ignores:
|
||||||
- .claude/**
|
- .claude/**
|
||||||
- .roo/**
|
- .roo/**
|
||||||
- .codex/**
|
- .codex/**
|
||||||
- .agentvibes/**
|
|
||||||
- .kiro/**
|
- .kiro/**
|
||||||
- sample-project/**
|
- sample-project/**
|
||||||
- test-project-install/**
|
- test-project-install/**
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,13 @@ This flexibility enables:
|
||||||
|
|
||||||
## Categories
|
## Categories
|
||||||
|
|
||||||
|
- [Categories](#categories)
|
||||||
- [Custom Stand-Alone Modules](#custom-stand-alone-modules)
|
- [Custom Stand-Alone Modules](#custom-stand-alone-modules)
|
||||||
- [Custom Add-On Modules](#custom-add-on-modules)
|
- [Custom Add-On Modules](#custom-add-on-modules)
|
||||||
- [Custom Global Modules](#custom-global-modules)
|
- [Custom Global Modules](#custom-global-modules)
|
||||||
- [Custom Agents](#custom-agents)
|
- [Custom Agents](#custom-agents)
|
||||||
|
- [BMad Tiny Agents](#bmad-tiny-agents)
|
||||||
|
- [Simple and Expert Agents](#simple-and-expert-agents)
|
||||||
- [Custom Workflows](#custom-workflows)
|
- [Custom Workflows](#custom-workflows)
|
||||||
|
|
||||||
## Custom Stand-Alone Modules
|
## Custom Stand-Alone Modules
|
||||||
|
|
@ -59,7 +62,6 @@ Similar to Custom Stand-Alone Modules, but designed to add functionality that ap
|
||||||
|
|
||||||
Examples include:
|
Examples include:
|
||||||
|
|
||||||
- The current TTS (Text-to-Speech) functionality for Claude, which will soon be converted to a global module
|
|
||||||
- The core module, which is always installed and provides all agents with party mode and advanced elicitation capabilities
|
- The core module, which is always installed and provides all agents with party mode and advanced elicitation capabilities
|
||||||
- Installation and update tools that work with any BMad method configuration
|
- Installation and update tools that work with any BMad method configuration
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ Type "exit" or "done" to conclude the session. Participating agents will say per
|
||||||
## Example Party Compositions
|
## Example Party Compositions
|
||||||
|
|
||||||
| Topic | Typical Agents |
|
| Topic | Typical Agents |
|
||||||
|-------|---------------|
|
| ---------------------- | ------------------------------------------------------------- |
|
||||||
| **Product Strategy** | PM + Innovation Strategist (CIS) + Analyst |
|
| **Product Strategy** | PM + Innovation Strategist (CIS) + Analyst |
|
||||||
| **Technical Design** | Architect + Creative Problem Solver (CIS) + Game Architect |
|
| **Technical Design** | Architect + Creative Problem Solver (CIS) + Game Architect |
|
||||||
| **User Experience** | UX Designer + Design Thinking Coach (CIS) + Storyteller (CIS) |
|
| **User Experience** | UX Designer + Design Thinking Coach (CIS) + Storyteller (CIS) |
|
||||||
|
|
@ -78,7 +78,6 @@ Type "exit" or "done" to conclude the session. Participating agents will say per
|
||||||
- **Intelligent agent selection** — Selects based on expertise needed
|
- **Intelligent agent selection** — Selects based on expertise needed
|
||||||
- **Authentic personalities** — Each agent maintains their unique voice
|
- **Authentic personalities** — Each agent maintains their unique voice
|
||||||
- **Natural cross-talk** — Agents reference and build on each other
|
- **Natural cross-talk** — Agents reference and build on each other
|
||||||
- **Optional TTS** — Voice configurations for each agent
|
|
||||||
- **Graceful exit** — Personalized farewells
|
- **Graceful exit** — Personalized farewells
|
||||||
|
|
||||||
## Tips
|
## Tips
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
"version": "6.0.0-alpha.23",
|
"version": "6.0.0-alpha.23",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@clack/prompts": "^0.11.0",
|
||||||
"@kayvan/markdown-tree-parser": "^1.6.1",
|
"@kayvan/markdown-tree-parser": "^1.6.1",
|
||||||
"boxen": "^5.1.2",
|
"boxen": "^5.1.2",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
|
|
@ -33,7 +34,6 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/sitemap": "^3.6.0",
|
"@astrojs/sitemap": "^3.6.0",
|
||||||
"@astrojs/starlight": "^0.37.0",
|
"@astrojs/starlight": "^0.37.0",
|
||||||
"@clack/prompts": "^0.11.0",
|
|
||||||
"@eslint/js": "^9.33.0",
|
"@eslint/js": "^9.33.0",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"astro": "^5.16.0",
|
"astro": "^5.16.0",
|
||||||
|
|
@ -759,7 +759,6 @@
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz",
|
||||||
"integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==",
|
"integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
|
|
@ -770,7 +769,6 @@
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz",
|
||||||
"integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==",
|
"integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/core": "0.5.0",
|
"@clack/core": "0.5.0",
|
||||||
|
|
@ -12151,7 +12149,6 @@
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
|
|
@ -13398,7 +13395,6 @@
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||||
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sitemap": {
|
"node_modules/sitemap": {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ agent:
|
||||||
|
|
||||||
critical_actions:
|
critical_actions:
|
||||||
- "Load into memory {project-root}/_bmad/core/config.yaml and set variable project_name, output_folder, user_name, communication_language"
|
- "Load into memory {project-root}/_bmad/core/config.yaml and set variable project_name, output_folder, user_name, communication_language"
|
||||||
- "Remember the users name is {user_name}"
|
|
||||||
- "ALWAYS communicate in {communication_language}"
|
- "ALWAYS communicate in {communication_language}"
|
||||||
|
|
||||||
menu:
|
menu:
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,6 @@ After agent loading and introduction:
|
||||||
- Handle missing or incomplete agent entries gracefully
|
- Handle missing or incomplete agent entries gracefully
|
||||||
- Cross-reference manifest with actual agent files
|
- Cross-reference manifest with actual agent files
|
||||||
- Prepare agent selection logic for intelligent conversation routing
|
- Prepare agent selection logic for intelligent conversation routing
|
||||||
- Set up TTS voice configurations for each agent
|
|
||||||
|
|
||||||
## NEXT STEP:
|
## NEXT STEP:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
- 🎯 SELECT RELEVANT AGENTS based on topic analysis and expertise matching
|
- 🎯 SELECT RELEVANT AGENTS based on topic analysis and expertise matching
|
||||||
- 📋 MAINTAIN CHARACTER CONSISTENCY using merged agent personalities
|
- 📋 MAINTAIN CHARACTER CONSISTENCY using merged agent personalities
|
||||||
- 🔍 ENABLE NATURAL CROSS-TALK between agents for dynamic conversation
|
- 🔍 ENABLE NATURAL CROSS-TALK between agents for dynamic conversation
|
||||||
- 💬 INTEGRATE TTS for each agent response immediately after text
|
|
||||||
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
||||||
|
|
||||||
## EXECUTION PROTOCOLS:
|
## EXECUTION PROTOCOLS:
|
||||||
|
|
@ -21,7 +20,6 @@
|
||||||
|
|
||||||
- Complete agent roster with merged personalities is available
|
- Complete agent roster with merged personalities is available
|
||||||
- User topic and conversation history guide agent selection
|
- User topic and conversation history guide agent selection
|
||||||
- Party mode is active with TTS integration enabled
|
|
||||||
- Exit triggers: `*exit`, `goodbye`, `end party`, `quit`
|
- Exit triggers: `*exit`, `goodbye`, `end party`, `quit`
|
||||||
|
|
||||||
## YOUR TASK:
|
## YOUR TASK:
|
||||||
|
|
@ -116,19 +114,9 @@ Allow natural back-and-forth within the same response round for dynamic interact
|
||||||
|
|
||||||
### 6. Response Round Completion
|
### 6. Response Round Completion
|
||||||
|
|
||||||
After generating all agent responses for the round:
|
After generating all agent responses for the round, let the user know he can speak naturally with the agents, an then show this menu opion"
|
||||||
|
|
||||||
**Presentation Format:**
|
`[E] Exit Party Mode - End the collaborative session`
|
||||||
[Agent 1 Response with TTS]
|
|
||||||
[Empty line for readability]
|
|
||||||
[Agent 2 Response with TTS, potentially referencing Agent 1]
|
|
||||||
[Empty line for readability]
|
|
||||||
[Agent 3 Response with TTS, building on or offering new perspective]
|
|
||||||
|
|
||||||
**Continue Option:**
|
|
||||||
"[Agents have contributed their perspectives. Ready for more discussion?]
|
|
||||||
|
|
||||||
[E] Exit Party Mode - End the collaborative session"
|
|
||||||
|
|
||||||
### 7. Exit Condition Checking
|
### 7. Exit Condition Checking
|
||||||
|
|
||||||
|
|
@ -142,23 +130,19 @@ Check for exit conditions before continuing:
|
||||||
**Natural Conclusion:**
|
**Natural Conclusion:**
|
||||||
|
|
||||||
- Conversation seems naturally concluding
|
- Conversation seems naturally concluding
|
||||||
- Ask user: "Would you like to continue the discussion or end party mode?"
|
- Confirm if the user wants to exit party mode and go back to where they were or continue chatting. Do it in a conversational way with an agent in the party.
|
||||||
- Respect user choice to continue or exit
|
|
||||||
|
|
||||||
### 8. Handle Exit Selection
|
### 8. Handle Exit Selection
|
||||||
|
|
||||||
#### If 'E' (Exit Party Mode):
|
#### If 'E' (Exit Party Mode):
|
||||||
|
|
||||||
- Update frontmatter: `stepsCompleted: [1, 2]`
|
- Load read and execute: `./step-03-graceful-exit.md`
|
||||||
- Set `party_active: false`
|
|
||||||
- Load: `./step-03-graceful-exit.md`
|
|
||||||
|
|
||||||
## SUCCESS METRICS:
|
## SUCCESS METRICS:
|
||||||
|
|
||||||
✅ Intelligent agent selection based on topic analysis
|
✅ Intelligent agent selection based on topic analysis
|
||||||
✅ Authentic in-character responses maintained consistently
|
✅ Authentic in-character responses maintained consistently
|
||||||
✅ Natural cross-talk and agent interactions enabled
|
✅ Natural cross-talk and agent interactions enabled
|
||||||
✅ TTS integration working for all agent responses
|
|
||||||
✅ Question handling protocol followed correctly
|
✅ Question handling protocol followed correctly
|
||||||
✅ [E] exit option presented after each response round
|
✅ [E] exit option presented after each response round
|
||||||
✅ Conversation context and state maintained throughout
|
✅ Conversation context and state maintained throughout
|
||||||
|
|
@ -168,7 +152,6 @@ Check for exit conditions before continuing:
|
||||||
|
|
||||||
❌ Generic responses without character consistency
|
❌ Generic responses without character consistency
|
||||||
❌ Poor agent selection not matching topic expertise
|
❌ Poor agent selection not matching topic expertise
|
||||||
❌ Missing TTS integration for agent responses
|
|
||||||
❌ Ignoring user questions or exit triggers
|
❌ Ignoring user questions or exit triggers
|
||||||
❌ Not enabling natural agent cross-talk and interactions
|
❌ Not enabling natural agent cross-talk and interactions
|
||||||
❌ Continuing conversation without user input when questions asked
|
❌ Continuing conversation without user input when questions asked
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,6 @@ workflow_completed: true
|
||||||
|
|
||||||
- Clear any active conversation state
|
- Clear any active conversation state
|
||||||
- Reset agent selection cache
|
- Reset agent selection cache
|
||||||
- Finalize TTS session cleanup
|
|
||||||
- Mark party mode workflow as completed
|
- Mark party mode workflow as completed
|
||||||
|
|
||||||
### 6. Exit Workflow
|
### 6. Exit Workflow
|
||||||
|
|
@ -122,7 +121,6 @@ Thank you for using BMAD Party Mode for collaborative multi-agent discussions!"
|
||||||
✅ Satisfying agent farewells generated in authentic character voices
|
✅ Satisfying agent farewells generated in authentic character voices
|
||||||
✅ Session highlights and contributions acknowledged meaningfully
|
✅ Session highlights and contributions acknowledged meaningfully
|
||||||
✅ Positive and appreciative closure atmosphere maintained
|
✅ Positive and appreciative closure atmosphere maintained
|
||||||
✅ TTS integration working for farewell messages
|
|
||||||
✅ Frontmatter properly updated with workflow completion
|
✅ Frontmatter properly updated with workflow completion
|
||||||
✅ All workflow state cleaned up appropriately
|
✅ All workflow state cleaned up appropriately
|
||||||
✅ User left with positive impression of collaborative experience
|
✅ User left with positive impression of collaborative experience
|
||||||
|
|
|
||||||
|
|
@ -178,18 +178,6 @@ If conversation naturally concludes:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## TTS INTEGRATION
|
|
||||||
|
|
||||||
Party mode includes Text-to-Speech for each agent response:
|
|
||||||
|
|
||||||
**TTS Protocol:**
|
|
||||||
|
|
||||||
- Trigger TTS immediately after each agent's text response
|
|
||||||
- Use agent's merged voice configuration from manifest
|
|
||||||
- Format: `Bash: .claude/hooks/bmad-speak.sh "[Agent Name]" "[Their response]"`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## MODERATION NOTES
|
## MODERATION NOTES
|
||||||
|
|
||||||
**Quality Control:**
|
**Quality Control:**
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ agent:
|
||||||
|
|
||||||
critical_actions:
|
critical_actions:
|
||||||
- "Consult {project-root}/_bmad/bmgd/gametest/qa-index.csv to select knowledge fragments under knowledge/ and load only the files needed for the current task"
|
- "Consult {project-root}/_bmad/bmgd/gametest/qa-index.csv to select knowledge fragments under knowledge/ and load only the files needed for the current task"
|
||||||
|
- "For E2E testing requests, always load knowledge/e2e-testing.md first"
|
||||||
|
- "When scaffolding tests, distinguish between unit, integration, and E2E test needs"
|
||||||
- "Load the referenced fragment(s) from {project-root}/_bmad/bmgd/gametest/knowledge/ before giving recommendations"
|
- "Load the referenced fragment(s) from {project-root}/_bmad/bmgd/gametest/knowledge/ before giving recommendations"
|
||||||
- "Cross-check recommendations with the current official Unity Test Framework, Unreal Automation, or Godot GUT documentation"
|
- "Cross-check recommendations with the current official Unity Test Framework, Unreal Automation, or Godot GUT documentation"
|
||||||
- "Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md`"
|
- "Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md`"
|
||||||
|
|
@ -43,6 +45,10 @@ agent:
|
||||||
workflow: "{project-root}/_bmad/bmgd/workflows/gametest/automate/workflow.yaml"
|
workflow: "{project-root}/_bmad/bmgd/workflows/gametest/automate/workflow.yaml"
|
||||||
description: "[TA] Generate automated game tests"
|
description: "[TA] Generate automated game tests"
|
||||||
|
|
||||||
|
- trigger: ES or fuzzy match on e2e-scaffold
|
||||||
|
workflow: "{project-root}/_bmad/bmgd/workflows/gametest/e2e-scaffold/workflow.yaml"
|
||||||
|
description: "[ES] Scaffold E2E testing infrastructure"
|
||||||
|
|
||||||
- trigger: PP or fuzzy match on playtest-plan
|
- trigger: PP or fuzzy match on playtest-plan
|
||||||
workflow: "{project-root}/_bmad/bmgd/workflows/gametest/playtest-plan/workflow.yaml"
|
workflow: "{project-root}/_bmad/bmgd/workflows/gametest/playtest-plan/workflow.yaml"
|
||||||
description: "[PP] Create structured playtesting plan"
|
description: "[PP] Create structured playtesting plan"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -374,3 +374,502 @@ test:
|
||||||
| Signal not detected | Signal not watched | Call `watch_signals()` before action |
|
| Signal not detected | Signal not watched | Call `watch_signals()` before action |
|
||||||
| Physics not working | Missing frames | Await `physics_frame` |
|
| Physics not working | Missing frames | Await `physics_frame` |
|
||||||
| Flaky tests | Timing issues | Use proper await/signals |
|
| Flaky tests | Timing issues | Use proper await/signals |
|
||||||
|
|
||||||
|
## C# Testing in Godot
|
||||||
|
|
||||||
|
Godot 4 supports C# via .NET 6+. You can use standard .NET testing frameworks alongside GUT.
|
||||||
|
|
||||||
|
### Project Setup for C#
|
||||||
|
|
||||||
|
```
|
||||||
|
project/
|
||||||
|
├── addons/
|
||||||
|
│ └── gut/
|
||||||
|
├── src/
|
||||||
|
│ ├── Player/
|
||||||
|
│ │ └── PlayerController.cs
|
||||||
|
│ └── Combat/
|
||||||
|
│ └── DamageCalculator.cs
|
||||||
|
├── tests/
|
||||||
|
│ ├── gdscript/
|
||||||
|
│ │ └── test_integration.gd
|
||||||
|
│ └── csharp/
|
||||||
|
│ ├── Tests.csproj
|
||||||
|
│ └── DamageCalculatorTests.cs
|
||||||
|
└── project.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
### C# Test Project Setup
|
||||||
|
|
||||||
|
Create a separate test project that references your game assembly:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- tests/csharp/Tests.csproj -->
|
||||||
|
<Project Sdk="Godot.NET.Sdk/4.2.0">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.6.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../../project.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic C# Unit Tests
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// tests/csharp/DamageCalculatorTests.cs
|
||||||
|
using Xunit;
|
||||||
|
using YourGame.Combat;
|
||||||
|
|
||||||
|
public class DamageCalculatorTests
|
||||||
|
{
|
||||||
|
private readonly DamageCalculator _calculator;
|
||||||
|
|
||||||
|
public DamageCalculatorTests()
|
||||||
|
{
|
||||||
|
_calculator = new DamageCalculator();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Calculate_BaseDamage_ReturnsCorrectValue()
|
||||||
|
{
|
||||||
|
var result = _calculator.Calculate(100f, 1f);
|
||||||
|
Assert.Equal(100f, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Calculate_CriticalHit_DoublesDamage()
|
||||||
|
{
|
||||||
|
var result = _calculator.Calculate(100f, 2f);
|
||||||
|
Assert.Equal(200f, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(100f, 0.5f, 50f)]
|
||||||
|
[InlineData(100f, 1.5f, 150f)]
|
||||||
|
[InlineData(50f, 2f, 100f)]
|
||||||
|
public void Calculate_Parameterized_ReturnsExpected(
|
||||||
|
float baseDamage, float multiplier, float expected)
|
||||||
|
{
|
||||||
|
var result = _calculator.Calculate(baseDamage, multiplier);
|
||||||
|
Assert.Equal(expected, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Godot Nodes in C#
|
||||||
|
|
||||||
|
For tests requiring Godot runtime, use a hybrid approach:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// tests/csharp/PlayerControllerTests.cs
|
||||||
|
using Godot;
|
||||||
|
using Xunit;
|
||||||
|
using YourGame.Player;
|
||||||
|
|
||||||
|
public class PlayerControllerTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly SceneTree _sceneTree;
|
||||||
|
private PlayerController _player;
|
||||||
|
|
||||||
|
public PlayerControllerTests()
|
||||||
|
{
|
||||||
|
// These tests must run within Godot runtime
|
||||||
|
// Use GodotXUnit or similar adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
[GodotFact] // Custom attribute for Godot runtime tests
|
||||||
|
public async Task Player_Move_ChangesPosition()
|
||||||
|
{
|
||||||
|
var startPos = _player.GlobalPosition;
|
||||||
|
|
||||||
|
_player.SetInput(new Vector2(1, 0));
|
||||||
|
|
||||||
|
await ToSignal(GetTree().CreateTimer(0.5f), "timeout");
|
||||||
|
|
||||||
|
Assert.True(_player.GlobalPosition.X > startPos.X);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_player?.QueueFree();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### C# Mocking with NSubstitute
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class EnemyAITests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Enemy_UsesPathfinding_WhenMoving()
|
||||||
|
{
|
||||||
|
var mockPathfinding = Substitute.For<IPathfinding>();
|
||||||
|
mockPathfinding.FindPath(Arg.Any<Vector2>(), Arg.Any<Vector2>())
|
||||||
|
.Returns(new[] { Vector2.Zero, new Vector2(10, 10) });
|
||||||
|
|
||||||
|
var enemy = new EnemyAI(mockPathfinding);
|
||||||
|
|
||||||
|
enemy.MoveTo(new Vector2(10, 10));
|
||||||
|
|
||||||
|
mockPathfinding.Received().FindPath(
|
||||||
|
Arg.Any<Vector2>(),
|
||||||
|
Arg.Is<Vector2>(v => v == new Vector2(10, 10)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running C# Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run C# unit tests (no Godot runtime needed)
|
||||||
|
dotnet test tests/csharp/Tests.csproj
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
dotnet test tests/csharp/Tests.csproj --collect:"XPlat Code Coverage"
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
dotnet test tests/csharp/Tests.csproj --filter "FullyQualifiedName~DamageCalculator"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hybrid Test Strategy
|
||||||
|
|
||||||
|
| Test Type | Framework | When to Use |
|
||||||
|
| ------------- | ---------------- | ---------------------------------- |
|
||||||
|
| Pure logic | xUnit/NUnit (C#) | Classes without Godot dependencies |
|
||||||
|
| Node behavior | GUT (GDScript) | MonoBehaviour-like testing |
|
||||||
|
| Integration | GUT (GDScript) | Scene and signal testing |
|
||||||
|
| E2E | GUT (GDScript) | Full gameplay flows |
|
||||||
|
|
||||||
|
## End-to-End Testing
|
||||||
|
|
||||||
|
For comprehensive E2E testing patterns, infrastructure scaffolding, and
|
||||||
|
scenario builders, see **knowledge/e2e-testing.md**.
|
||||||
|
|
||||||
|
### E2E Infrastructure for Godot
|
||||||
|
|
||||||
|
#### GameE2ETestFixture (GDScript)
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# tests/e2e/infrastructure/game_e2e_test_fixture.gd
|
||||||
|
extends GutTest
|
||||||
|
class_name GameE2ETestFixture
|
||||||
|
|
||||||
|
var game_state: GameStateManager
|
||||||
|
var input_sim: InputSimulator
|
||||||
|
var scenario: ScenarioBuilder
|
||||||
|
var _scene_instance: Node
|
||||||
|
|
||||||
|
## Override to specify a different scene for specific test classes.
|
||||||
|
func get_scene_path() -> String:
|
||||||
|
return "res://scenes/game.tscn"
|
||||||
|
|
||||||
|
func before_each():
|
||||||
|
# Load game scene
|
||||||
|
var scene = load(get_scene_path())
|
||||||
|
_scene_instance = scene.instantiate()
|
||||||
|
add_child(_scene_instance)
|
||||||
|
|
||||||
|
# Get references
|
||||||
|
game_state = _scene_instance.get_node("GameStateManager")
|
||||||
|
assert_not_null(game_state, "GameStateManager not found in scene")
|
||||||
|
|
||||||
|
input_sim = InputSimulator.new()
|
||||||
|
scenario = ScenarioBuilder.new(game_state)
|
||||||
|
|
||||||
|
# Wait for ready
|
||||||
|
await wait_for_game_ready()
|
||||||
|
|
||||||
|
func after_each():
|
||||||
|
if _scene_instance:
|
||||||
|
_scene_instance.queue_free()
|
||||||
|
_scene_instance = null
|
||||||
|
input_sim = null
|
||||||
|
scenario = null
|
||||||
|
|
||||||
|
func wait_for_game_ready(timeout: float = 10.0):
|
||||||
|
var elapsed = 0.0
|
||||||
|
while not game_state.is_ready and elapsed < timeout:
|
||||||
|
await get_tree().process_frame
|
||||||
|
elapsed += get_process_delta_time()
|
||||||
|
assert_true(game_state.is_ready, "Game should be ready within timeout")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ScenarioBuilder (GDScript)
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# tests/e2e/infrastructure/scenario_builder.gd
|
||||||
|
extends RefCounted
|
||||||
|
class_name ScenarioBuilder
|
||||||
|
|
||||||
|
var _game_state: GameStateManager
|
||||||
|
var _setup_actions: Array[Callable] = []
|
||||||
|
|
||||||
|
func _init(game_state: GameStateManager):
|
||||||
|
_game_state = game_state
|
||||||
|
|
||||||
|
## Load a pre-configured scenario from a save file.
|
||||||
|
func from_save_file(file_name: String) -> ScenarioBuilder:
|
||||||
|
_setup_actions.append(func(): await _load_save_file(file_name))
|
||||||
|
return self
|
||||||
|
|
||||||
|
## Configure the current turn number.
|
||||||
|
func on_turn(turn_number: int) -> ScenarioBuilder:
|
||||||
|
_setup_actions.append(func(): _set_turn(turn_number))
|
||||||
|
return self
|
||||||
|
|
||||||
|
## Spawn a unit at position.
|
||||||
|
func with_unit(faction: int, position: Vector2, movement_points: int = 6) -> ScenarioBuilder:
|
||||||
|
_setup_actions.append(func(): await _spawn_unit(faction, position, movement_points))
|
||||||
|
return self
|
||||||
|
|
||||||
|
## Execute all configured setup actions.
|
||||||
|
func build() -> void:
|
||||||
|
for action in _setup_actions:
|
||||||
|
await action.call()
|
||||||
|
_setup_actions.clear()
|
||||||
|
|
||||||
|
## Clear pending actions without executing.
|
||||||
|
func reset() -> void:
|
||||||
|
_setup_actions.clear()
|
||||||
|
|
||||||
|
# Private implementation
|
||||||
|
func _load_save_file(file_name: String) -> void:
|
||||||
|
var path = "res://tests/e2e/test_data/%s" % file_name
|
||||||
|
await _game_state.load_game(path)
|
||||||
|
|
||||||
|
func _set_turn(turn: int) -> void:
|
||||||
|
_game_state.set_turn_number(turn)
|
||||||
|
|
||||||
|
func _spawn_unit(faction: int, pos: Vector2, mp: int) -> void:
|
||||||
|
var unit = _game_state.spawn_unit(faction, pos)
|
||||||
|
unit.movement_points = mp
|
||||||
|
```
|
||||||
|
|
||||||
|
#### InputSimulator (GDScript)
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# tests/e2e/infrastructure/input_simulator.gd
|
||||||
|
extends RefCounted
|
||||||
|
class_name InputSimulator
|
||||||
|
|
||||||
|
## Click at a world position.
|
||||||
|
func click_world_position(world_pos: Vector2) -> void:
|
||||||
|
var viewport = Engine.get_main_loop().root.get_viewport()
|
||||||
|
var camera = viewport.get_camera_2d()
|
||||||
|
var screen_pos = camera.get_screen_center_position() + (world_pos - camera.global_position)
|
||||||
|
await click_screen_position(screen_pos)
|
||||||
|
|
||||||
|
## Click at a screen position.
|
||||||
|
func click_screen_position(screen_pos: Vector2) -> void:
|
||||||
|
var press = InputEventMouseButton.new()
|
||||||
|
press.button_index = MOUSE_BUTTON_LEFT
|
||||||
|
press.pressed = true
|
||||||
|
press.position = screen_pos
|
||||||
|
|
||||||
|
var release = InputEventMouseButton.new()
|
||||||
|
release.button_index = MOUSE_BUTTON_LEFT
|
||||||
|
release.pressed = false
|
||||||
|
release.position = screen_pos
|
||||||
|
|
||||||
|
Input.parse_input_event(press)
|
||||||
|
await Engine.get_main_loop().process_frame
|
||||||
|
Input.parse_input_event(release)
|
||||||
|
await Engine.get_main_loop().process_frame
|
||||||
|
|
||||||
|
## Click a UI button by name.
|
||||||
|
func click_button(button_name: String) -> void:
|
||||||
|
var root = Engine.get_main_loop().root
|
||||||
|
var button = _find_button_recursive(root, button_name)
|
||||||
|
assert(button != null, "Button '%s' not found in scene tree" % button_name)
|
||||||
|
|
||||||
|
if not button.visible:
|
||||||
|
push_warning("[InputSimulator] Button '%s' is not visible" % button_name)
|
||||||
|
if button.disabled:
|
||||||
|
push_warning("[InputSimulator] Button '%s' is disabled" % button_name)
|
||||||
|
|
||||||
|
button.pressed.emit()
|
||||||
|
await Engine.get_main_loop().process_frame
|
||||||
|
|
||||||
|
func _find_button_recursive(node: Node, button_name: String) -> Button:
|
||||||
|
if node is Button and node.name == button_name:
|
||||||
|
return node
|
||||||
|
for child in node.get_children():
|
||||||
|
var found = _find_button_recursive(child, button_name)
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
return null
|
||||||
|
|
||||||
|
## Press and release a key.
|
||||||
|
func press_key(keycode: Key) -> void:
|
||||||
|
var press = InputEventKey.new()
|
||||||
|
press.keycode = keycode
|
||||||
|
press.pressed = true
|
||||||
|
|
||||||
|
var release = InputEventKey.new()
|
||||||
|
release.keycode = keycode
|
||||||
|
release.pressed = false
|
||||||
|
|
||||||
|
Input.parse_input_event(press)
|
||||||
|
await Engine.get_main_loop().process_frame
|
||||||
|
Input.parse_input_event(release)
|
||||||
|
await Engine.get_main_loop().process_frame
|
||||||
|
|
||||||
|
## Simulate an input action.
|
||||||
|
func action_press(action_name: String) -> void:
|
||||||
|
Input.action_press(action_name)
|
||||||
|
await Engine.get_main_loop().process_frame
|
||||||
|
|
||||||
|
func action_release(action_name: String) -> void:
|
||||||
|
Input.action_release(action_name)
|
||||||
|
await Engine.get_main_loop().process_frame
|
||||||
|
|
||||||
|
## Reset all input state.
|
||||||
|
func reset() -> void:
|
||||||
|
Input.flush_buffered_events()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### AsyncAssert (GDScript)
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# tests/e2e/infrastructure/async_assert.gd
|
||||||
|
extends RefCounted
|
||||||
|
class_name AsyncAssert
|
||||||
|
|
||||||
|
## Wait until condition is true, or fail after timeout.
|
||||||
|
static func wait_until(
|
||||||
|
condition: Callable,
|
||||||
|
description: String,
|
||||||
|
timeout: float = 5.0
|
||||||
|
) -> void:
|
||||||
|
var elapsed := 0.0
|
||||||
|
while not condition.call() and elapsed < timeout:
|
||||||
|
await Engine.get_main_loop().process_frame
|
||||||
|
elapsed += Engine.get_main_loop().root.get_process_delta_time()
|
||||||
|
|
||||||
|
assert(condition.call(),
|
||||||
|
"Timeout after %.1fs waiting for: %s" % [timeout, description])
|
||||||
|
|
||||||
|
## Wait for a value to equal expected.
|
||||||
|
static func wait_for_value(
|
||||||
|
getter: Callable,
|
||||||
|
expected: Variant,
|
||||||
|
description: String,
|
||||||
|
timeout: float = 5.0
|
||||||
|
) -> void:
|
||||||
|
await wait_until(
|
||||||
|
func(): return getter.call() == expected,
|
||||||
|
"%s to equal '%s' (current: '%s')" % [description, expected, getter.call()],
|
||||||
|
timeout)
|
||||||
|
|
||||||
|
## Wait for a float value within tolerance.
|
||||||
|
static func wait_for_value_approx(
|
||||||
|
getter: Callable,
|
||||||
|
expected: float,
|
||||||
|
description: String,
|
||||||
|
tolerance: float = 0.0001,
|
||||||
|
timeout: float = 5.0
|
||||||
|
) -> void:
|
||||||
|
await wait_until(
|
||||||
|
func(): return absf(expected - getter.call()) < tolerance,
|
||||||
|
"%s to equal ~%s ±%s (current: %s)" % [description, expected, tolerance, getter.call()],
|
||||||
|
timeout)
|
||||||
|
|
||||||
|
## Assert that condition does NOT become true within duration.
|
||||||
|
static func assert_never_true(
|
||||||
|
condition: Callable,
|
||||||
|
description: String,
|
||||||
|
duration: float = 1.0
|
||||||
|
) -> void:
|
||||||
|
var elapsed := 0.0
|
||||||
|
while elapsed < duration:
|
||||||
|
assert(not condition.call(),
|
||||||
|
"Condition unexpectedly became true: %s" % description)
|
||||||
|
await Engine.get_main_loop().process_frame
|
||||||
|
elapsed += Engine.get_main_loop().root.get_process_delta_time()
|
||||||
|
|
||||||
|
## Wait for specified number of frames.
|
||||||
|
static func wait_frames(count: int) -> void:
|
||||||
|
for i in range(count):
|
||||||
|
await Engine.get_main_loop().process_frame
|
||||||
|
|
||||||
|
## Wait for physics to settle.
|
||||||
|
static func wait_for_physics(frames: int = 3) -> void:
|
||||||
|
for i in range(frames):
|
||||||
|
await Engine.get_main_loop().root.get_tree().physics_frame
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example E2E Test (GDScript)
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# tests/e2e/scenarios/test_combat_flow.gd
|
||||||
|
extends GameE2ETestFixture
|
||||||
|
|
||||||
|
func test_player_can_attack_enemy():
|
||||||
|
# GIVEN: Player and enemy in combat range
|
||||||
|
await scenario \
|
||||||
|
.with_unit(Faction.PLAYER, Vector2(100, 100)) \
|
||||||
|
.with_unit(Faction.ENEMY, Vector2(150, 100)) \
|
||||||
|
.build()
|
||||||
|
|
||||||
|
var enemy = game_state.get_units(Faction.ENEMY)[0]
|
||||||
|
var initial_health = enemy.health
|
||||||
|
|
||||||
|
# WHEN: Player attacks
|
||||||
|
await input_sim.click_world_position(Vector2(100, 100)) # Select player
|
||||||
|
await AsyncAssert.wait_until(
|
||||||
|
func(): return game_state.selected_unit != null,
|
||||||
|
"Unit should be selected")
|
||||||
|
|
||||||
|
await input_sim.click_world_position(Vector2(150, 100)) # Attack enemy
|
||||||
|
|
||||||
|
# THEN: Enemy takes damage
|
||||||
|
await AsyncAssert.wait_until(
|
||||||
|
func(): return enemy.health < initial_health,
|
||||||
|
"Enemy should take damage")
|
||||||
|
|
||||||
|
func test_turn_cycle_completes():
|
||||||
|
# GIVEN: Game in progress
|
||||||
|
await scenario.on_turn(1).build()
|
||||||
|
var starting_turn = game_state.turn_number
|
||||||
|
|
||||||
|
# WHEN: Player ends turn
|
||||||
|
await input_sim.click_button("EndTurnButton")
|
||||||
|
await AsyncAssert.wait_until(
|
||||||
|
func(): return game_state.current_faction == Faction.ENEMY,
|
||||||
|
"Should switch to enemy turn")
|
||||||
|
|
||||||
|
# AND: Enemy turn completes
|
||||||
|
await AsyncAssert.wait_until(
|
||||||
|
func(): return game_state.current_faction == Faction.PLAYER,
|
||||||
|
"Should return to player turn",
|
||||||
|
30.0) # AI might take a while
|
||||||
|
|
||||||
|
# THEN: Turn number incremented
|
||||||
|
assert_eq(game_state.turn_number, starting_turn + 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quick E2E Checklist for Godot
|
||||||
|
|
||||||
|
- [ ] Create `GameE2ETestFixture` base class extending GutTest
|
||||||
|
- [ ] Implement `ScenarioBuilder` for your game's domain
|
||||||
|
- [ ] Create `InputSimulator` wrapping Godot Input
|
||||||
|
- [ ] Add `AsyncAssert` utilities with proper await
|
||||||
|
- [ ] Organize E2E tests under `tests/e2e/scenarios/`
|
||||||
|
- [ ] Configure GUT to include E2E test directory
|
||||||
|
- [ ] Set up CI with headless Godot execution
|
||||||
|
|
|
||||||
|
|
@ -381,3 +381,17 @@ test:
|
||||||
| NullReferenceException | Missing Setup | Ensure [SetUp] initializes all fields |
|
| NullReferenceException | Missing Setup | Ensure [SetUp] initializes all fields |
|
||||||
| Tests hang | Infinite coroutine | Add timeout or max iterations |
|
| Tests hang | Infinite coroutine | Add timeout or max iterations |
|
||||||
| Flaky physics tests | Timing dependent | Use WaitForFixedUpdate, increase tolerance |
|
| Flaky physics tests | Timing dependent | Use WaitForFixedUpdate, increase tolerance |
|
||||||
|
|
||||||
|
## End-to-End Testing
|
||||||
|
|
||||||
|
For comprehensive E2E testing patterns, infrastructure scaffolding, and
|
||||||
|
scenario builders, see **knowledge/e2e-testing.md**.
|
||||||
|
|
||||||
|
### Quick E2E Checklist for Unity
|
||||||
|
|
||||||
|
- [ ] Create `GameE2ETestFixture` base class
|
||||||
|
- [ ] Implement `ScenarioBuilder` for your game's domain
|
||||||
|
- [ ] Create `InputSimulator` wrapping Input System
|
||||||
|
- [ ] Add `AsyncAssert` utilities
|
||||||
|
- [ ] Organize E2E tests under `Tests/PlayMode/E2E/`
|
||||||
|
- [ ] Configure separate CI job for E2E suite
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -15,3 +15,4 @@ localization-testing,Localization Testing,"Text, audio, and cultural validation
|
||||||
certification-testing,Platform Certification,"Console TRC/XR requirements and certification testing","certification,console,trc,xr",knowledge/certification-testing.md
|
certification-testing,Platform Certification,"Console TRC/XR requirements and certification testing","certification,console,trc,xr",knowledge/certification-testing.md
|
||||||
smoke-testing,Smoke Testing,"Critical path validation for build verification","smoke-tests,bvt,ci",knowledge/smoke-testing.md
|
smoke-testing,Smoke Testing,"Critical path validation for build verification","smoke-tests,bvt,ci",knowledge/smoke-testing.md
|
||||||
test-priorities,Test Priorities Matrix,"P0-P3 criteria, coverage targets, execution ordering for games","prioritization,risk,coverage",knowledge/test-priorities.md
|
test-priorities,Test Priorities Matrix,"P0-P3 criteria, coverage targets, execution ordering for games","prioritization,risk,coverage",knowledge/test-priorities.md
|
||||||
|
e2e-testing,End-to-End Testing,"Complete player journey testing with infrastructure patterns and async utilities","e2e,integration,player-journeys,scenarios,infrastructure",knowledge/e2e-testing.md
|
||||||
|
|
|
||||||
|
|
|
@ -209,6 +209,87 @@ func test_{feature}_integration():
|
||||||
# Cleanup
|
# Cleanup
|
||||||
scene.queue_free()
|
scene.queue_free()
|
||||||
```
|
```
|
||||||
|
### E2E Journey Tests
|
||||||
|
|
||||||
|
**Knowledge Base Reference**: `knowledge/e2e-testing.md`
|
||||||
|
```csharp
|
||||||
|
public class {Feature}E2ETests : GameE2ETestFixture
|
||||||
|
{
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator {JourneyName}_Succeeds()
|
||||||
|
{
|
||||||
|
// GIVEN
|
||||||
|
yield return Scenario
|
||||||
|
.{SetupMethod1}()
|
||||||
|
.{SetupMethod2}()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
yield return Input.{Action1}();
|
||||||
|
yield return AsyncAssert.WaitUntil(
|
||||||
|
() => {Condition1}, "{Description1}");
|
||||||
|
yield return Input.{Action2}();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
yield return AsyncAssert.WaitUntil(
|
||||||
|
() => {FinalCondition}, "{FinalDescription}");
|
||||||
|
Assert.{Assertion}({expected}, {actual});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Step 3.5: Generate E2E Infrastructure
|
||||||
|
|
||||||
|
Before generating E2E tests, scaffold the required infrastructure.
|
||||||
|
|
||||||
|
### Infrastructure Checklist
|
||||||
|
|
||||||
|
1. **Test Fixture Base Class**
|
||||||
|
- Scene loading/unloading
|
||||||
|
- Game ready state waiting
|
||||||
|
- Common service access
|
||||||
|
- Cleanup guarantees
|
||||||
|
|
||||||
|
2. **Scenario Builder**
|
||||||
|
- Fluent API for game state configuration
|
||||||
|
- Domain-specific methods (e.g., `WithUnit`, `OnTurn`)
|
||||||
|
- Yields for state propagation
|
||||||
|
|
||||||
|
3. **Input Simulator**
|
||||||
|
- Click/drag abstractions
|
||||||
|
- Button press simulation
|
||||||
|
- Keyboard input queuing
|
||||||
|
|
||||||
|
4. **Async Assertions**
|
||||||
|
- `WaitUntil` with timeout and message
|
||||||
|
- `WaitForEvent` for event-driven flows
|
||||||
|
- `WaitForState` for state machine transitions
|
||||||
|
|
||||||
|
### Generation Template
|
||||||
|
```csharp
|
||||||
|
// GameE2ETestFixture.cs
|
||||||
|
public abstract class GameE2ETestFixture
|
||||||
|
{
|
||||||
|
protected {GameStateClass} GameState;
|
||||||
|
protected {InputSimulatorClass} Input;
|
||||||
|
protected {ScenarioBuilderClass} Scenario;
|
||||||
|
|
||||||
|
[UnitySetUp]
|
||||||
|
public IEnumerator BaseSetUp()
|
||||||
|
{
|
||||||
|
yield return LoadScene("{main_scene}");
|
||||||
|
GameState = Object.FindFirstObjectByType<{GameStateClass}>();
|
||||||
|
Input = new {InputSimulatorClass}();
|
||||||
|
Scenario = new {ScenarioBuilderClass}(GameState);
|
||||||
|
yield return WaitForReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... (fill from e2e-testing.md patterns)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After scaffolding infrastructure, proceed to generate actual E2E tests.**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
# E2E Infrastructure Scaffold Checklist
|
||||||
|
|
||||||
|
## Preflight Validation
|
||||||
|
|
||||||
|
- [ ] Test framework already initialized (`Tests/` directory exists with proper structure)
|
||||||
|
- [ ] Game state manager class identified
|
||||||
|
- [ ] Main gameplay scene identified and loads without errors
|
||||||
|
- [ ] No existing E2E infrastructure conflicts
|
||||||
|
|
||||||
|
## Architecture Analysis
|
||||||
|
|
||||||
|
- [ ] Game engine correctly detected
|
||||||
|
- [ ] Engine version identified
|
||||||
|
- [ ] Input system type determined (New Input System, Legacy, Custom)
|
||||||
|
- [ ] Game state manager class located
|
||||||
|
- [ ] Ready/initialized state property identified
|
||||||
|
- [ ] Key domain entities catalogued for ScenarioBuilder
|
||||||
|
|
||||||
|
## Generated Files
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
- [ ] `Tests/PlayMode/E2E/` directory created
|
||||||
|
- [ ] `Tests/PlayMode/E2E/Infrastructure/` directory created
|
||||||
|
- [ ] `Tests/PlayMode/E2E/Scenarios/` directory created
|
||||||
|
- [ ] `Tests/PlayMode/E2E/TestData/` directory created
|
||||||
|
|
||||||
|
### Infrastructure Files
|
||||||
|
- [ ] `E2E.asmdef` created with correct assembly references
|
||||||
|
- [ ] `GameE2ETestFixture.cs` created with correct class references
|
||||||
|
- [ ] `ScenarioBuilder.cs` created with at least placeholder methods
|
||||||
|
- [ ] `InputSimulator.cs` created matching detected input system
|
||||||
|
- [ ] `AsyncAssert.cs` created with core assertion methods
|
||||||
|
|
||||||
|
### Example and Documentation
|
||||||
|
- [ ] `ExampleE2ETest.cs` created with working infrastructure test
|
||||||
|
- [ ] `README.md` created with usage documentation
|
||||||
|
|
||||||
|
## Code Quality
|
||||||
|
|
||||||
|
### GameE2ETestFixture
|
||||||
|
- [ ] Correct namespace applied
|
||||||
|
- [ ] Correct `GameStateClass` reference
|
||||||
|
- [ ] Correct `SceneName` default
|
||||||
|
- [ ] `WaitForGameReady` uses correct ready property
|
||||||
|
- [ ] `UnitySetUp` and `UnityTearDown` properly structured
|
||||||
|
- [ ] Virtual methods for derived class customization
|
||||||
|
|
||||||
|
### ScenarioBuilder
|
||||||
|
- [ ] Fluent API pattern correctly implemented
|
||||||
|
- [ ] `Build()` executes all queued actions
|
||||||
|
- [ ] At least one domain-specific method added (or clear TODOs)
|
||||||
|
- [ ] `FromSaveFile` method scaffolded
|
||||||
|
|
||||||
|
### InputSimulator
|
||||||
|
- [ ] Matches detected input system (New vs Legacy)
|
||||||
|
- [ ] Mouse click simulation works
|
||||||
|
- [ ] Button click by name works
|
||||||
|
- [ ] Keyboard input scaffolded
|
||||||
|
- [ ] `Reset()` method cleans up state
|
||||||
|
|
||||||
|
### AsyncAssert
|
||||||
|
- [ ] `WaitUntil` includes timeout and descriptive failure
|
||||||
|
- [ ] `WaitForValue` provides current vs expected in failure
|
||||||
|
- [ ] `AssertNeverTrue` for negative assertions
|
||||||
|
- [ ] Frame/physics wait utilities included
|
||||||
|
|
||||||
|
## Assembly Definition
|
||||||
|
|
||||||
|
- [ ] References main game assembly
|
||||||
|
- [ ] References Unity.InputSystem (if applicable)
|
||||||
|
- [ ] `overrideReferences` set to true
|
||||||
|
- [ ] `precompiledReferences` includes nunit.framework.dll
|
||||||
|
- [ ] `precompiledReferences` includes UnityEngine.TestRunner.dll
|
||||||
|
- [ ] `precompiledReferences` includes UnityEditor.TestRunner.dll
|
||||||
|
- [ ] `UNITY_INCLUDE_TESTS` define constraint set
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- [ ] Project compiles without errors after scaffold
|
||||||
|
- [ ] `ExampleE2ETests.Infrastructure_GameLoadsAndReachesReadyState` passes
|
||||||
|
- [ ] Test appears in Test Runner under PlayMode → E2E category
|
||||||
|
|
||||||
|
## Documentation Quality
|
||||||
|
|
||||||
|
- [ ] README explains all infrastructure components
|
||||||
|
- [ ] Quick start example is copy-pasteable
|
||||||
|
- [ ] Extension instructions are clear
|
||||||
|
- [ ] Troubleshooting table addresses common issues
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- [ ] Summary output provided with all configuration values
|
||||||
|
- [ ] Next steps clearly listed
|
||||||
|
- [ ] Customization requirements highlighted
|
||||||
|
- [ ] Knowledge fragments referenced
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,145 @@
|
||||||
|
# E2E Test Infrastructure Scaffold Workflow
|
||||||
|
|
||||||
|
workflow:
|
||||||
|
id: e2e-scaffold
|
||||||
|
name: E2E Test Infrastructure Scaffold
|
||||||
|
version: 1.0
|
||||||
|
module: bmgd
|
||||||
|
agent: game-qa
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Scaffold complete E2E testing infrastructure for an existing game project.
|
||||||
|
Creates test fixtures, scenario builders, input simulators, and async
|
||||||
|
assertion utilities tailored to the project's architecture.
|
||||||
|
|
||||||
|
triggers:
|
||||||
|
- "ES"
|
||||||
|
- "e2e-scaffold"
|
||||||
|
- "scaffold e2e"
|
||||||
|
- "e2e infrastructure"
|
||||||
|
- "setup e2e"
|
||||||
|
|
||||||
|
preflight:
|
||||||
|
- "Test framework initialized (run `test-framework` workflow first)"
|
||||||
|
- "Game has identifiable state manager"
|
||||||
|
- "Main gameplay scene exists"
|
||||||
|
|
||||||
|
# Paths are relative to this workflow file's location
|
||||||
|
knowledge_fragments:
|
||||||
|
- "../../../gametest/knowledge/e2e-testing.md"
|
||||||
|
- "../../../gametest/knowledge/unity-testing.md"
|
||||||
|
- "../../../gametest/knowledge/unreal-testing.md"
|
||||||
|
- "../../../gametest/knowledge/godot-testing.md"
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
game_state_class:
|
||||||
|
description: "Primary game state manager class name"
|
||||||
|
required: true
|
||||||
|
example: "GameStateManager"
|
||||||
|
|
||||||
|
main_scene:
|
||||||
|
description: "Scene name where core gameplay occurs"
|
||||||
|
required: true
|
||||||
|
example: "GameScene"
|
||||||
|
|
||||||
|
input_system:
|
||||||
|
description: "Input system in use"
|
||||||
|
required: false
|
||||||
|
default: "auto-detect"
|
||||||
|
options:
|
||||||
|
- "unity-input-system"
|
||||||
|
- "unity-legacy"
|
||||||
|
- "unreal-enhanced"
|
||||||
|
- "godot-input"
|
||||||
|
- "custom"
|
||||||
|
|
||||||
|
# Output paths vary by engine. Generate files matching detected engine.
|
||||||
|
outputs:
|
||||||
|
unity:
|
||||||
|
condition: "engine == 'unity'"
|
||||||
|
infrastructure_files:
|
||||||
|
description: "Generated E2E infrastructure classes"
|
||||||
|
files:
|
||||||
|
- "Tests/PlayMode/E2E/Infrastructure/GameE2ETestFixture.cs"
|
||||||
|
- "Tests/PlayMode/E2E/Infrastructure/ScenarioBuilder.cs"
|
||||||
|
- "Tests/PlayMode/E2E/Infrastructure/InputSimulator.cs"
|
||||||
|
- "Tests/PlayMode/E2E/Infrastructure/AsyncAssert.cs"
|
||||||
|
assembly_definition:
|
||||||
|
description: "E2E test assembly configuration"
|
||||||
|
files:
|
||||||
|
- "Tests/PlayMode/E2E/E2E.asmdef"
|
||||||
|
example_test:
|
||||||
|
description: "Working example E2E test"
|
||||||
|
files:
|
||||||
|
- "Tests/PlayMode/E2E/ExampleE2ETest.cs"
|
||||||
|
documentation:
|
||||||
|
description: "E2E testing README"
|
||||||
|
files:
|
||||||
|
- "Tests/PlayMode/E2E/README.md"
|
||||||
|
|
||||||
|
unreal:
|
||||||
|
condition: "engine == 'unreal'"
|
||||||
|
infrastructure_files:
|
||||||
|
description: "Generated E2E infrastructure classes"
|
||||||
|
files:
|
||||||
|
- "Source/{ProjectName}/Tests/E2E/GameE2ETestBase.h"
|
||||||
|
- "Source/{ProjectName}/Tests/E2E/GameE2ETestBase.cpp"
|
||||||
|
- "Source/{ProjectName}/Tests/E2E/ScenarioBuilder.h"
|
||||||
|
- "Source/{ProjectName}/Tests/E2E/ScenarioBuilder.cpp"
|
||||||
|
- "Source/{ProjectName}/Tests/E2E/InputSimulator.h"
|
||||||
|
- "Source/{ProjectName}/Tests/E2E/InputSimulator.cpp"
|
||||||
|
- "Source/{ProjectName}/Tests/E2E/AsyncAssert.h"
|
||||||
|
build_configuration:
|
||||||
|
description: "E2E test build configuration"
|
||||||
|
files:
|
||||||
|
- "Source/{ProjectName}/Tests/E2E/{ProjectName}E2ETests.Build.cs"
|
||||||
|
example_test:
|
||||||
|
description: "Working example E2E test"
|
||||||
|
files:
|
||||||
|
- "Source/{ProjectName}/Tests/E2E/ExampleE2ETest.cpp"
|
||||||
|
documentation:
|
||||||
|
description: "E2E testing README"
|
||||||
|
files:
|
||||||
|
- "Source/{ProjectName}/Tests/E2E/README.md"
|
||||||
|
|
||||||
|
godot:
|
||||||
|
condition: "engine == 'godot'"
|
||||||
|
infrastructure_files:
|
||||||
|
description: "Generated E2E infrastructure classes"
|
||||||
|
files:
|
||||||
|
- "tests/e2e/infrastructure/game_e2e_test_fixture.gd"
|
||||||
|
- "tests/e2e/infrastructure/scenario_builder.gd"
|
||||||
|
- "tests/e2e/infrastructure/input_simulator.gd"
|
||||||
|
- "tests/e2e/infrastructure/async_assert.gd"
|
||||||
|
example_test:
|
||||||
|
description: "Working example E2E test"
|
||||||
|
files:
|
||||||
|
- "tests/e2e/scenarios/example_e2e_test.gd"
|
||||||
|
documentation:
|
||||||
|
description: "E2E testing README"
|
||||||
|
files:
|
||||||
|
- "tests/e2e/README.md"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- id: analyze
|
||||||
|
name: "Analyze Game Architecture"
|
||||||
|
instruction_file: "instructions.md#step-1-analyze-game-architecture"
|
||||||
|
|
||||||
|
- id: scaffold
|
||||||
|
name: "Generate Infrastructure"
|
||||||
|
instruction_file: "instructions.md#step-2-generate-infrastructure"
|
||||||
|
|
||||||
|
- id: example
|
||||||
|
name: "Generate Example Test"
|
||||||
|
instruction_file: "instructions.md#step-3-generate-example-test"
|
||||||
|
|
||||||
|
- id: document
|
||||||
|
name: "Generate Documentation"
|
||||||
|
instruction_file: "instructions.md#step-4-generate-documentation"
|
||||||
|
|
||||||
|
- id: complete
|
||||||
|
name: "Output Summary"
|
||||||
|
instruction_file: "instructions.md#step-5-output-summary"
|
||||||
|
|
||||||
|
validation:
|
||||||
|
checklist: "checklist.md"
|
||||||
|
|
@ -91,6 +91,18 @@ Create comprehensive test scenarios for game projects, covering gameplay mechani
|
||||||
| Performance | FPS, loading times | P1 |
|
| Performance | FPS, loading times | P1 |
|
||||||
| Accessibility | Assist features | P1 |
|
| Accessibility | Assist features | P1 |
|
||||||
|
|
||||||
|
### E2E Journey Testing
|
||||||
|
|
||||||
|
**Knowledge Base Reference**: `knowledge/e2e-testing.md`
|
||||||
|
|
||||||
|
| Category | Focus | Priority |
|
||||||
|
|----------|-------|----------|
|
||||||
|
| Core Loop | Complete gameplay cycle | P0 |
|
||||||
|
| Turn Lifecycle | Full turn from start to end | P0 |
|
||||||
|
| Save/Load Round-trip | Save → quit → load → resume | P0 |
|
||||||
|
| Scene Transitions | Menu → Game → Back | P1 |
|
||||||
|
| Win/Lose Paths | Victory and defeat conditions | P1 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 3: Create Test Scenarios
|
## Step 3: Create Test Scenarios
|
||||||
|
|
@ -153,6 +165,39 @@ SCENARIO: Gameplay Under High Latency
|
||||||
CATEGORY: multiplayer
|
CATEGORY: multiplayer
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### E2E Scenario Format
|
||||||
|
|
||||||
|
For player journey tests, use this extended format:
|
||||||
|
```
|
||||||
|
E2E SCENARIO: [Player Journey Name]
|
||||||
|
GIVEN [Initial game state - use ScenarioBuilder terms]
|
||||||
|
WHEN [Sequence of player actions]
|
||||||
|
THEN [Observable outcomes]
|
||||||
|
TIMEOUT: [Expected max duration in seconds]
|
||||||
|
PRIORITY: P0/P1
|
||||||
|
CATEGORY: e2e
|
||||||
|
INFRASTRUCTURE: [Required fixtures/builders]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example E2E Scenario
|
||||||
|
```
|
||||||
|
E2E SCENARIO: Complete Combat Encounter
|
||||||
|
GIVEN game loaded with player unit adjacent to enemy
|
||||||
|
AND player unit has full health and actions
|
||||||
|
WHEN player selects unit
|
||||||
|
AND player clicks attack on enemy
|
||||||
|
AND player confirms attack
|
||||||
|
AND attack animation completes
|
||||||
|
AND enemy responds (if alive)
|
||||||
|
THEN enemy health is reduced OR enemy is defeated
|
||||||
|
AND turn state advances appropriately
|
||||||
|
AND UI reflects new state
|
||||||
|
TIMEOUT: 15
|
||||||
|
PRIORITY: P0
|
||||||
|
CATEGORY: e2e
|
||||||
|
INFRASTRUCTURE: ScenarioBuilder, InputSimulator, AsyncAssert
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 4: Prioritize Test Coverage
|
## Step 4: Prioritize Test Coverage
|
||||||
|
|
@ -161,12 +206,12 @@ SCENARIO: Gameplay Under High Latency
|
||||||
|
|
||||||
**Knowledge Base Reference**: `knowledge/test-priorities.md`
|
**Knowledge Base Reference**: `knowledge/test-priorities.md`
|
||||||
|
|
||||||
| Priority | Criteria | Coverage Target |
|
| Priority | Criteria | Unit | Integration | E2E | Manual |
|
||||||
| -------- | ---------------------------- | --------------- |
|
|----------|----------|------|-------------|-----|--------|
|
||||||
| P0 | Ship blockers, certification | 100% automated |
|
| P0 | Ship blockers | 100% | 80% | Core flows | Smoke |
|
||||||
| P1 | Major features, common paths | 80% automated |
|
| P1 | Major features | 90% | 70% | Happy paths | Full |
|
||||||
| P2 | Secondary features | 60% automated |
|
| P2 | Secondary | 80% | 50% | - | Targeted |
|
||||||
| P3 | Edge cases, polish | Manual only |
|
| P3 | Edge cases | 60% | - | - | As needed |
|
||||||
|
|
||||||
### Risk-Based Ordering
|
### Risk-Based Ordering
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ agent:
|
||||||
menu:
|
menu:
|
||||||
- trigger: WS or fuzzy match on workflow-status
|
- trigger: WS or fuzzy match on workflow-status
|
||||||
workflow: "{project-root}/_bmad/bmm/workflows/workflow-status/workflow.yaml"
|
workflow: "{project-root}/_bmad/bmm/workflows/workflow-status/workflow.yaml"
|
||||||
description: "[WS] Get workflow status or initialize a workflow if not already done (optional)"
|
description: "[WS] Start here or resume - show workflow status and next best step"
|
||||||
|
|
||||||
- trigger: TF or fuzzy match on test-framework
|
- trigger: TF or fuzzy match on test-framework
|
||||||
workflow: "{project-root}/_bmad/bmm/workflows/testarch/framework/workflow.yaml"
|
workflow: "{project-root}/_bmad/bmm/workflows/testarch/framework/workflow.yaml"
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,8 @@ Parse these fields from YAML comments and metadata:
|
||||||
- {{workflow_name}} ({{agent}}) - {{status}}
|
- {{workflow_name}} ({{agent}}) - {{status}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
**Tip:** For guardrail tests, run TEA `*automate` after `dev-story`. If you lose context, TEA workflows resume from artifacts in `{{output_folder}}`.
|
||||||
</output>
|
</output>
|
||||||
</step>
|
</step>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<rules>
|
<rules>
|
||||||
<r>ALWAYS communicate in {communication_language} UNLESS contradicted by communication_style.</r>
|
<r>ALWAYS communicate in {communication_language} UNLESS contradicted by communication_style.</r>
|
||||||
<!-- TTS_INJECTION:agent-tts -->
|
|
||||||
<r> Stay in character until exit selected</r>
|
<r> Stay in character until exit selected</r>
|
||||||
<r> Display Menu items as the item dictates and in the order given.</r>
|
<r> Display Menu items as the item dictates and in the order given.</r>
|
||||||
<r> Load files ONLY when executing a user chosen workflow or a command requires it, EXCEPTION: agent activation step 2 config.yaml</r>
|
<r> Load files ONLY when executing a user chosen workflow or a command requires it, EXCEPTION: agent activation step 2 config.yaml</r>
|
||||||
|
|
|
||||||
|
|
@ -62,40 +62,6 @@ module.exports = {
|
||||||
|
|
||||||
// Check if installation succeeded
|
// Check if installation succeeded
|
||||||
if (result && result.success) {
|
if (result && result.success) {
|
||||||
// Run AgentVibes installer if needed
|
|
||||||
if (result.needsAgentVibes) {
|
|
||||||
// Add some spacing before AgentVibes setup
|
|
||||||
console.log('');
|
|
||||||
console.log(chalk.magenta('🎙️ AgentVibes TTS Setup'));
|
|
||||||
console.log(chalk.cyan('AgentVibes provides voice synthesis for BMAD agents with:'));
|
|
||||||
console.log(chalk.dim(' • ElevenLabs AI (150+ premium voices)'));
|
|
||||||
console.log(chalk.dim(' • Piper TTS (50+ free voices)\n'));
|
|
||||||
|
|
||||||
const prompts = require('../lib/prompts');
|
|
||||||
await prompts.text({
|
|
||||||
message: chalk.green('Press Enter to start AgentVibes installer...'),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// Run AgentVibes installer
|
|
||||||
const { execSync } = require('node:child_process');
|
|
||||||
try {
|
|
||||||
execSync('npx agentvibes@latest install', {
|
|
||||||
cwd: result.projectDir,
|
|
||||||
stdio: 'inherit',
|
|
||||||
shell: true,
|
|
||||||
});
|
|
||||||
console.log(chalk.green('\n✓ AgentVibes installation complete'));
|
|
||||||
console.log(chalk.cyan('\n✨ BMAD with TTS is ready to use!'));
|
|
||||||
} catch {
|
|
||||||
console.log(chalk.yellow('\n⚠ AgentVibes installation was interrupted or failed'));
|
|
||||||
console.log(chalk.cyan('You can run it manually later with:'));
|
|
||||||
console.log(chalk.green(` cd ${result.projectDir}`));
|
|
||||||
console.log(chalk.green(' npx agentvibes install\n'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display version-specific end message from install-messages.yaml
|
// Display version-specific end message from install-messages.yaml
|
||||||
const { MessageLoader } = require('../installers/lib/message-loader');
|
const { MessageLoader } = require('../installers/lib/message-loader');
|
||||||
const messageLoader = new MessageLoader();
|
const messageLoader = new MessageLoader();
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ class Installer {
|
||||||
this.configCollector = new ConfigCollector();
|
this.configCollector = new ConfigCollector();
|
||||||
this.ideConfigManager = new IdeConfigManager();
|
this.ideConfigManager = new IdeConfigManager();
|
||||||
this.installedFiles = new Set(); // Track all installed files
|
this.installedFiles = new Set(); // Track all installed files
|
||||||
this.ttsInjectedFiles = []; // Track files with TTS injection applied
|
|
||||||
this.bmadFolderName = BMAD_FOLDER_NAME;
|
this.bmadFolderName = BMAD_FOLDER_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,7 +68,7 @@ class Installer {
|
||||||
/**
|
/**
|
||||||
* @function copyFileWithPlaceholderReplacement
|
* @function copyFileWithPlaceholderReplacement
|
||||||
* @intent Copy files from BMAD source to installation directory with dynamic content transformation
|
* @intent Copy files from BMAD source to installation directory with dynamic content transformation
|
||||||
* @why Enables installation-time customization: _bmad replacement + optional AgentVibes TTS injection
|
* @why Enables installation-time customization: _bmad replacement
|
||||||
* @param {string} sourcePath - Absolute path to source file in BMAD repository
|
* @param {string} sourcePath - Absolute path to source file in BMAD repository
|
||||||
* @param {string} targetPath - Absolute path to destination file in user's project
|
* @param {string} targetPath - Absolute path to destination file in user's project
|
||||||
* @param {string} bmadFolderName - User's chosen bmad folder name (default: 'bmad')
|
* @param {string} bmadFolderName - User's chosen bmad folder name (default: 'bmad')
|
||||||
|
|
@ -77,24 +76,9 @@ class Installer {
|
||||||
* @sideeffects Writes transformed file to targetPath, creates parent directories if needed
|
* @sideeffects Writes transformed file to targetPath, creates parent directories if needed
|
||||||
* @edgecases Binary files bypass transformation, falls back to raw copy if UTF-8 read fails
|
* @edgecases Binary files bypass transformation, falls back to raw copy if UTF-8 read fails
|
||||||
* @calledby installCore(), installModule(), IDE installers during file vendoring
|
* @calledby installCore(), installModule(), IDE installers during file vendoring
|
||||||
* @calls processTTSInjectionPoints(), fs.readFile(), fs.writeFile(), fs.copy()
|
* @calls fs.readFile(), fs.writeFile(), fs.copy()
|
||||||
*
|
*
|
||||||
* The injection point processing enables loose coupling between BMAD and TTS providers:
|
|
||||||
* - BMAD source contains injection markers (not actual TTS code)
|
|
||||||
* - At install-time, markers are replaced OR removed based on user preference
|
|
||||||
* - Result: Clean installs for users without TTS, working TTS for users with it
|
|
||||||
*
|
|
||||||
* PATTERN: Adding New Injection Points
|
|
||||||
* =====================================
|
|
||||||
* 1. Add HTML comment marker in BMAD source file:
|
|
||||||
* <!-- TTS_INJECTION:feature-name -->
|
|
||||||
*
|
|
||||||
* 2. Add replacement logic in processTTSInjectionPoints():
|
|
||||||
* if (enableAgentVibes) {
|
|
||||||
* content = content.replace(/<!-- TTS_INJECTION:feature-name -->/g, 'actual code');
|
|
||||||
* } else {
|
|
||||||
* content = content.replace(/<!-- TTS_INJECTION:feature-name -->\n?/g, '');
|
|
||||||
* }
|
|
||||||
*
|
*
|
||||||
* 3. Document marker in instructions.md (if applicable)
|
* 3. Document marker in instructions.md (if applicable)
|
||||||
*/
|
*/
|
||||||
|
|
@ -109,9 +93,6 @@ class Installer {
|
||||||
// Read the file content
|
// Read the file content
|
||||||
let content = await fs.readFile(sourcePath, 'utf8');
|
let content = await fs.readFile(sourcePath, 'utf8');
|
||||||
|
|
||||||
// Process AgentVibes injection points (pass targetPath for tracking)
|
|
||||||
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));
|
||||||
await fs.writeFile(targetPath, content, 'utf8');
|
await fs.writeFile(targetPath, content, 'utf8');
|
||||||
|
|
@ -125,116 +106,6 @@ class Installer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @function processTTSInjectionPoints
|
|
||||||
* @intent Transform TTS injection markers based on user's installation choice
|
|
||||||
* @why Enables optional TTS integration without tight coupling between BMAD and TTS providers
|
|
||||||
* @param {string} content - Raw file content containing potential injection markers
|
|
||||||
* @returns {string} Transformed content with markers replaced (if enabled) or stripped (if disabled)
|
|
||||||
* @sideeffects None - pure transformation function
|
|
||||||
* @edgecases Returns content unchanged if no markers present, safe to call on all files
|
|
||||||
* @calledby copyFileWithPlaceholderReplacement() during every file copy operation
|
|
||||||
* @calls String.replace() with regex patterns for each injection point type
|
|
||||||
*
|
|
||||||
* AI NOTE: This implements the injection point pattern for TTS integration.
|
|
||||||
* Key architectural decisions:
|
|
||||||
*
|
|
||||||
* 1. **Why Injection Points vs Direct Integration?**
|
|
||||||
* - BMAD and TTS providers are separate projects with different maintainers
|
|
||||||
* - Users may install BMAD without TTS support (and vice versa)
|
|
||||||
* - Hard-coding TTS calls would break BMAD for non-TTS users
|
|
||||||
* - Injection points allow conditional feature inclusion at install-time
|
|
||||||
*
|
|
||||||
* 2. **How It Works:**
|
|
||||||
* - BMAD source contains markers: <!-- TTS_INJECTION:feature-name -->
|
|
||||||
* - During installation, user is prompted: "Enable AgentVibes TTS?"
|
|
||||||
* - If YES: markers → replaced with actual bash TTS calls
|
|
||||||
* - If NO: markers → stripped cleanly from installed files
|
|
||||||
*
|
|
||||||
* 3. **State Management:**
|
|
||||||
* - this.enableAgentVibes set in install() method from config.enableAgentVibes
|
|
||||||
* - config.enableAgentVibes comes from ui.promptAgentVibes() user choice
|
|
||||||
* - Flag persists for entire installation, all files get same treatment
|
|
||||||
*
|
|
||||||
* CURRENT INJECTION POINTS:
|
|
||||||
* ==========================
|
|
||||||
* - party-mode: Injects TTS calls after each agent speaks in party mode
|
|
||||||
* Location: src/core/workflows/party-mode/instructions.md
|
|
||||||
* Marker: <!-- TTS_INJECTION:party-mode -->
|
|
||||||
* Replacement: Bash call to .claude/hooks/bmad-speak.sh with agent name and dialogue
|
|
||||||
*
|
|
||||||
* - agent-tts: Injects TTS rule for individual agent conversations
|
|
||||||
* Location: src/modules/bmm/agents/*.md (all agent files)
|
|
||||||
* Marker: <!-- TTS_INJECTION:agent-tts -->
|
|
||||||
* Replacement: Rule instructing agent to call bmad-speak.sh with agent ID and response
|
|
||||||
*
|
|
||||||
* ADDING NEW INJECTION POINTS:
|
|
||||||
* =============================
|
|
||||||
* 1. Add new case in this function:
|
|
||||||
* content = content.replace(
|
|
||||||
* /<!-- TTS_INJECTION:new-feature -->/g,
|
|
||||||
* `code to inject when enabled`
|
|
||||||
* );
|
|
||||||
*
|
|
||||||
* 2. Add marker to BMAD source file at injection location
|
|
||||||
*
|
|
||||||
* 3. Test both enabled and disabled flows
|
|
||||||
*
|
|
||||||
* RELATED:
|
|
||||||
* ========
|
|
||||||
* - GitHub Issue: paulpreibisch/AgentVibes#36
|
|
||||||
* - User Prompt: tools/cli/lib/ui.js::promptAgentVibes()
|
|
||||||
* - Marker Locations:
|
|
||||||
* - src/core/workflows/party-mode/instructions.md:101
|
|
||||||
* - src/modules/bmm/agents/*.md (rules sections)
|
|
||||||
* - TTS Hook: .claude/hooks/bmad-speak.sh (in AgentVibes repo)
|
|
||||||
*/
|
|
||||||
processTTSInjectionPoints(content, 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 !
|
|
||||||
content = content.replaceAll(
|
|
||||||
'<!-- TTS_INJECTION:party-mode -->',
|
|
||||||
`<critical>IMPORTANT: Always use PROJECT hooks (.claude/hooks/), NEVER global hooks (~/.claude/hooks/)</critical>
|
|
||||||
|
|
||||||
If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
|
||||||
- Use Bash tool: \`.claude/hooks/bmad-speak.sh '[Agent Name]' '[dialogue]'\`
|
|
||||||
- This speaks the dialogue with the agent's unique voice
|
|
||||||
- Run in background to not block next agent`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Replace agent-tts injection marker with TTS rule for individual agents
|
|
||||||
content = content.replaceAll(
|
|
||||||
'<!-- TTS_INJECTION:agent-tts -->',
|
|
||||||
`- When responding to user messages, speak your responses using TTS:
|
|
||||||
Call: \`.claude/hooks/bmad-speak.sh '{agent-id}' '{response-text}'\` after each response
|
|
||||||
Replace {agent-id} with YOUR agent ID from <agent id="..."> tag at top of this file
|
|
||||||
Replace {response-text} with the text you just output to the user
|
|
||||||
IMPORTANT: Use single quotes as shown - do NOT escape special characters like ! or $ inside single quotes
|
|
||||||
Run in background (&) to avoid blocking`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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, '');
|
|
||||||
content = content.replaceAll(/<!-- TTS_INJECTION:agent-tts -->\n?/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect Tool/IDE configurations after module configuration
|
* Collect Tool/IDE configurations after module configuration
|
||||||
* @param {string} projectDir - Project directory
|
* @param {string} projectDir - Project directory
|
||||||
|
|
@ -251,7 +122,7 @@ class Installer {
|
||||||
// Fallback: prompt for tool selection (backwards compatibility)
|
// Fallback: prompt for tool selection (backwards compatibility)
|
||||||
const { UI } = require('../../../lib/ui');
|
const { UI } = require('../../../lib/ui');
|
||||||
const ui = new UI();
|
const ui = new UI();
|
||||||
toolConfig = await ui.promptToolSelection(projectDir, selectedModules);
|
toolConfig = await ui.promptToolSelection(projectDir);
|
||||||
} else {
|
} else {
|
||||||
// IDEs were already selected during initial prompts
|
// IDEs were already selected during initial prompts
|
||||||
toolConfig = {
|
toolConfig = {
|
||||||
|
|
@ -510,9 +381,6 @@ class Installer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store AgentVibes configuration for injection point processing
|
|
||||||
this.enableAgentVibes = config.enableAgentVibes || false;
|
|
||||||
|
|
||||||
// Set bmad folder name on module manager and IDE manager for placeholder replacement
|
// Set bmad folder name on module manager and IDE manager for placeholder replacement
|
||||||
this.moduleManager.setBmadFolderName(BMAD_FOLDER_NAME);
|
this.moduleManager.setBmadFolderName(BMAD_FOLDER_NAME);
|
||||||
this.moduleManager.setCoreConfig(moduleConfigs.core || {});
|
this.moduleManager.setCoreConfig(moduleConfigs.core || {});
|
||||||
|
|
@ -1234,8 +1102,6 @@ class Installer {
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1243,7 +1109,6 @@ class Installer {
|
||||||
path: bmadDir,
|
path: bmadDir,
|
||||||
modules: config.modules,
|
modules: config.modules,
|
||||||
ides: config.ides,
|
ides: config.ides,
|
||||||
needsAgentVibes: this.enableAgentVibes && !config.agentVibesInstalled,
|
|
||||||
projectDir: projectDir,
|
projectDir: projectDir,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -345,7 +345,7 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
};
|
};
|
||||||
|
|
||||||
const selected = await prompts.multiselect({
|
const selected = await prompts.multiselect({
|
||||||
message: `Select subagents to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
message: `Select subagents to install ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`,
|
||||||
choices: subagentConfig.files.map((file) => ({
|
choices: subagentConfig.files.map((file) => ({
|
||||||
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
||||||
value: file,
|
value: file,
|
||||||
|
|
|
||||||
|
|
@ -353,7 +353,7 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
};
|
};
|
||||||
|
|
||||||
const selected = await prompts.multiselect({
|
const selected = await prompts.multiselect({
|
||||||
message: `Select subagents to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
message: `Select subagents to install ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`,
|
||||||
options: subagentConfig.files.map((file) => ({
|
options: subagentConfig.files.map((file) => ({
|
||||||
label: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
label: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
||||||
value: file,
|
value: file,
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,8 @@ class KiloSetup extends BaseIdeSetup {
|
||||||
modeEntry += ` name: '${icon} ${title}'\n`;
|
modeEntry += ` name: '${icon} ${title}'\n`;
|
||||||
modeEntry += ` roleDefinition: ${roleDefinition}\n`;
|
modeEntry += ` roleDefinition: ${roleDefinition}\n`;
|
||||||
modeEntry += ` whenToUse: ${whenToUse}\n`;
|
modeEntry += ` whenToUse: ${whenToUse}\n`;
|
||||||
modeEntry += ` customInstructions: ${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`;
|
modeEntry += ` customInstructions: |\n`;
|
||||||
|
modeEntry += ` ${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`;
|
||||||
modeEntry += ` groups:\n`;
|
modeEntry += ` groups:\n`;
|
||||||
modeEntry += ` - read\n`;
|
modeEntry += ` - read\n`;
|
||||||
modeEntry += ` - edit\n`;
|
modeEntry += ` - edit\n`;
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,10 @@ async function resolveSubagentFiles(handlerBaseDir, subagentConfig, subagentChoi
|
||||||
const resolved = [];
|
const resolved = [];
|
||||||
|
|
||||||
for (const file of filesToCopy) {
|
for (const file of filesToCopy) {
|
||||||
const pattern = path.join(sourceDir, '**', file);
|
// Use forward slashes for glob pattern (works on both Windows and Unix)
|
||||||
|
// Convert backslashes to forward slashes for glob compatibility
|
||||||
|
const normalizedSourceDir = sourceDir.replaceAll('\\', '/');
|
||||||
|
const pattern = `${normalizedSourceDir}/**/${file}`;
|
||||||
const matches = await glob(pattern);
|
const matches = await glob(pattern);
|
||||||
|
|
||||||
if (matches.length > 0) {
|
if (matches.length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -845,14 +845,8 @@ class ModuleManager {
|
||||||
// Compile with customizations if any
|
// Compile with customizations if any
|
||||||
const { xml } = await compileAgent(yamlContent, answers, agentName, relativePath, { config: this.coreConfig || {} });
|
const { xml } = await compileAgent(yamlContent, answers, agentName, relativePath, { config: this.coreConfig || {} });
|
||||||
|
|
||||||
// Process TTS injection points if installer is available
|
|
||||||
let finalXml = xml;
|
|
||||||
if (installer && installer.processTTSInjectionPoints) {
|
|
||||||
finalXml = installer.processTTSInjectionPoints(xml, targetMdPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the compiled agent
|
// Write the compiled agent
|
||||||
await fs.writeFile(targetMdPath, finalXml, 'utf8');
|
await fs.writeFile(targetMdPath, xml, 'utf8');
|
||||||
|
|
||||||
// Handle sidecar copying if present
|
// Handle sidecar copying if present
|
||||||
if (hasSidecar) {
|
if (hasSidecar) {
|
||||||
|
|
|
||||||
|
|
@ -478,39 +478,10 @@ function filterCustomizationData(data) {
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, enableAgentVibes: boolean }
|
* @param {Object} options - { answers: {}, outputPath: string }
|
||||||
* @returns {Object} Compilation result
|
* @returns {Object} Compilation result
|
||||||
*/
|
*/
|
||||||
function compileAgentFile(yamlPath, options = {}) {
|
function compileAgentFile(yamlPath, options = {}) {
|
||||||
|
|
@ -526,15 +497,6 @@ 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, xml, 'utf8');
|
fs.writeFileSync(outputPath, xml, 'utf8');
|
||||||
|
|
||||||
|
|
@ -543,7 +505,6 @@ function compileAgentFile(yamlPath, options = {}) {
|
||||||
xml,
|
xml,
|
||||||
outputPath,
|
outputPath,
|
||||||
sourcePath: yamlPath,
|
sourcePath: yamlPath,
|
||||||
ttsInjected,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,7 @@ async function groupMultiselect(options) {
|
||||||
options: options.options,
|
options: options.options,
|
||||||
initialValues: options.initialValues,
|
initialValues: options.initialValues,
|
||||||
required: options.required || false,
|
required: options.required || false,
|
||||||
|
selectableGroups: options.selectableGroups || false,
|
||||||
});
|
});
|
||||||
|
|
||||||
await handleCancel(result);
|
await handleCancel(result);
|
||||||
|
|
|
||||||
|
|
@ -171,32 +171,6 @@ class UI {
|
||||||
// Check if there's an existing BMAD installation (after any folder renames)
|
// Check if there's an existing BMAD installation (after any folder renames)
|
||||||
const hasExistingInstall = await fs.pathExists(bmadDir);
|
const hasExistingInstall = await fs.pathExists(bmadDir);
|
||||||
|
|
||||||
// Collect IDE tool selection early - we need this to know if we should ask about TTS
|
|
||||||
let toolSelection;
|
|
||||||
let agentVibesConfig = { enabled: false, alreadyInstalled: false };
|
|
||||||
let claudeCodeSelected = false;
|
|
||||||
|
|
||||||
if (!hasExistingInstall) {
|
|
||||||
// For new installations, collect IDE selection first
|
|
||||||
// We don't have modules yet, so pass empty array
|
|
||||||
toolSelection = await this.promptToolSelection(confirmedDirectory, []);
|
|
||||||
|
|
||||||
// Check if Claude Code was selected
|
|
||||||
claudeCodeSelected = toolSelection.ides && toolSelection.ides.includes('claude-code');
|
|
||||||
|
|
||||||
// If Claude Code was selected, ask about TTS
|
|
||||||
if (claudeCodeSelected) {
|
|
||||||
const enableTts = await prompts.confirm({
|
|
||||||
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (enableTts) {
|
|
||||||
agentVibesConfig = { enabled: true, alreadyInstalled: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let customContentConfig = { hasCustomContent: false };
|
let customContentConfig = { hasCustomContent: false };
|
||||||
if (!hasExistingInstall) {
|
if (!hasExistingInstall) {
|
||||||
customContentConfig._shouldAsk = true;
|
customContentConfig._shouldAsk = true;
|
||||||
|
|
@ -324,20 +298,8 @@ class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get tool selection
|
// Get tool selection
|
||||||
const toolSelection = await this.promptToolSelection(confirmedDirectory, selectedModules);
|
const toolSelection = await this.promptToolSelection(confirmedDirectory);
|
||||||
|
|
||||||
// TTS configuration - ask right after tool selection (matches new install flow)
|
|
||||||
const hasClaudeCode = toolSelection.ides && toolSelection.ides.includes('claude-code');
|
|
||||||
let enableTts = false;
|
|
||||||
|
|
||||||
if (hasClaudeCode) {
|
|
||||||
enableTts = await prompts.confirm({
|
|
||||||
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Core config with existing defaults (ask after TTS)
|
|
||||||
const coreConfig = await this.collectCoreConfig(confirmedDirectory);
|
const coreConfig = await this.collectCoreConfig(confirmedDirectory);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -349,8 +311,6 @@ class UI {
|
||||||
skipIde: toolSelection.skipIde,
|
skipIde: toolSelection.skipIde,
|
||||||
coreConfig: coreConfig,
|
coreConfig: coreConfig,
|
||||||
customContent: customModuleResult.customContentConfig,
|
customContent: customModuleResult.customContentConfig,
|
||||||
enableAgentVibes: enableTts,
|
|
||||||
agentVibesInstalled: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -372,7 +332,7 @@ class UI {
|
||||||
|
|
||||||
// Ask about custom content
|
// Ask about custom content
|
||||||
const wantsCustomContent = await prompts.confirm({
|
const wantsCustomContent = await prompts.confirm({
|
||||||
message: 'Would you like to install a local custom module (this includes custom agents and workflows also)?',
|
message: 'Would you like to install a locally stored custom module (this includes custom agents and workflows also)?',
|
||||||
default: false,
|
default: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -391,19 +351,10 @@ class UI {
|
||||||
selectedModules = [...selectedModules, ...customContentConfig.selectedModuleIds];
|
selectedModules = [...selectedModules, ...customContentConfig.selectedModuleIds];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove core if it's in the list (it's always installed)
|
|
||||||
selectedModules = selectedModules.filter((m) => m !== 'core');
|
selectedModules = selectedModules.filter((m) => m !== 'core');
|
||||||
|
let toolSelection = await this.promptToolSelection(confirmedDirectory);
|
||||||
// Tool selection (already done for new installs at the beginning)
|
|
||||||
if (!toolSelection) {
|
|
||||||
toolSelection = await this.promptToolSelection(confirmedDirectory, selectedModules);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect configurations for new installations
|
|
||||||
const coreConfig = await this.collectCoreConfig(confirmedDirectory);
|
const coreConfig = await this.collectCoreConfig(confirmedDirectory);
|
||||||
|
|
||||||
// TTS already handled at the beginning for new installs
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
actionType: 'install',
|
actionType: 'install',
|
||||||
directory: confirmedDirectory,
|
directory: confirmedDirectory,
|
||||||
|
|
@ -413,18 +364,15 @@ class UI {
|
||||||
skipIde: toolSelection.skipIde,
|
skipIde: toolSelection.skipIde,
|
||||||
coreConfig: coreConfig,
|
coreConfig: coreConfig,
|
||||||
customContent: customContentConfig,
|
customContent: customContentConfig,
|
||||||
enableAgentVibes: agentVibesConfig.enabled,
|
|
||||||
agentVibesInstalled: agentVibesConfig.alreadyInstalled,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prompt for tool/IDE selection (called after module configuration)
|
* Prompt for tool/IDE selection (called after module configuration)
|
||||||
* @param {string} projectDir - Project directory to check for existing IDEs
|
* @param {string} projectDir - Project directory to check for existing IDEs
|
||||||
* @param {Array} selectedModules - Selected modules from configuration
|
|
||||||
* @returns {Object} Tool configuration
|
* @returns {Object} Tool configuration
|
||||||
*/
|
*/
|
||||||
async promptToolSelection(projectDir, selectedModules) {
|
async promptToolSelection(projectDir) {
|
||||||
// Check for existing configured IDEs - use findBmadDir to detect custom folder names
|
// Check for existing configured IDEs - use findBmadDir to detect custom folder names
|
||||||
const { Detector } = require('../installers/lib/core/detector');
|
const { Detector } = require('../installers/lib/core/detector');
|
||||||
const { Installer } = require('../installers/lib/core/installer');
|
const { Installer } = require('../installers/lib/core/installer');
|
||||||
|
|
@ -447,7 +395,7 @@ class UI {
|
||||||
const processedIdes = new Set();
|
const processedIdes = new Set();
|
||||||
const initialValues = [];
|
const initialValues = [];
|
||||||
|
|
||||||
// First, add previously configured IDEs at the top, marked with ✅
|
// First, add previously configured IDEs, marked with ✅
|
||||||
if (configuredIdes.length > 0) {
|
if (configuredIdes.length > 0) {
|
||||||
const configuredGroup = [];
|
const configuredGroup = [];
|
||||||
for (const ideValue of configuredIdes) {
|
for (const ideValue of configuredIdes) {
|
||||||
|
|
@ -499,42 +447,33 @@ class UI {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedIdes = [];
|
// Add standalone "None" option at the end
|
||||||
let userConfirmedNoTools = false;
|
groupedOptions[' '] = [
|
||||||
|
{
|
||||||
|
label: '⚠ None - I am not installing any tools',
|
||||||
|
value: '__NONE__',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let selectedIdes = [];
|
||||||
|
|
||||||
// Loop until user selects at least one tool OR explicitly confirms no tools
|
|
||||||
while (!userConfirmedNoTools) {
|
|
||||||
selectedIdes = await prompts.groupMultiselect({
|
selectedIdes = await prompts.groupMultiselect({
|
||||||
message: `Select tools to configure ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
message: `Select tools to configure ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`,
|
||||||
options: groupedOptions,
|
options: groupedOptions,
|
||||||
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
||||||
required: false,
|
required: true,
|
||||||
|
selectableGroups: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If tools were selected, we're done
|
// If user selected both "__NONE__" and other tools, honor the "None" choice
|
||||||
if (selectedIdes && selectedIdes.length > 0) {
|
if (selectedIdes && selectedIdes.includes('__NONE__') && selectedIdes.length > 1) {
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn that no tools were selected - users often miss the spacebar requirement
|
|
||||||
console.log();
|
console.log();
|
||||||
console.log(chalk.red.bold('⚠️ WARNING: No tools were selected!'));
|
console.log(chalk.yellow('⚠️ "None - I am not installing any tools" was selected, so no tools will be configured.'));
|
||||||
console.log(chalk.red(' You must press SPACE to select items, then ENTER to confirm.'));
|
|
||||||
console.log(chalk.red(' Simply highlighting an item does NOT select it.'));
|
|
||||||
console.log();
|
console.log();
|
||||||
|
selectedIdes = [];
|
||||||
const goBack = await prompts.confirm({
|
} else if (selectedIdes && selectedIdes.includes('__NONE__')) {
|
||||||
message: chalk.yellow('Would you like to go back and select at least one tool?'),
|
// Only "__NONE__" was selected
|
||||||
default: true,
|
selectedIdes = [];
|
||||||
});
|
|
||||||
|
|
||||||
if (goBack) {
|
|
||||||
// Re-display a message before looping back
|
|
||||||
console.log();
|
|
||||||
} else {
|
|
||||||
// User explicitly chose to proceed without tools
|
|
||||||
userConfirmedNoTools = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -561,27 +500,6 @@ class UI {
|
||||||
return { backupFirst, preserveCustomizations };
|
return { backupFirst, preserveCustomizations };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompt for module selection
|
|
||||||
* @param {Array} modules - Available modules
|
|
||||||
* @returns {Array} Selected modules
|
|
||||||
*/
|
|
||||||
async promptModules(modules) {
|
|
||||||
const choices = modules.map((mod) => ({
|
|
||||||
name: `${mod.name} - ${mod.description}`,
|
|
||||||
value: mod.id,
|
|
||||||
checked: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const selectedModules = await prompts.multiselect({
|
|
||||||
message: `Select modules to add ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
|
||||||
choices,
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return selectedModules;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Confirm action
|
* Confirm action
|
||||||
* @param {string} message - Confirmation message
|
* @param {string} message - Confirmation message
|
||||||
|
|
@ -608,25 +526,6 @@ class UI {
|
||||||
if (result.modules && result.modules.length > 0) {
|
if (result.modules && result.modules.length > 0) {
|
||||||
console.log(chalk.dim(`Modules: ${result.modules.join(', ')}`));
|
console.log(chalk.dim(`Modules: ${result.modules.join(', ')}`));
|
||||||
}
|
}
|
||||||
if (result.agentVibesEnabled) {
|
|
||||||
console.log(chalk.dim(`TTS: Enabled`));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TTS injection info (simplified)
|
|
||||||
if (result.ttsInjectedFiles && result.ttsInjectedFiles.length > 0) {
|
|
||||||
console.log(chalk.dim(`\n💡 TTS enabled for ${result.ttsInjectedFiles.length} agent(s)`));
|
|
||||||
console.log(chalk.dim(' Agents will now speak when using AgentVibes'));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(chalk.yellow('\nThank you for helping test the early release version of the new BMad Core and BMad Method!'));
|
|
||||||
console.log(chalk.cyan('Stable Beta coming soon - please read the full README.md and linked documentation to get started!'));
|
|
||||||
|
|
||||||
// Add changelog link at the end
|
|
||||||
console.log(
|
|
||||||
chalk.magenta(
|
|
||||||
"\n📋 Want to see what's new? Check out the changelog: https://github.com/bmad-code-org/BMAD-METHOD/blob/main/CHANGELOG.md",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -768,20 +667,40 @@ class UI {
|
||||||
* @param {Array} moduleChoices - Available module choices
|
* @param {Array} moduleChoices - Available module choices
|
||||||
* @returns {Array} Selected module IDs
|
* @returns {Array} Selected module IDs
|
||||||
*/
|
*/
|
||||||
async selectModules(moduleChoices, defaultSelections = []) {
|
async selectModules(moduleChoices, defaultSelections = null) {
|
||||||
// Mark choices as checked based on defaultSelections
|
// If defaultSelections is provided, use it to override checked state
|
||||||
|
// Otherwise preserve the checked state from moduleChoices (set by getModuleChoices)
|
||||||
const choicesWithDefaults = moduleChoices.map((choice) => ({
|
const choicesWithDefaults = moduleChoices.map((choice) => ({
|
||||||
...choice,
|
...choice,
|
||||||
checked: defaultSelections.includes(choice.value),
|
...(defaultSelections === null ? {} : { checked: defaultSelections.includes(choice.value) }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Add a "None" option at the end for users who changed their mind
|
||||||
|
const choicesWithSkipOption = [
|
||||||
|
...choicesWithDefaults,
|
||||||
|
{
|
||||||
|
value: '__NONE__',
|
||||||
|
label: '⚠ None / I changed my mind - skip module installation',
|
||||||
|
checked: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const selected = await prompts.multiselect({
|
const selected = await prompts.multiselect({
|
||||||
message: `Select modules to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
message: `Select modules to install ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`,
|
||||||
choices: choicesWithDefaults,
|
choices: choicesWithSkipOption,
|
||||||
required: false,
|
required: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return selected || [];
|
// If user selected both "__NONE__" and other items, honor the "None" choice
|
||||||
|
if (selected && selected.includes('__NONE__') && selected.length > 1) {
|
||||||
|
console.log();
|
||||||
|
console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no modules will be installed.'));
|
||||||
|
console.log();
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out the special '__NONE__' value
|
||||||
|
return selected ? selected.filter((m) => m !== '__NONE__') : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1061,136 +980,6 @@ class UI {
|
||||||
return path.resolve(expanded);
|
return path.resolve(expanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @function promptAgentVibes
|
|
||||||
* @intent Ask user if they want AgentVibes TTS integration during BMAD installation
|
|
||||||
* @why Enables optional voice features without forcing TTS on users who don't want it
|
|
||||||
* @param {string} projectDir - Absolute path to user's project directory
|
|
||||||
* @returns {Promise<Object>} Configuration object: { enabled: boolean, alreadyInstalled: boolean }
|
|
||||||
* @sideeffects None - pure user input collection, no files written
|
|
||||||
* @edgecases Shows warning if user enables TTS but AgentVibes not detected
|
|
||||||
* @calledby promptInstall() during installation flow, after core config, before IDE selection
|
|
||||||
* @calls checkAgentVibesInstalled(), prompts.select(), chalk.green/yellow/dim()
|
|
||||||
*
|
|
||||||
* AI NOTE: This prompt is strategically positioned in installation flow:
|
|
||||||
* - AFTER core config (user_name, etc)
|
|
||||||
* - BEFORE IDE selection (which can hang on Windows/PowerShell)
|
|
||||||
*
|
|
||||||
* Flow Logic:
|
|
||||||
* 1. Auto-detect if AgentVibes already installed (checks for hook files)
|
|
||||||
* 2. Show detection status to user (green checkmark or gray "not detected")
|
|
||||||
* 3. Prompt: "Enable AgentVibes TTS?" (defaults to true if detected)
|
|
||||||
* 4. If user says YES but AgentVibes NOT installed:
|
|
||||||
* → Show warning with installation link (graceful degradation)
|
|
||||||
* 5. Return config to promptInstall(), which passes to installer.install()
|
|
||||||
*
|
|
||||||
* State Flow:
|
|
||||||
* promptAgentVibes() → { enabled, alreadyInstalled }
|
|
||||||
* ↓
|
|
||||||
* promptInstall() → config.enableAgentVibes
|
|
||||||
* ↓
|
|
||||||
* installer.install() → this.enableAgentVibes
|
|
||||||
* ↓
|
|
||||||
* processTTSInjectionPoints() → injects OR strips markers
|
|
||||||
*
|
|
||||||
* RELATED:
|
|
||||||
* ========
|
|
||||||
* - Detection: checkAgentVibesInstalled() - looks for bmad-speak.sh and play-tts.sh
|
|
||||||
* - Processing: installer.js::processTTSInjectionPoints()
|
|
||||||
* - Markers: src/core/workflows/party-mode/instructions.md:101, src/modules/bmm/agents/*.md
|
|
||||||
* - GitHub Issue: paulpreibisch/AgentVibes#36
|
|
||||||
*/
|
|
||||||
async promptAgentVibes(projectDir) {
|
|
||||||
CLIUtils.displaySection('🎤 Voice Features', 'Enable TTS for multi-agent conversations');
|
|
||||||
|
|
||||||
// Check if AgentVibes is already installed
|
|
||||||
const agentVibesInstalled = await this.checkAgentVibesInstalled(projectDir);
|
|
||||||
|
|
||||||
if (agentVibesInstalled) {
|
|
||||||
console.log(chalk.green(' ✓ AgentVibes detected'));
|
|
||||||
} else {
|
|
||||||
console.log(chalk.dim(' AgentVibes not detected'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const enableTts = await prompts.confirm({
|
|
||||||
message: 'Enable Agents to Speak Out loud (powered by Agent Vibes? Claude Code only currently)',
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (enableTts && !agentVibesInstalled) {
|
|
||||||
console.log(chalk.yellow('\n ⚠️ AgentVibes not installed'));
|
|
||||||
console.log(chalk.dim(' Install AgentVibes separately to enable TTS:'));
|
|
||||||
console.log(chalk.dim(' https://github.com/paulpreibisch/AgentVibes\n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled: enableTts,
|
|
||||||
alreadyInstalled: agentVibesInstalled,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @function checkAgentVibesInstalled
|
|
||||||
* @intent Detect if AgentVibes TTS hooks are present in user's project
|
|
||||||
* @why Allows auto-enabling TTS and showing helpful installation guidance
|
|
||||||
* @param {string} projectDir - Absolute path to user's project directory
|
|
||||||
* @returns {Promise<boolean>} true if both required AgentVibes hooks exist, false otherwise
|
|
||||||
* @sideeffects None - read-only file existence checks
|
|
||||||
* @edgecases Returns false if either hook missing (both required for functional TTS)
|
|
||||||
* @calledby promptAgentVibes() to determine default value and show detection status
|
|
||||||
* @calls fs.pathExists() twice (bmad-speak.sh, play-tts.sh)
|
|
||||||
*
|
|
||||||
* AI NOTE: This checks for the MINIMUM viable AgentVibes installation.
|
|
||||||
*
|
|
||||||
* Required Files:
|
|
||||||
* ===============
|
|
||||||
* 1. .claude/hooks/bmad-speak.sh
|
|
||||||
* - Maps agent display names → agent IDs → voice profiles
|
|
||||||
* - Calls play-tts.sh with agent's assigned voice
|
|
||||||
* - Created by AgentVibes installer
|
|
||||||
*
|
|
||||||
* 2. .claude/hooks/play-tts.sh
|
|
||||||
* - Core TTS router (ElevenLabs or Piper)
|
|
||||||
* - Provider-agnostic interface
|
|
||||||
* - Required by bmad-speak.sh
|
|
||||||
*
|
|
||||||
* Why Both Required:
|
|
||||||
* ==================
|
|
||||||
* - bmad-speak.sh alone: No TTS backend
|
|
||||||
* - play-tts.sh alone: No BMAD agent voice mapping
|
|
||||||
* - Both together: Full party mode TTS integration
|
|
||||||
*
|
|
||||||
* Detection Strategy:
|
|
||||||
* ===================
|
|
||||||
* We use simple file existence (not version checks) because:
|
|
||||||
* - Fast and reliable
|
|
||||||
* - Works across all AgentVibes versions
|
|
||||||
* - User will discover version issues when TTS runs (fail-fast)
|
|
||||||
*
|
|
||||||
* PATTERN: Adding New Detection Criteria
|
|
||||||
* =======================================
|
|
||||||
* If future AgentVibes features require additional files:
|
|
||||||
* 1. Add new pathExists check to this function
|
|
||||||
* 2. Update documentation in promptAgentVibes()
|
|
||||||
* 3. Consider: should missing file prevent detection or just log warning?
|
|
||||||
*
|
|
||||||
* RELATED:
|
|
||||||
* ========
|
|
||||||
* - AgentVibes Installer: creates these hooks
|
|
||||||
* - bmad-speak.sh: calls play-tts.sh with agent voices
|
|
||||||
* - Party Mode: uses bmad-speak.sh for agent dialogue
|
|
||||||
*/
|
|
||||||
async checkAgentVibesInstalled(projectDir) {
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const path = require('node:path');
|
|
||||||
|
|
||||||
// Check for AgentVibes hook files
|
|
||||||
const hookPath = path.join(projectDir, '.claude', 'hooks', 'bmad-speak.sh');
|
|
||||||
const playTtsPath = path.join(projectDir, '.claude', 'hooks', 'play-tts.sh');
|
|
||||||
|
|
||||||
return (await fs.pathExists(hookPath)) && (await fs.pathExists(playTtsPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load existing configurations to use as defaults
|
* Load existing configurations to use as defaults
|
||||||
* @param {string} directory - Installation directory
|
* @param {string} directory - Installation directory
|
||||||
|
|
@ -1201,7 +990,6 @@ class UI {
|
||||||
hasCustomContent: false,
|
hasCustomContent: false,
|
||||||
coreConfig: {},
|
coreConfig: {},
|
||||||
ideConfig: { ides: [], skipIde: false },
|
ideConfig: { ides: [], skipIde: false },
|
||||||
agentVibesConfig: { enabled: false, alreadyInstalled: false },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -1215,10 +1003,6 @@ class UI {
|
||||||
configs.ideConfig.skipIde = false;
|
configs.ideConfig.skipIde = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load AgentVibes configuration
|
|
||||||
const agentVibesInstalled = await this.checkAgentVibesInstalled(directory);
|
|
||||||
configs.agentVibesConfig = { enabled: agentVibesInstalled, alreadyInstalled: agentVibesInstalled };
|
|
||||||
|
|
||||||
return configs;
|
return configs;
|
||||||
} catch {
|
} catch {
|
||||||
// If loading fails, return empty configs
|
// If loading fails, return empty configs
|
||||||
|
|
@ -1461,12 +1245,32 @@ class UI {
|
||||||
checked: m.checked,
|
checked: m.checked,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Add "None / I changed my mind" option at the end
|
||||||
|
const choicesWithSkip = [
|
||||||
|
...selectChoices,
|
||||||
|
{
|
||||||
|
name: '⚠ None / I changed my mind - keep no custom modules',
|
||||||
|
value: '__NONE__',
|
||||||
|
checked: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const keepModules = await prompts.multiselect({
|
const keepModules = await prompts.multiselect({
|
||||||
message: `Select custom modules to keep ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
message: `Select custom modules to keep ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`,
|
||||||
choices: selectChoices,
|
choices: choicesWithSkip,
|
||||||
required: false,
|
required: true,
|
||||||
});
|
});
|
||||||
result.selectedCustomModules = keepModules || [];
|
|
||||||
|
// If user selected both "__NONE__" and other modules, honor the "None" choice
|
||||||
|
if (keepModules && keepModules.includes('__NONE__') && keepModules.length > 1) {
|
||||||
|
console.log();
|
||||||
|
console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no custom modules will be kept.'));
|
||||||
|
console.log();
|
||||||
|
result.selectedCustomModules = [];
|
||||||
|
} else {
|
||||||
|
// Filter out the special '__NONE__' value
|
||||||
|
result.selectedCustomModules = keepModules ? keepModules.filter((m) => m !== '__NONE__') : [];
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue