From d0e80e75087e29821f4c388f932e1cbe046789f9 Mon Sep 17 00:00:00 2001 From: Sjoerd Bozon Date: Mon, 19 Jan 2026 13:11:53 +0100 Subject: [PATCH] feat(workflows): add VS Code workflow prompt recommendations - Add workflow-prompt-generator.js for dynamic prompt generation from path files - Add workflow-prompts-config.js for static workflow prompt configuration - Update github-copilot.js to use WorkflowPromptGenerator - Include model frontmatter and new-chat notes in prompts - Deep-merge prompt recommendations to preserve existing settings --- .../cli/installers/lib/ide/github-copilot.js | 63 +++- .../ide/shared/workflow-prompt-generator.js | 297 ++++++++++++++++++ .../lib/ide/shared/workflow-prompts-config.js | 115 +++++++ 3 files changed, 468 insertions(+), 7 deletions(-) create mode 100644 tools/cli/installers/lib/ide/shared/workflow-prompt-generator.js create mode 100644 tools/cli/installers/lib/ide/shared/workflow-prompts-config.js diff --git a/tools/cli/installers/lib/ide/github-copilot.js b/tools/cli/installers/lib/ide/github-copilot.js index c500a284..07dd0925 100644 --- a/tools/cli/installers/lib/ide/github-copilot.js +++ b/tools/cli/installers/lib/ide/github-copilot.js @@ -2,6 +2,7 @@ const path = require('node:path'); const { BaseIdeSetup } = require('./_base-ide'); const chalk = require('chalk'); const { AgentCommandGenerator } = require('./shared/agent-command-generator'); +const { WorkflowPromptGenerator } = require('./shared/workflow-prompt-generator'); const prompts = require('../../../lib/prompts'); /** @@ -13,6 +14,7 @@ class GitHubCopilotSetup extends BaseIdeSetup { super('github-copilot', 'GitHub Copilot', true); // preferred IDE this.configDir = '.github'; this.agentsDir = 'agents'; + this.promptsDir = 'prompts'; this.vscodeDir = '.vscode'; } @@ -90,14 +92,12 @@ class GitHubCopilotSetup extends BaseIdeSetup { async setup(projectDir, bmadDir, options = {}) { console.log(chalk.cyan(`Setting up ${this.name}...`)); - // Configure VS Code settings using pre-collected config if available - const config = options.preCollectedConfig || {}; - await this.configureVsCodeSettings(projectDir, { ...options, ...config }); - // Create .github/agents directory const githubDir = path.join(projectDir, this.configDir); const agentsDir = path.join(githubDir, this.agentsDir); + const promptsDir = path.join(githubDir, this.promptsDir); await this.ensureDir(agentsDir); + await this.ensureDir(promptsDir); // Clean up any existing BMAD files before reinstalling await this.cleanup(projectDir); @@ -113,22 +113,40 @@ class GitHubCopilotSetup extends BaseIdeSetup { const agentContent = await this.createAgentContent({ module: artifact.module, name: artifact.name }, content); // Use bmd- prefix: bmd-custom-{module}-{name}.agent.md - const targetPath = path.join(agentsDir, `bmd-custom-${artifact.module}-${artifact.name}.agent.md`); + const agentFileName = `bmd-custom-${artifact.module}-${artifact.name}`; + const targetPath = path.join(agentsDir, `${agentFileName}.agent.md`); await this.writeFile(targetPath, agentContent); agentCount++; - console.log(chalk.green(` ✓ Created agent: bmd-custom-${artifact.module}-${artifact.name}`)); + console.log(chalk.green(` ✓ Created agent: ${agentFileName}`)); } + // Generate workflow prompts from config (shared logic) + // Each prompt includes nextSteps guidance for the agent to suggest next workflows + const promptGen = new WorkflowPromptGenerator(); + const promptRecommendations = await promptGen.generatePromptFiles(promptsDir, options.selectedModules || [], { + projectDir, + bmadDir, + }); + const promptCount = Object.keys(promptRecommendations).length; + + // Configure VS Code settings using pre-collected config if available + const config = options.preCollectedConfig || {}; + await this.configureVsCodeSettings(projectDir, { ...options, ...config, promptRecommendations }); + console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${agentCount} agents created`)); + console.log(chalk.dim(` - ${promptCount} workflow prompts configured`)); console.log(chalk.dim(` - Agents directory: ${path.relative(projectDir, agentsDir)}`)); + console.log(chalk.dim(` - Prompts directory: ${path.relative(projectDir, promptsDir)}`)); console.log(chalk.dim(` - VS Code settings configured`)); console.log(chalk.dim('\n Agents available in VS Code Chat view')); + console.log(chalk.dim(' Workflow prompts show as new chat starters')); return { success: true, agents: agentCount, + prompts: promptCount, settings: true, }; } @@ -195,9 +213,22 @@ class GitHubCopilotSetup extends BaseIdeSetup { }; } - // Merge settings (existing take precedence) + // Add prompt file recommendations for new chat starters + if (options.promptRecommendations && Object.keys(options.promptRecommendations).length > 0) { + bmadSettings['chat.promptFilesRecommendations'] = options.promptRecommendations; + } + + // Merge settings (existing take precedence, except for prompt recommendations) const mergedSettings = { ...bmadSettings, ...existingSettings }; + // Deep-merge prompt recommendations (new prompts added, existing preserved) + if (options.promptRecommendations && Object.keys(options.promptRecommendations).length > 0) { + mergedSettings['chat.promptFilesRecommendations'] = { + ...existingSettings['chat.promptFilesRecommendations'], + ...options.promptRecommendations, + }; + } + // Write settings await fs.writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2)); console.log(chalk.green(' ✓ VS Code settings configured')); @@ -303,6 +334,24 @@ ${cleanContent} console.log(chalk.dim(` Cleaned up ${removed} existing BMAD agents`)); } } + + // Clean up prompts directory + const promptsDir = path.join(projectDir, this.configDir, this.promptsDir); + if (await fs.pathExists(promptsDir)) { + const files = await fs.readdir(promptsDir); + let removed = 0; + + for (const file of files) { + if (file.startsWith('bmd-') && file.endsWith('.prompt.md')) { + await fs.remove(path.join(promptsDir, file)); + removed++; + } + } + + if (removed > 0) { + console.log(chalk.dim(` Cleaned up ${removed} existing BMAD prompt files`)); + } + } } /** diff --git a/tools/cli/installers/lib/ide/shared/workflow-prompt-generator.js b/tools/cli/installers/lib/ide/shared/workflow-prompt-generator.js new file mode 100644 index 00000000..193b4e51 --- /dev/null +++ b/tools/cli/installers/lib/ide/shared/workflow-prompt-generator.js @@ -0,0 +1,297 @@ +const path = require('node:path'); +const fs = require('fs-extra'); +const yaml = require('yaml'); +const { workflowPromptsConfig } = require('./workflow-prompts-config'); + +/** + * Generate workflow prompt recommendations for IDE new chat starters + * Uses static configuration from workflow-prompts-config.js which mirrors + * the workflows documented in quick-start.md + * + * The implementation-readiness and sprint-planning workflows update + * VS Code settings to toggle which prompts are shown based on project phase. + */ +class WorkflowPromptGenerator { + /** + * Get workflow prompts for selected modules + * @param {Array} selectedModules - Modules to include (e.g., ['bmm', 'bmgd']) + * @returns {Array} Array of workflow prompt configurations + */ + getWorkflowPrompts(selectedModules = []) { + const allPrompts = []; + + // Always include core prompts + if (workflowPromptsConfig.core) { + allPrompts.push(...workflowPromptsConfig.core); + } + + // Add prompts for each selected module + for (const moduleName of selectedModules) { + if (workflowPromptsConfig[moduleName]) { + allPrompts.push(...workflowPromptsConfig[moduleName]); + } + } + + return allPrompts; + } + + /** + * Generate prompt files for an IDE + * @param {string} promptsDir - Directory to write prompt files + * @param {Array} selectedModules - Modules to include + * @returns {Object} Map of prompt names to true for VS Code settings + */ + async generatePromptFiles(promptsDir, selectedModules = [], options = {}) { + const prompts = this.getWorkflowPrompts(selectedModules); + const pathPrompts = await this.getWorkflowPathPrompts({ + bmadDir: options.bmadDir, + projectDir: options.projectDir, + selectedModules, + }); + const mergedPrompts = this.mergePrompts(prompts, pathPrompts); + const recommendations = {}; + + for (const prompt of mergedPrompts) { + const frontmatter = ['---', `agent: ${prompt.agent}`, `description: "${prompt.description}"`]; + + if (prompt.model) { + frontmatter.push(`model: ${prompt.model}`); + } + + const promptContent = [...frontmatter, '---', '', prompt.prompt, ''].join('\n'); + + const promptFilePath = path.join(promptsDir, `bmd-${prompt.name}.prompt.md`); + await fs.writeFile(promptFilePath, promptContent); + recommendations[`bmd-${prompt.name}`] = true; + } + + return recommendations; + } + + mergePrompts(staticPrompts, dynamicPrompts) { + const merged = []; + const seen = new Set(); + + for (const prompt of [...staticPrompts, ...dynamicPrompts]) { + if (seen.has(prompt.name)) { + continue; + } + seen.add(prompt.name); + merged.push(prompt); + } + + return merged; + } + + async getWorkflowPathPrompts({ bmadDir, projectDir, selectedModules = [] }) { + if (!bmadDir) { + return []; + } + + const prompts = []; + const promptKeys = new Map(); + + for (const moduleName of selectedModules) { + const pathFilesDir = await this.resolvePathFilesDir({ moduleName, bmadDir, projectDir }); + if (!pathFilesDir) { + continue; + } + + let pathFiles = []; + try { + pathFiles = await fs.readdir(pathFilesDir); + } catch { + continue; + } + + const yamlFiles = pathFiles.filter((file) => file.endsWith('.yaml') || file.endsWith('.yml')); + + for (const file of yamlFiles) { + const filePath = path.join(pathFilesDir, file); + let pathData; + + try { + pathData = yaml.parse(await fs.readFile(filePath, 'utf8')); + } catch { + continue; + } + + const phases = Array.isArray(pathData?.phases) ? pathData.phases : []; + let lastAgent = null; + + for (const phase of phases) { + const phaseName = phase?.name || ''; + const workflows = Array.isArray(phase?.workflows) ? phase.workflows : []; + + for (const workflow of workflows) { + const agentKey = workflow?.agent || ''; + const command = workflow?.command || ''; + const workflowId = workflow?.id || ''; + + if (!command && !workflowId) { + continue; + } + + const promptName = this.buildPromptName({ moduleName, workflowId, command }); + const prompt = this.buildPromptText({ moduleName, workflowId, command }); + const agent = this.buildAgentName({ moduleName, agentKey }); + const requiresNewChat = !!lastAgent && !!agentKey && agentKey !== lastAgent; + const description = this.buildPromptDescription({ + workflow, + promptName, + requiresNewChat, + }); + const model = this.resolveModel({ phaseName, workflow, agentKey }); + + lastAgent = agentKey || lastAgent; + + const promptKey = `${moduleName}:${promptName}`; + const existing = promptKeys.get(promptKey); + + if (!existing) { + const promptEntry = { + name: promptName, + agent, + description, + model, + prompt, + }; + + promptKeys.set(promptKey, promptEntry); + prompts.push(promptEntry); + } else if (requiresNewChat && !existing.description.includes('Ctrl+Shift+Enter')) { + existing.description = this.appendNewChatHint(existing.description); + } + } + } + } + } + + return prompts; + } + + async resolvePathFilesDir({ moduleName, bmadDir, projectDir }) { + const workflowStatusPath = path.join(bmadDir, moduleName, 'workflows', 'workflow-status', 'workflow.yaml'); + + if (!(await fs.pathExists(workflowStatusPath))) { + return null; + } + + let workflowStatus; + try { + workflowStatus = yaml.parse(await fs.readFile(workflowStatusPath, 'utf8')); + } catch { + return null; + } + + const pathFilesRaw = workflowStatus?.path_files; + if (!pathFilesRaw || typeof pathFilesRaw !== 'string') { + return null; + } + + const workflowDir = path.dirname(workflowStatusPath).replaceAll('\\', '/'); + let resolved = pathFilesRaw.replace('{installed_path}', workflowDir); + + if (projectDir) { + const normalizedProjectDir = projectDir.replaceAll('\\', '/'); + resolved = resolved.replace('{project-root}', normalizedProjectDir); + + const relativeBmadDir = path.relative(projectDir, bmadDir).replaceAll('\\', '/'); + if (relativeBmadDir && relativeBmadDir !== '_bmad') { + resolved = resolved.replace('/_bmad/', `/${relativeBmadDir}/`); + } + } + + resolved = resolved.replaceAll('\\', '/'); + + return path.resolve(resolved); + } + + buildPromptName({ moduleName, workflowId, command }) { + let baseName = workflowId || command || 'workflow'; + + if (command && command.startsWith('/')) { + baseName = command.split(':').pop() || baseName; + } else if (command) { + baseName = command; + } + + baseName = baseName.replace(/^\/+/, ''); + + return `${moduleName}-${baseName}`; + } + + buildPromptText({ moduleName, workflowId, command }) { + if (command) { + if (command.startsWith('/')) { + return command; + } + + return `/bmad:${moduleName}:workflows:${command}`; + } + + if (workflowId) { + return `/bmad:${moduleName}:workflows:${workflowId}`; + } + + return '*workflow-status'; + } + + buildAgentName({ moduleName, agentKey }) { + if (!agentKey) { + return `bmd-custom-${moduleName}-pm`; + } + + return `bmd-custom-${moduleName}-${agentKey}`; + } + + buildPromptDescription({ workflow, promptName, requiresNewChat }) { + const title = this.toTitle(promptName.replace(/^[^-]+-/, '')); + const detail = workflow?.note || workflow?.output || workflow?.description || ''; + const baseDescription = detail ? `${title} — ${detail}` : title; + + return requiresNewChat ? this.appendNewChatHint(baseDescription) : baseDescription; + } + + appendNewChatHint(description) { + return `${description} (open a new chat with Ctrl+Shift+Enter)`; + } + + resolveModel({ phaseName, workflow, agentKey }) { + const phase = (phaseName || '').toLowerCase(); + const id = (workflow?.id || '').toLowerCase(); + const command = (workflow?.command || '').toLowerCase(); + const agent = (agentKey || '').toLowerCase(); + + if (id.includes('code-review') || command.includes('code-review') || agent.endsWith('dev') || agent.includes('game-dev')) { + return 'gpt-5.2-codex'; + } + + if (id.includes('dev-story') || command.includes('dev-story') || id.includes('implement') || command.includes('implement')) { + return 'gpt-5.2-codex'; + } + + if (id.includes('sprint-planning') || command.includes('sprint-planning')) { + return 'claude-opus-4.5'; + } + + if ( + phase.includes('analysis') || + phase.includes('planning') || + phase.includes('solution') || + phase.includes('design') || + phase.includes('technical') || + phase.includes('pre-production') + ) { + return 'claude-opus-4.5'; + } + + return 'gpt-5.2'; + } + + toTitle(value) { + return value.replaceAll(/[-_]/g, ' ').replaceAll(/\b\w/g, (match) => match.toUpperCase()); + } +} + +module.exports = { WorkflowPromptGenerator }; diff --git a/tools/cli/installers/lib/ide/shared/workflow-prompts-config.js b/tools/cli/installers/lib/ide/shared/workflow-prompts-config.js new file mode 100644 index 00000000..ffe472e8 --- /dev/null +++ b/tools/cli/installers/lib/ide/shared/workflow-prompts-config.js @@ -0,0 +1,115 @@ +/** + * Workflow prompt configuration for IDE new chat starters + * + * This configuration defines the workflow prompts that appear as suggestions + * when starting a new chat in VS Code (via chat.promptFilesRecommendations). + * + * The implementation-readiness and sprint-planning workflows update the + * VS Code settings to toggle which prompts are shown based on project phase. + * + * Reference: docs/modules/bmm-bmad-method/quick-start.md + */ + +const workflowPromptsConfig = { + // BMad Method Module (bmm) - Static prompts not covered by path files + bmm: [ + { + name: 'workflow-init', + agent: 'bmd-custom-bmm-analyst', + shortcut: 'WI', + description: '[WI] Initialize workflow and choose planning track', + model: 'claude-opus-4.5', + prompt: '*workflow-init', + }, + { + name: 'workflow-status', + agent: 'bmd-custom-bmm-pm', + shortcut: 'WS', + description: '[WS] Check current workflow status and next steps (open a new chat with Ctrl+Shift+Enter)', + model: 'claude-opus-4.5', + prompt: '*workflow-status', + }, + { + name: 'create-story', + agent: 'bmd-custom-bmm-sm', + shortcut: 'CS', + description: '[CS] Create developer-ready story from epic', + model: 'gpt-5.2-codex', + prompt: '*create-story', + }, + { + name: 'dev-story', + agent: 'bmd-custom-bmm-dev', + shortcut: 'DS', + description: '[DS] Implement the current story (open a new chat with Ctrl+Shift+Enter)', + model: 'gpt-5.2-codex', + prompt: '*dev-story', + }, + { + name: 'code-review', + agent: 'bmd-custom-bmm-dev', + shortcut: 'CR', + description: '[CR] Perform code review on implementation', + model: 'gpt-5.2-codex', + prompt: '*code-review', + }, + { + name: 'retrospective', + agent: 'bmd-custom-bmm-sm', + shortcut: 'ER', + description: '[ER] Run epic retrospective after completion (open a new chat with Ctrl+Shift+Enter)', + model: 'claude-opus-4.5', + prompt: '*epic-retrospective', + }, + { + name: 'correct-course', + agent: 'bmd-custom-bmm-sm', + shortcut: 'CC', + description: '[CC] Course correction when things go off track', + model: 'claude-opus-4.5', + prompt: '*correct-course', + }, + ], + + // BMad Game Development Module (bmgd) - Static prompts not covered by path files + bmgd: [ + { + name: 'game-implement', + agent: 'bmd-custom-bmgd-game-dev', + shortcut: 'GI', + description: '[GI] Implement game feature', + model: 'gpt-5.2-codex', + prompt: '*game-implement', + }, + { + name: 'game-qa', + agent: 'bmd-custom-bmgd-game-qa', + shortcut: 'GQ', + description: '[GQ] Test and QA game feature (open a new chat with Ctrl+Shift+Enter)', + model: 'gpt-5.2', + prompt: '*game-qa', + }, + ], + + // Core agents (always available) + core: [ + { + name: 'list-tasks', + agent: 'bmd-custom-core-bmad-master', + shortcut: 'LT', + description: '[LT] List available tasks', + model: 'gpt-5-mini', + prompt: '*list-tasks', + }, + { + name: 'list-workflows', + agent: 'bmd-custom-core-bmad-master', + shortcut: 'LW', + description: '[LW] List available workflows', + model: 'gpt-5-mini', + prompt: '*list-workflows', + }, + ], +}; + +module.exports = { workflowPromptsConfig };