From 25793c33d74c66cce4e21481de4d14aadd99fd13 Mon Sep 17 00:00:00 2001 From: Sjoerd Bozon Date: Mon, 19 Jan 2026 11:16:21 +0100 Subject: [PATCH] feat: generate workflow prompts from path files --- .../cli/installers/lib/ide/github-copilot.js | 5 +- .../ide/shared/workflow-prompt-generator.js | 236 +++++++++++++++++- .../lib/ide/shared/workflow-prompts-config.js | 102 +------- 3 files changed, 240 insertions(+), 103 deletions(-) diff --git a/tools/cli/installers/lib/ide/github-copilot.js b/tools/cli/installers/lib/ide/github-copilot.js index 3621f5ce..fb03e764 100644 --- a/tools/cli/installers/lib/ide/github-copilot.js +++ b/tools/cli/installers/lib/ide/github-copilot.js @@ -128,7 +128,10 @@ class GitHubCopilotSetup extends BaseIdeSetup { // 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 || []); + 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 diff --git a/tools/cli/installers/lib/ide/shared/workflow-prompt-generator.js b/tools/cli/installers/lib/ide/shared/workflow-prompt-generator.js index 79edc0e3..193b4e51 100644 --- a/tools/cli/installers/lib/ide/shared/workflow-prompt-generator.js +++ b/tools/cli/installers/lib/ide/shared/workflow-prompt-generator.js @@ -1,5 +1,6 @@ const path = require('node:path'); const fs = require('fs-extra'); +const yaml = require('yaml'); const { workflowPromptsConfig } = require('./workflow-prompts-config'); /** @@ -40,11 +41,17 @@ class WorkflowPromptGenerator { * @param {Array} selectedModules - Modules to include * @returns {Object} Map of prompt names to true for VS Code settings */ - async generatePromptFiles(promptsDir, selectedModules = []) { + 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 prompts) { + for (const prompt of mergedPrompts) { const frontmatter = ['---', `agent: ${prompt.agent}`, `description: "${prompt.description}"`]; if (prompt.model) { @@ -60,6 +67,231 @@ class WorkflowPromptGenerator { 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 index 767a9ce1..ffe472e8 100644 --- a/tools/cli/installers/lib/ide/shared/workflow-prompts-config.js +++ b/tools/cli/installers/lib/ide/shared/workflow-prompts-config.js @@ -11,11 +11,8 @@ */ const workflowPromptsConfig = { - // BMad Method Module (bmm) - Standard development workflow + // BMad Method Module (bmm) - Static prompts not covered by path files bmm: [ - // ═══════════════════════════════════════════════════════════════════════ - // Phase 1 - Analysis (Optional) - // ═══════════════════════════════════════════════════════════════════════ { name: 'workflow-init', agent: 'bmd-custom-bmm-analyst', @@ -24,14 +21,6 @@ const workflowPromptsConfig = { model: 'claude-opus-4.5', prompt: '*workflow-init', }, - { - name: 'brainstorm', - agent: 'bmd-custom-bmm-analyst', - shortcut: 'BP', - description: '[BP] Brainstorm project ideas and concepts', - model: 'claude-opus-4.5', - prompt: '*brainstorm-project', - }, { name: 'workflow-status', agent: 'bmd-custom-bmm-pm', @@ -40,67 +29,6 @@ const workflowPromptsConfig = { model: 'claude-opus-4.5', prompt: '*workflow-status', }, - - // ═══════════════════════════════════════════════════════════════════════ - // Phase 2 - Planning (Required) - // ═══════════════════════════════════════════════════════════════════════ - { - name: 'prd', - agent: 'bmd-custom-bmm-pm', - shortcut: 'PD', - description: '[PD] Create Product Requirements Document (PRD)', - model: 'claude-opus-4.5', - prompt: '*prd', - }, - { - name: 'ux-design', - agent: 'bmd-custom-bmm-ux-designer', - shortcut: 'UD', - description: '[UD] Create UX Design specification (open a new chat with Ctrl+Shift+Enter)', - model: 'claude-opus-4.5', - prompt: '*ux-design', - }, - - // ═══════════════════════════════════════════════════════════════════════ - // Phase 3 - Solutioning - // ═══════════════════════════════════════════════════════════════════════ - { - name: 'create-architecture', - agent: 'bmd-custom-bmm-architect', - shortcut: 'CA', - description: '[CA] Create system architecture document (open a new chat with Ctrl+Shift+Enter)', - model: 'claude-opus-4.5', - prompt: '*create-architecture', - }, - { - name: 'epics-stories', - agent: 'bmd-custom-bmm-pm', - shortcut: 'ES', - description: '[ES] Create Epics and User Stories from PRD (open a new chat with Ctrl+Shift+Enter)', - model: 'claude-opus-4.5', - prompt: '*epics-stories', - }, - { - name: 'implementation-readiness', - agent: 'bmd-custom-bmm-architect', - shortcut: 'IR', - description: '[IR] Check implementation readiness across all docs (open a new chat with Ctrl+Shift+Enter)', - model: 'claude-opus-4.5', - prompt: '*implementation-readiness', - }, - { - name: 'sprint-planning', - agent: 'bmd-custom-bmm-sm', - shortcut: 'SP', - description: '[SP] Initialize sprint planning from epics (open a new chat with Ctrl+Shift+Enter)', - model: 'claude-opus-4.5', - prompt: '*sprint-planning', - }, - - // ═══════════════════════════════════════════════════════════════════════ - // Phase 4 - Implementation: The "Keep Going" Cycle - // SM → create-story → DEV → dev-story → code-review → (create-story | retrospective) - // ═══════════════════════════════════════════════════════════════════════ { name: 'create-story', agent: 'bmd-custom-bmm-sm', @@ -143,9 +71,8 @@ const workflowPromptsConfig = { }, ], - // BMad Game Development Module (bmgd) + // BMad Game Development Module (bmgd) - Static prompts not covered by path files bmgd: [ - // Implementation cycle { name: 'game-implement', agent: 'bmd-custom-bmgd-game-dev', @@ -162,31 +89,6 @@ const workflowPromptsConfig = { model: 'gpt-5.2', prompt: '*game-qa', }, - // Planning & Design - { - name: 'game-design', - agent: 'bmd-custom-bmgd-game-designer', - shortcut: 'GD', - description: '[GD] Design game mechanics and systems (open a new chat with Ctrl+Shift+Enter)', - model: 'claude-opus-4.5', - prompt: '*game-design', - }, - { - name: 'game-architecture', - agent: 'bmd-custom-bmgd-game-architect', - shortcut: 'GA', - description: '[GA] Create game technical architecture (open a new chat with Ctrl+Shift+Enter)', - model: 'claude-opus-4.5', - prompt: '*game-architecture', - }, - { - name: 'game-sprint', - agent: 'bmd-custom-bmgd-game-scrum-master', - shortcut: 'GS', - description: '[GS] Plan game development sprint (open a new chat with Ctrl+Shift+Enter)', - model: 'claude-opus-4.5', - prompt: '*game-sprint', - }, ], // Core agents (always available)