diff --git a/tools/installer/lib/ide-setup.js b/tools/installer/lib/ide-setup.js index d80681dd..9e19c23a 100644 --- a/tools/installer/lib/ide-setup.js +++ b/tools/installer/lib/ide-setup.js @@ -523,6 +523,158 @@ class IdeSetup extends BaseIdeSetup { return { configObj, summary }; }; + // Helper: generate AGENTS.md section for OpenCode (acts as system prompt memory) + const generateOpenCodeAgentsMd = async () => { + try { + const filePath = path.join(installDir, 'AGENTS.md'); + const startMarker = ''; + const endMarker = ''; + + const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); + const tasks = await this.getAllTaskIds(installDir); + + let section = ''; + section += `${startMarker}\n`; + section += `# BMAD-METHOD Agents and Tasks (OpenCode)\n\n`; + section += `OpenCode reads AGENTS.md during initialization and uses it as part of its system prompt for the session. This section is auto-generated by BMAD-METHOD for OpenCode.\n\n`; + section += `## How To Use With OpenCode\n\n`; + section += `- Run \`opencode\` in this project. OpenCode will read \`AGENTS.md\` and your OpenCode config (opencode.json[c]).\n`; + section += `- Reference a role naturally, e.g., "As dev, implement ..." or use commands defined in your BMAD tasks.\n`; + section += `- Commit \`.bmad-core\` and \`AGENTS.md\` if you want teammates to share the same configuration.\n`; + section += `- Refresh this section after BMAD updates: \`npx bmad-method install -f -i opencode\`.\n\n`; + + section += `### Helpful Commands\n\n`; + section += `- List agents: \`npx bmad-method list:agents\`\n`; + section += `- Reinstall BMAD core and regenerate this section: \`npx bmad-method install -f -i opencode\`\n`; + section += `- Validate configuration: \`npx bmad-method validate\`\n\n`; + + // Brief context note for modes and tools + section += `Note\n`; + section += `- Orchestrators run as mode: primary; other agents as subagent.\n`; + section += `- All agents have tools enabled: write, edit, bash.\n\n`; + + section += `## Agents\n\n`; + section += `### Directory\n\n`; + section += `| Title | ID | When To Use |\n|---|---|---|\n`; + + // Fallback descriptions for core agents (used if whenToUse is missing) + const fallbackDescriptions = { + 'ux-expert': + 'Use for UI/UX design, wireframes, prototypes, front-end specs, and user experience optimization', + sm: 'Use for story creation, epic management, retrospectives in party-mode, and agile process guidance', + qa: 'Ensure quality strategy, test design, risk profiling, and QA gates across features', + po: 'Backlog management, story refinement, acceptance criteria, sprint planning, prioritization decisions', + pm: 'PRDs, product strategy, feature prioritization, roadmap planning, and stakeholder communication', + dev: 'Code implementation, debugging, refactoring, and development best practices', + 'bmad-orchestrator': + 'Workflow coordination, multi-agent tasks, role switching guidance, and when unsure which specialist to consult', + 'bmad-master': + 'Comprehensive cross-domain execution for tasks that do not require a specific persona', + architect: + 'System design, architecture docs, technology selection, API design, and infrastructure planning', + analyst: + 'Discovery/research, competitive analysis, project briefs, initial discovery, and brownfield documentation', + }; + + const sanitizeDesc = (s) => { + if (!s) return ''; + let t = String(s).trim(); + // Drop surrounding single/double/backtick quotes + t = t.replaceAll(/^['"`]+|['"`]+$/g, ''); + // Collapse whitespace + t = t.replaceAll(/\s+/g, ' ').trim(); + return t; + }; + const agentSummaries = []; + for (const agentId of agents) { + const agentPath = await this.findAgentPath(agentId, installDir); + if (!agentPath) continue; + let whenToUse = ''; + try { + const raw = await fileManager.readFile(agentPath); + const yamlMatch = raw.match(/```ya?ml\r?\n([\s\S]*?)```/); + const yamlBlock = yamlMatch ? yamlMatch[1].trim() : null; + if (yamlBlock) { + try { + const data = yaml.load(yamlBlock); + if (data && typeof data.whenToUse === 'string') { + whenToUse = data.whenToUse; + } + } catch { + // ignore YAML parse errors + } + if (!whenToUse) { + // Fallback regex supporting single or double quotes + const m1 = yamlBlock.match(/whenToUse:\s*"([^\n"]+)"/i); + const m2 = yamlBlock.match(/whenToUse:\s*'([^\n']+)'/i); + const m3 = yamlBlock.match(/whenToUse:\s*([^\n\r]+)/i); + whenToUse = (m1?.[1] || m2?.[1] || m3?.[1] || '').trim(); + } + } + } catch { + // ignore read/parse errors for agent metadata extraction + } + const title = await this.getAgentTitle(agentId, installDir); + const finalDesc = sanitizeDesc(whenToUse) || fallbackDescriptions[agentId] || '—'; + agentSummaries.push({ agentId, title, whenToUse: finalDesc, path: agentPath }); + // Strict 3-column row + section += `| ${title} | ${agentId} | ${finalDesc} |\n`; + } + section += `\n`; + + for (const { agentId, title, whenToUse, path: agentPath } of agentSummaries) { + const relativePath = path.relative(installDir, agentPath).replaceAll('\\', '/'); + section += `### ${title} (id: ${agentId})\n`; + section += `Source: [${relativePath}](${relativePath})\n\n`; + if (whenToUse) section += `- When to use: ${whenToUse}\n`; + section += `- How to activate: Mention "As ${agentId}, ..." to get role-aligned behavior\n`; + section += `- Full definition: open the source file above (content not embedded)\n\n`; + } + + if (tasks && tasks.length > 0) { + section += `## Tasks\n\n`; + section += `These are reusable task briefs; use the paths to open them as needed.\n\n`; + for (const taskId of tasks) { + const taskPath = await this.findTaskPath(taskId, installDir); + if (!taskPath) continue; + const relativePath = path.relative(installDir, taskPath).replaceAll('\\', '/'); + section += `### Task: ${taskId}\n`; + section += `Source: [${relativePath}](${relativePath})\n`; + section += `- How to use: Reference the task in your prompt or execute via your configured commands.\n`; + section += `- Full brief: open the source file above (content not embedded)\n\n`; + } + } + + section += `${endMarker}\n`; + + let finalContent = ''; + if (await fileManager.pathExists(filePath)) { + const existing = await fileManager.readFile(filePath); + if (existing.includes(startMarker) && existing.includes(endMarker)) { + const pattern = String.raw`${startMarker}[\s\S]*?${endMarker}`; + const replaced = existing.replace(new RegExp(pattern, 'm'), section); + finalContent = replaced; + } else { + finalContent = existing.trimEnd() + `\n\n` + section; + } + } else { + finalContent += '# Project Agents\n\n'; + finalContent += 'This file provides guidance and memory for your coding CLI.\n\n'; + finalContent += section; + } + + await fileManager.writeFile(filePath, finalContent); + console.log(chalk.green('✓ Created/updated AGENTS.md for OpenCode CLI integration')); + console.log( + chalk.dim( + 'OpenCode reads AGENTS.md automatically on init. Run `opencode` in this project to use BMAD agents.', + ), + ); + } catch { + console.log(chalk.yellow('⚠︎ Skipped creating AGENTS.md for OpenCode (write failed)')); + } + }; + if (hasJson || hasJsonc) { // Preserve existing top-level fields; only touch instructions const targetPath = hasJsonc ? jsoncPath : jsonPath; @@ -545,6 +697,8 @@ class IdeSetup extends BaseIdeSetup { ` File: ${path.basename(targetPath)} | Agents +${summary.agentsAdded} ~${summary.agentsUpdated} ⨯${summary.agentsSkipped} | Commands +${summary.commandsAdded} ~${summary.commandsUpdated} ⨯${summary.commandsSkipped}`, ), ); + // Ensure AGENTS.md is created/updated for OpenCode as well + await generateOpenCodeAgentsMd(); } catch (error) { console.log(chalk.red('✗ Failed to update existing OpenCode config'), error.message); return false; @@ -571,6 +725,8 @@ class IdeSetup extends BaseIdeSetup { ` File: opencode.jsonc | Agents +${summary.agentsAdded} | Commands +${summary.commandsAdded}`, ), ); + // Also create/update AGENTS.md for OpenCode on new-config path + await generateOpenCodeAgentsMd(); return true; } catch (error) { console.log(chalk.red('✗ Failed to create opencode.jsonc'), error.message); @@ -715,7 +871,6 @@ class IdeSetup extends BaseIdeSetup { if (options.webEnabled) { if (exists) { let gi = await fileManager.readFile(gitignorePath); - // Remove lines that ignore BMAD dot-folders const updated = gi .split(/\r?\n/) .filter((l) => !/^\s*\.bmad-core\/?\s*$/.test(l) && !/^\s*\.bmad-\*\/?\s*$/.test(l))