Merge branch 'main' into fix/issue-1030-remove-validate-prd

This commit is contained in:
Alex Verkhovsky 2025-12-05 17:35:12 -08:00 committed by GitHub
commit 0b6adf8fc3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 182 additions and 17 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -482,10 +482,39 @@ function compileAgent(yamlContent, answers = {}, agentName = '', targetPath = ''
}; };
} }
/**
* Process TTS injection markers in content
* @param {string} content - Content to process
* @param {boolean} enableAgentVibes - Whether AgentVibes is enabled
* @returns {Object} { content: string, hadInjection: boolean }
*/
function processTTSInjectionPoints(content, enableAgentVibes) {
const hasAgentTTS = content.includes('<!-- TTS_INJECTION:agent-tts -->');
if (enableAgentVibes && hasAgentTTS) {
// Replace agent-tts injection marker with TTS rule
content = content.replaceAll(
'<!-- TTS_INJECTION:agent-tts -->',
`- When responding to user messages, speak your responses using TTS:
Call: \`.claude/hooks/bmad-speak.sh '{agent-id}' '{response-text}'\` after each response
Replace {agent-id} with YOUR agent ID from <agent id="..."> tag at top of this file
Replace {response-text} with the text you just output to the user
IMPORTANT: Use single quotes as shown - do NOT escape special characters like ! or $ inside single quotes
Run in background (&) to avoid blocking`,
);
return { content, hadInjection: true };
} else if (!enableAgentVibes && hasAgentTTS) {
// Strip injection markers when disabled
content = content.replaceAll(/<!-- TTS_INJECTION:agent-tts -->\n?/g, '');
}
return { content, hadInjection: false };
}
/** /**
* Compile agent file to .md * Compile agent file to .md
* @param {string} yamlPath - Path to agent YAML file * @param {string} yamlPath - Path to agent YAML file
* @param {Object} options - { answers: {}, outputPath: string } * @param {Object} options - { answers: {}, outputPath: string, enableAgentVibes: boolean }
* @returns {Object} Compilation result * @returns {Object} Compilation result
*/ */
function compileAgentFile(yamlPath, options = {}) { function compileAgentFile(yamlPath, options = {}) {
@ -501,13 +530,24 @@ function compileAgentFile(yamlPath, options = {}) {
outputPath = path.join(dir, `${basename}.md`); outputPath = path.join(dir, `${basename}.md`);
} }
// Process TTS injection points if enableAgentVibes option is provided
let xml = result.xml;
let ttsInjected = false;
if (options.enableAgentVibes !== undefined) {
const ttsResult = processTTSInjectionPoints(xml, options.enableAgentVibes);
xml = ttsResult.content;
ttsInjected = ttsResult.hadInjection;
}
// Write compiled XML // Write compiled XML
fs.writeFileSync(outputPath, result.xml, 'utf8'); fs.writeFileSync(outputPath, xml, 'utf8');
return { return {
...result, ...result,
xml,
outputPath, outputPath,
sourcePath: yamlPath, sourcePath: yamlPath,
ttsInjected,
}; };
} }

View File

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