bmad-method 6.0.0-alpha.10 → 6.0.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +219 -1105
  2. package/README.md +129 -359
  3. package/docs/custom-agent-installation.md +169 -0
  4. package/{v6-open-items.md → docs/v6-open-items.md} +1 -1
  5. package/package.json +4 -2
  6. package/src/core/resources/excalidraw/README.md +160 -0
  7. package/src/core/resources/excalidraw/library-loader.md +50 -0
  8. package/src/modules/bmb/docs/agent-compilation.md +340 -0
  9. package/src/modules/bmb/docs/agent-menu-patterns.md +524 -0
  10. package/src/modules/bmb/docs/expert-agent-architecture.md +364 -0
  11. package/src/modules/bmb/docs/index.md +55 -0
  12. package/src/modules/bmb/docs/module-agent-architecture.md +367 -0
  13. package/src/modules/bmb/docs/simple-agent-architecture.md +288 -0
  14. package/src/modules/bmb/docs/understanding-agent-types.md +184 -0
  15. package/src/modules/bmb/reference/agents/expert-examples/journal-keeper/README.md +242 -0
  16. package/src/modules/bmb/reference/agents/expert-examples/journal-keeper/journal-keeper-sidecar/breakthroughs.md +24 -0
  17. package/src/modules/bmb/reference/agents/expert-examples/journal-keeper/journal-keeper-sidecar/instructions.md +108 -0
  18. package/src/modules/bmb/reference/agents/expert-examples/journal-keeper/journal-keeper-sidecar/memories.md +46 -0
  19. package/src/modules/bmb/reference/agents/expert-examples/journal-keeper/journal-keeper-sidecar/mood-patterns.md +39 -0
  20. package/src/modules/bmb/reference/agents/expert-examples/journal-keeper/journal-keeper.agent.yaml +152 -0
  21. package/src/modules/bmb/reference/agents/module-examples/README.md +50 -0
  22. package/src/modules/bmb/reference/agents/module-examples/security-engineer.agent.yaml +53 -0
  23. package/src/modules/bmb/reference/agents/module-examples/trend-analyst.agent.yaml +57 -0
  24. package/src/modules/bmb/reference/agents/simple-examples/README.md +223 -0
  25. package/src/modules/bmb/reference/agents/simple-examples/commit-poet.agent.yaml +126 -0
  26. package/src/modules/bmb/reference/readme.md +3 -0
  27. package/src/modules/bmb/workflows/create-agent/agent-validation-checklist.md +174 -0
  28. package/src/modules/bmb/workflows/create-agent/brainstorm-context.md +99 -120
  29. package/src/modules/bmb/workflows/create-agent/communication-presets.csv +61 -0
  30. package/src/modules/bmb/workflows/create-agent/instructions.md +126 -65
  31. package/src/modules/bmb/workflows/create-agent/workflow.yaml +19 -12
  32. package/src/modules/bmb/workflows/edit-agent/README.md +174 -47
  33. package/src/modules/bmb/workflows/edit-agent/instructions.md +397 -33
  34. package/src/modules/bmb/workflows/edit-agent/workflow.yaml +24 -8
  35. package/src/modules/bmgd/workflows/4-production/story-context/workflow.yaml +1 -1
  36. package/src/modules/bmm/agents/analyst.agent.yaml +2 -2
  37. package/src/modules/bmm/agents/architect.agent.yaml +10 -2
  38. package/src/modules/bmm/agents/dev.agent.yaml +2 -2
  39. package/src/modules/bmm/agents/pm.agent.yaml +7 -3
  40. package/src/modules/bmm/agents/sm.agent.yaml +2 -2
  41. package/src/modules/bmm/agents/tea.agent.yaml +2 -2
  42. package/src/modules/bmm/agents/tech-writer.agent.yaml +15 -3
  43. package/src/modules/bmm/agents/ux-designer.agent.yaml +6 -2
  44. package/src/modules/bmm/docs/README.md +4 -0
  45. package/src/modules/bmm/docs/images/workflow-method-greenfield.excalidraw +5919 -0
  46. package/src/modules/bmm/docs/images/workflow-method-greenfield.svg +2 -0
  47. package/src/modules/bmm/docs/quick-start.md +6 -0
  48. package/src/modules/bmm/docs/scale-adaptive-system.md +6 -0
  49. package/src/modules/bmm/docs/workflows-implementation.md +10 -0
  50. package/src/modules/bmm/workflows/2-plan-workflows/prd/workflow.yaml +4 -4
  51. package/src/modules/bmm/workflows/{2-plan-workflows → 3-solutioning}/create-epics-and-stories/workflow.yaml +5 -5
  52. package/src/modules/bmm/workflows/4-implementation/story-context/workflow.yaml +1 -1
  53. package/src/modules/bmm/workflows/{frame-expert → diagrams}/create-dataflow/instructions.md +7 -8
  54. package/src/modules/bmm/workflows/diagrams/create-dataflow/workflow.yaml +27 -0
  55. package/src/modules/bmm/workflows/{frame-expert → diagrams}/create-diagram/instructions.md +9 -10
  56. package/src/modules/bmm/workflows/diagrams/create-diagram/workflow.yaml +27 -0
  57. package/src/modules/bmm/workflows/{frame-expert → diagrams}/create-flowchart/instructions.md +4 -5
  58. package/src/modules/bmm/workflows/diagrams/create-flowchart/workflow.yaml +27 -0
  59. package/src/modules/bmm/workflows/{frame-expert → diagrams}/create-wireframe/instructions.md +3 -3
  60. package/src/modules/bmm/workflows/diagrams/create-wireframe/workflow.yaml +27 -0
  61. package/src/modules/bmm/workflows/workflow-status/paths/enterprise-brownfield.yaml +18 -30
  62. package/src/modules/bmm/workflows/workflow-status/paths/enterprise-greenfield.yaml +2 -14
  63. package/src/modules/bmm/workflows/workflow-status/paths/method-brownfield.yaml +2 -14
  64. package/src/modules/bmm/workflows/workflow-status/paths/method-greenfield.yaml +2 -14
  65. package/src/modules/cis/agents/presentation-master.agent.yaml +60 -0
  66. package/tools/cli/commands/agent-install.js +409 -0
  67. package/tools/cli/installers/lib/core/installer.js +119 -0
  68. package/tools/cli/installers/lib/ide/_base-ide.js +25 -0
  69. package/tools/cli/installers/lib/ide/antigravity.js +463 -0
  70. package/tools/cli/installers/lib/ide/claude-code.js +43 -0
  71. package/tools/cli/installers/lib/ide/codex.js +217 -32
  72. package/tools/cli/installers/lib/ide/cursor.js +48 -0
  73. package/tools/cli/installers/lib/ide/github-copilot.js +74 -0
  74. package/tools/cli/installers/lib/ide/manager.js +35 -0
  75. package/tools/cli/installers/lib/ide/opencode.js +45 -0
  76. package/tools/cli/installers/lib/ide/windsurf.js +47 -0
  77. package/tools/cli/lib/agent/compiler.js +390 -0
  78. package/tools/cli/lib/agent/installer.js +725 -0
  79. package/tools/cli/lib/agent/template-engine.js +152 -0
  80. package/docs/installers-bundlers/web-bundler-usage.md +0 -54
  81. package/src/modules/bmb/workflows/create-agent/README.md +0 -203
  82. package/src/modules/bmb/workflows/create-agent/agent-architecture.md +0 -415
  83. package/src/modules/bmb/workflows/create-agent/agent-command-patterns.md +0 -759
  84. package/src/modules/bmb/workflows/create-agent/agent-types.md +0 -292
  85. package/src/modules/bmb/workflows/create-agent/checklist.md +0 -62
  86. package/src/modules/bmb/workflows/create-agent/communication-styles.md +0 -202
  87. package/src/modules/bmb/workflows/edit-agent/checklist.md +0 -112
  88. package/src/modules/bmb/workflows/redoc/README.md +0 -87
  89. package/src/modules/bmb/workflows/redoc/checklist.md +0 -99
  90. package/src/modules/bmb/workflows/redoc/instructions.md +0 -265
  91. package/src/modules/bmb/workflows/redoc/workflow.yaml +0 -34
  92. package/src/modules/bmm/agents/frame-expert.agent.yaml +0 -42
  93. package/src/modules/bmm/workflows/frame-expert/create-dataflow/workflow.yaml +0 -24
  94. package/src/modules/bmm/workflows/frame-expert/create-diagram/workflow.yaml +0 -25
  95. package/src/modules/bmm/workflows/frame-expert/create-flowchart/workflow.yaml +0 -28
  96. package/src/modules/bmm/workflows/frame-expert/create-wireframe/workflow.yaml +0 -24
  97. package/src/modules/bmm/workflows/workflow-status/paths/game-design.yaml +0 -52
  98. /package/src/{modules/bmm/workflows/frame-expert/_shared → core/resources/excalidraw}/excalidraw-helpers.md +0 -0
  99. /package/src/{modules/bmm/workflows/frame-expert/_shared → core/resources/excalidraw}/validate-json-instructions.md +0 -0
  100. /package/src/modules/bmm/workflows/{2-plan-workflows → 3-solutioning}/create-epics-and-stories/epics-template.md +0 -0
  101. /package/src/modules/bmm/workflows/{2-plan-workflows → 3-solutioning}/create-epics-and-stories/instructions.md +0 -0
  102. /package/src/modules/bmm/workflows/{frame-expert → diagrams}/_shared/excalidraw-library.json +0 -0
  103. /package/src/modules/bmm/workflows/{frame-expert → diagrams}/_shared/excalidraw-templates.yaml +0 -0
  104. /package/src/modules/bmm/workflows/{frame-expert → diagrams}/create-dataflow/checklist.md +0 -0
  105. /package/src/modules/bmm/workflows/{frame-expert → diagrams}/create-diagram/checklist.md +0 -0
  106. /package/src/modules/bmm/workflows/{frame-expert → diagrams}/create-flowchart/checklist.md +0 -0
  107. /package/src/modules/bmm/workflows/{frame-expert → diagrams}/create-wireframe/checklist.md +0 -0
@@ -0,0 +1,725 @@
1
+ /**
2
+ * BMAD Agent Installer
3
+ * Discovers, prompts, compiles, and installs agents
4
+ */
5
+
6
+ const fs = require('node:fs');
7
+ const path = require('node:path');
8
+ const yaml = require('yaml');
9
+ const readline = require('node:readline');
10
+ const { compileAgent, compileAgentFile } = require('./compiler');
11
+ const { extractInstallConfig, getDefaultValues } = require('./template-engine');
12
+
13
+ /**
14
+ * Find BMAD config file in project
15
+ * @param {string} startPath - Starting directory to search from
16
+ * @returns {Object|null} Config data or null
17
+ */
18
+ function findBmadConfig(startPath = process.cwd()) {
19
+ // Look for common BMAD folder names
20
+ const possibleNames = ['.bmad', 'bmad', '.bmad-method'];
21
+
22
+ for (const name of possibleNames) {
23
+ const configPath = path.join(startPath, name, 'bmb', 'config.yaml');
24
+ if (fs.existsSync(configPath)) {
25
+ const content = fs.readFileSync(configPath, 'utf8');
26
+ const config = yaml.parse(content);
27
+ return {
28
+ ...config,
29
+ bmadFolder: path.join(startPath, name),
30
+ projectRoot: startPath,
31
+ };
32
+ }
33
+ }
34
+
35
+ return null;
36
+ }
37
+
38
+ /**
39
+ * Resolve path variables like {project-root} and {bmad-folder}
40
+ * @param {string} pathStr - Path with variables
41
+ * @param {Object} context - Contains projectRoot, bmadFolder
42
+ * @returns {string} Resolved path
43
+ */
44
+ function resolvePath(pathStr, context) {
45
+ return pathStr.replaceAll('{project-root}', context.projectRoot).replaceAll('{bmad-folder}', context.bmadFolder);
46
+ }
47
+
48
+ /**
49
+ * Discover available agents in the custom agent location
50
+ * @param {string} searchPath - Path to search for agents
51
+ * @returns {Array} List of agent info objects
52
+ */
53
+ function discoverAgents(searchPath) {
54
+ if (!fs.existsSync(searchPath)) {
55
+ return [];
56
+ }
57
+
58
+ const agents = [];
59
+ const entries = fs.readdirSync(searchPath, { withFileTypes: true });
60
+
61
+ for (const entry of entries) {
62
+ const fullPath = path.join(searchPath, entry.name);
63
+
64
+ if (entry.isFile() && entry.name.endsWith('.agent.yaml')) {
65
+ // Simple agent (single file)
66
+ agents.push({
67
+ type: 'simple',
68
+ name: entry.name.replace('.agent.yaml', ''),
69
+ path: fullPath,
70
+ yamlFile: fullPath,
71
+ });
72
+ } else if (entry.isDirectory()) {
73
+ // Check for agent with sidecar (folder containing .agent.yaml)
74
+ const yamlFiles = fs.readdirSync(fullPath).filter((f) => f.endsWith('.agent.yaml'));
75
+ if (yamlFiles.length === 1) {
76
+ const agentYamlPath = path.join(fullPath, yamlFiles[0]);
77
+ agents.push({
78
+ type: 'expert',
79
+ name: entry.name,
80
+ path: fullPath,
81
+ yamlFile: agentYamlPath,
82
+ hasSidecar: true,
83
+ });
84
+ }
85
+ }
86
+ }
87
+
88
+ return agents;
89
+ }
90
+
91
+ /**
92
+ * Load agent YAML and extract install_config
93
+ * @param {string} yamlPath - Path to agent YAML file
94
+ * @returns {Object} Agent YAML and install config
95
+ */
96
+ function loadAgentConfig(yamlPath) {
97
+ const content = fs.readFileSync(yamlPath, 'utf8');
98
+ const agentYaml = yaml.parse(content);
99
+ const installConfig = extractInstallConfig(agentYaml);
100
+ const defaults = installConfig ? getDefaultValues(installConfig) : {};
101
+
102
+ // Check for saved_answers (from previously installed custom agents)
103
+ // These take precedence over defaults
104
+ const savedAnswers = agentYaml?.saved_answers || {};
105
+
106
+ return {
107
+ yamlContent: content,
108
+ agentYaml,
109
+ installConfig,
110
+ defaults: { ...defaults, ...savedAnswers }, // saved_answers override defaults
111
+ metadata: agentYaml?.agent?.metadata || {},
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Interactive prompt for install_config questions
117
+ * @param {Object} installConfig - Install configuration with questions
118
+ * @param {Object} defaults - Default values
119
+ * @returns {Promise<Object>} User answers
120
+ */
121
+ async function promptInstallQuestions(installConfig, defaults, presetAnswers = {}) {
122
+ if (!installConfig || !installConfig.questions || installConfig.questions.length === 0) {
123
+ return { ...defaults, ...presetAnswers };
124
+ }
125
+
126
+ const rl = readline.createInterface({
127
+ input: process.stdin,
128
+ output: process.stdout,
129
+ });
130
+
131
+ const question = (prompt) =>
132
+ new Promise((resolve) => {
133
+ rl.question(prompt, resolve);
134
+ });
135
+
136
+ const answers = { ...defaults, ...presetAnswers };
137
+
138
+ console.log('\n📝 Agent Configuration\n');
139
+ if (installConfig.description) {
140
+ console.log(` ${installConfig.description}\n`);
141
+ }
142
+
143
+ for (const q of installConfig.questions) {
144
+ // Skip questions for variables that are already set (e.g., custom_name set upfront)
145
+ if (answers[q.var] !== undefined && answers[q.var] !== defaults[q.var]) {
146
+ console.log(chalk.dim(` ${q.var}: ${answers[q.var]} (already set)`));
147
+ continue;
148
+ }
149
+
150
+ let response;
151
+
152
+ switch (q.type) {
153
+ case 'text': {
154
+ const defaultHint = q.default ? ` (default: ${q.default})` : '';
155
+ response = await question(` ${q.prompt}${defaultHint}: `);
156
+ answers[q.var] = response || q.default || '';
157
+
158
+ break;
159
+ }
160
+ case 'boolean': {
161
+ const defaultHint = q.default ? ' [Y/n]' : ' [y/N]';
162
+ response = await question(` ${q.prompt}${defaultHint}: `);
163
+ if (response === '') {
164
+ answers[q.var] = q.default;
165
+ } else {
166
+ answers[q.var] = response.toLowerCase().startsWith('y');
167
+ }
168
+
169
+ break;
170
+ }
171
+ case 'choice': {
172
+ console.log(` ${q.prompt}`);
173
+ for (const [idx, opt] of q.options.entries()) {
174
+ const marker = opt.value === q.default ? '* ' : ' ';
175
+ console.log(` ${marker}${idx + 1}. ${opt.label}`);
176
+ }
177
+ const defaultIdx = q.options.findIndex((o) => o.value === q.default) + 1;
178
+ let validChoice = false;
179
+ let choiceIdx;
180
+ while (!validChoice) {
181
+ response = await question(` Choice (default: ${defaultIdx}): `);
182
+ if (response) {
183
+ choiceIdx = parseInt(response, 10) - 1;
184
+ if (isNaN(choiceIdx) || choiceIdx < 0 || choiceIdx >= q.options.length) {
185
+ console.log(` Invalid choice. Please enter 1-${q.options.length}`);
186
+ } else {
187
+ validChoice = true;
188
+ }
189
+ } else {
190
+ choiceIdx = defaultIdx - 1;
191
+ validChoice = true;
192
+ }
193
+ }
194
+ answers[q.var] = q.options[choiceIdx].value;
195
+
196
+ break;
197
+ }
198
+ // No default
199
+ }
200
+ }
201
+
202
+ rl.close();
203
+ return answers;
204
+ }
205
+
206
+ /**
207
+ * Install a compiled agent to target location
208
+ * @param {Object} agentInfo - Agent discovery info
209
+ * @param {Object} answers - User answers for install_config
210
+ * @param {string} targetPath - Target installation directory
211
+ * @returns {Object} Installation result
212
+ */
213
+ function installAgent(agentInfo, answers, targetPath) {
214
+ // Compile the agent
215
+ const { xml, metadata, processedYaml } = compileAgent(fs.readFileSync(agentInfo.yamlFile, 'utf8'), answers);
216
+
217
+ // Determine target agent folder name
218
+ const agentFolderName = metadata.name ? metadata.name.toLowerCase().replaceAll(/\s+/g, '-') : agentInfo.name;
219
+
220
+ const agentTargetDir = path.join(targetPath, agentFolderName);
221
+
222
+ // Create target directory
223
+ if (!fs.existsSync(agentTargetDir)) {
224
+ fs.mkdirSync(agentTargetDir, { recursive: true });
225
+ }
226
+
227
+ // Write compiled XML (.md)
228
+ const compiledFileName = `${agentFolderName}.md`;
229
+ const compiledPath = path.join(agentTargetDir, compiledFileName);
230
+ fs.writeFileSync(compiledPath, xml, 'utf8');
231
+
232
+ const result = {
233
+ success: true,
234
+ agentName: metadata.name || agentInfo.name,
235
+ targetDir: agentTargetDir,
236
+ compiledFile: compiledPath,
237
+ sidecarCopied: false,
238
+ };
239
+
240
+ // Copy sidecar files for expert agents
241
+ if (agentInfo.hasSidecar && agentInfo.type === 'expert') {
242
+ const sidecarFiles = copySidecarFiles(agentInfo.path, agentTargetDir, agentInfo.yamlFile);
243
+ result.sidecarCopied = true;
244
+ result.sidecarFiles = sidecarFiles;
245
+ }
246
+
247
+ return result;
248
+ }
249
+
250
+ /**
251
+ * Recursively copy sidecar files (everything except the .agent.yaml)
252
+ * @param {string} sourceDir - Source agent directory
253
+ * @param {string} targetDir - Target agent directory
254
+ * @param {string} excludeYaml - The .agent.yaml file to exclude
255
+ * @returns {Array} List of copied files
256
+ */
257
+ function copySidecarFiles(sourceDir, targetDir, excludeYaml) {
258
+ const copied = [];
259
+
260
+ function copyDir(src, dest) {
261
+ if (!fs.existsSync(dest)) {
262
+ fs.mkdirSync(dest, { recursive: true });
263
+ }
264
+
265
+ const entries = fs.readdirSync(src, { withFileTypes: true });
266
+ for (const entry of entries) {
267
+ const srcPath = path.join(src, entry.name);
268
+ const destPath = path.join(dest, entry.name);
269
+
270
+ // Skip the source YAML file
271
+ if (srcPath === excludeYaml) {
272
+ continue;
273
+ }
274
+
275
+ if (entry.isDirectory()) {
276
+ copyDir(srcPath, destPath);
277
+ } else {
278
+ fs.copyFileSync(srcPath, destPath);
279
+ copied.push(destPath);
280
+ }
281
+ }
282
+ }
283
+
284
+ copyDir(sourceDir, targetDir);
285
+ return copied;
286
+ }
287
+
288
+ /**
289
+ * Update agent metadata ID to reflect installed location
290
+ * @param {string} compiledContent - Compiled XML content
291
+ * @param {string} targetPath - Target installation path relative to project
292
+ * @returns {string} Updated content
293
+ */
294
+ function updateAgentId(compiledContent, targetPath) {
295
+ // Update the id attribute in the opening agent tag
296
+ return compiledContent.replace(/(<agent\s+id=")[^"]*(")/, `$1${targetPath}$2`);
297
+ }
298
+
299
+ /**
300
+ * Detect if a path is within a BMAD project
301
+ * @param {string} targetPath - Path to check
302
+ * @returns {Object|null} Project info with bmadFolder and cfgFolder
303
+ */
304
+ function detectBmadProject(targetPath) {
305
+ let checkPath = path.resolve(targetPath);
306
+ const root = path.parse(checkPath).root;
307
+
308
+ // Walk up directory tree looking for BMAD installation
309
+ while (checkPath !== root) {
310
+ const possibleNames = ['.bmad', 'bmad'];
311
+ for (const name of possibleNames) {
312
+ const bmadFolder = path.join(checkPath, name);
313
+ const cfgFolder = path.join(bmadFolder, '_cfg');
314
+ const manifestFile = path.join(cfgFolder, 'agent-manifest.csv');
315
+
316
+ if (fs.existsSync(manifestFile)) {
317
+ return {
318
+ projectRoot: checkPath,
319
+ bmadFolder,
320
+ cfgFolder,
321
+ manifestFile,
322
+ };
323
+ }
324
+ }
325
+ checkPath = path.dirname(checkPath);
326
+ }
327
+
328
+ return null;
329
+ }
330
+
331
+ /**
332
+ * Escape CSV field value
333
+ * @param {string} value - Value to escape
334
+ * @returns {string} Escaped value
335
+ */
336
+ function escapeCsvField(value) {
337
+ if (typeof value !== 'string') value = String(value);
338
+ // If contains comma, quote, or newline, wrap in quotes and escape internal quotes
339
+ if (value.includes(',') || value.includes('"') || value.includes('\n')) {
340
+ return '"' + value.replaceAll('"', '""') + '"';
341
+ }
342
+ return value;
343
+ }
344
+
345
+ /**
346
+ * Parse CSV line respecting quoted fields
347
+ * @param {string} line - CSV line
348
+ * @returns {Array} Parsed fields
349
+ */
350
+ function parseCsvLine(line) {
351
+ const fields = [];
352
+ let current = '';
353
+ let inQuotes = false;
354
+
355
+ for (let i = 0; i < line.length; i++) {
356
+ const char = line[i];
357
+ const nextChar = line[i + 1];
358
+
359
+ if (char === '"' && !inQuotes) {
360
+ inQuotes = true;
361
+ } else if (char === '"' && inQuotes) {
362
+ if (nextChar === '"') {
363
+ current += '"';
364
+ i++; // Skip escaped quote
365
+ } else {
366
+ inQuotes = false;
367
+ }
368
+ } else if (char === ',' && !inQuotes) {
369
+ fields.push(current);
370
+ current = '';
371
+ } else {
372
+ current += char;
373
+ }
374
+ }
375
+ fields.push(current);
376
+ return fields;
377
+ }
378
+
379
+ /**
380
+ * Check if agent name exists in manifest
381
+ * @param {string} manifestFile - Path to agent-manifest.csv
382
+ * @param {string} agentName - Agent name to check
383
+ * @returns {Object|null} Existing entry or null
384
+ */
385
+ function checkManifestForAgent(manifestFile, agentName) {
386
+ const content = fs.readFileSync(manifestFile, 'utf8');
387
+ const lines = content.trim().split('\n');
388
+
389
+ if (lines.length < 2) return null;
390
+
391
+ const header = parseCsvLine(lines[0]);
392
+ const nameIndex = header.indexOf('name');
393
+
394
+ if (nameIndex === -1) return null;
395
+
396
+ for (let i = 1; i < lines.length; i++) {
397
+ const fields = parseCsvLine(lines[i]);
398
+ if (fields[nameIndex] === agentName) {
399
+ const entry = {};
400
+ for (const [idx, col] of header.entries()) {
401
+ entry[col] = fields[idx] || '';
402
+ }
403
+ entry._lineNumber = i;
404
+ return entry;
405
+ }
406
+ }
407
+
408
+ return null;
409
+ }
410
+
411
+ /**
412
+ * Check if agent path exists in manifest
413
+ * @param {string} manifestFile - Path to agent-manifest.csv
414
+ * @param {string} agentPath - Agent path to check
415
+ * @returns {Object|null} Existing entry or null
416
+ */
417
+ function checkManifestForPath(manifestFile, agentPath) {
418
+ const content = fs.readFileSync(manifestFile, 'utf8');
419
+ const lines = content.trim().split('\n');
420
+
421
+ if (lines.length < 2) return null;
422
+
423
+ const header = parseCsvLine(lines[0]);
424
+ const pathIndex = header.indexOf('path');
425
+
426
+ if (pathIndex === -1) return null;
427
+
428
+ for (let i = 1; i < lines.length; i++) {
429
+ const fields = parseCsvLine(lines[i]);
430
+ if (fields[pathIndex] === agentPath) {
431
+ const entry = {};
432
+ for (const [idx, col] of header.entries()) {
433
+ entry[col] = fields[idx] || '';
434
+ }
435
+ entry._lineNumber = i;
436
+ return entry;
437
+ }
438
+ }
439
+
440
+ return null;
441
+ }
442
+
443
+ /**
444
+ * Update existing entry in manifest
445
+ * @param {string} manifestFile - Path to agent-manifest.csv
446
+ * @param {Object} agentData - New agent data
447
+ * @param {number} lineNumber - Line number to replace (1-indexed, excluding header)
448
+ * @returns {boolean} Success
449
+ */
450
+ function updateManifestEntry(manifestFile, agentData, lineNumber) {
451
+ const content = fs.readFileSync(manifestFile, 'utf8');
452
+ const lines = content.trim().split('\n');
453
+
454
+ const header = lines[0];
455
+ const columns = header.split(',');
456
+
457
+ // Build the new row
458
+ const row = columns.map((col) => {
459
+ const value = agentData[col] || '';
460
+ return escapeCsvField(value);
461
+ });
462
+
463
+ // Replace the line
464
+ lines[lineNumber] = row.join(',');
465
+
466
+ fs.writeFileSync(manifestFile, lines.join('\n') + '\n', 'utf8');
467
+ return true;
468
+ }
469
+
470
+ /**
471
+ * Add agent to manifest CSV
472
+ * @param {string} manifestFile - Path to agent-manifest.csv
473
+ * @param {Object} agentData - Agent metadata and path info
474
+ * @returns {boolean} Success
475
+ */
476
+ function addToManifest(manifestFile, agentData) {
477
+ const content = fs.readFileSync(manifestFile, 'utf8');
478
+ const lines = content.trim().split('\n');
479
+
480
+ // Parse header to understand column order
481
+ const header = lines[0];
482
+ const columns = header.split(',');
483
+
484
+ // Build the new row based on header columns
485
+ const row = columns.map((col) => {
486
+ const value = agentData[col] || '';
487
+ return escapeCsvField(value);
488
+ });
489
+
490
+ // Append new row
491
+ const newLine = row.join(',');
492
+ const updatedContent = content.trim() + '\n' + newLine + '\n';
493
+
494
+ fs.writeFileSync(manifestFile, updatedContent, 'utf8');
495
+ return true;
496
+ }
497
+
498
+ /**
499
+ * Save agent source YAML to _cfg/custom/agents/ for reinstallation
500
+ * Stores user answers in a top-level saved_answers section (cleaner than overwriting defaults)
501
+ * @param {Object} agentInfo - Agent info (path, type, etc.)
502
+ * @param {string} cfgFolder - Path to _cfg folder
503
+ * @param {string} agentName - Final agent name (e.g., "fred-commit-poet")
504
+ * @param {Object} answers - User answers to save for reinstallation
505
+ * @returns {Object} Info about saved source
506
+ */
507
+ function saveAgentSource(agentInfo, cfgFolder, agentName, answers = {}) {
508
+ // Save to _cfg/custom/agents/ instead of _cfg/agents/
509
+ const customAgentsCfgDir = path.join(cfgFolder, 'custom', 'agents');
510
+
511
+ if (!fs.existsSync(customAgentsCfgDir)) {
512
+ fs.mkdirSync(customAgentsCfgDir, { recursive: true });
513
+ }
514
+
515
+ const yamlLib = require('yaml');
516
+
517
+ /**
518
+ * Add saved_answers section to store user's actual answers
519
+ */
520
+ function addSavedAnswers(agentYaml, answers) {
521
+ // Store answers in a clear, separate section
522
+ agentYaml.saved_answers = answers;
523
+ return agentYaml;
524
+ }
525
+
526
+ if (agentInfo.type === 'simple') {
527
+ // Simple agent: copy YAML with saved_answers section
528
+ const targetYaml = path.join(customAgentsCfgDir, `${agentName}.agent.yaml`);
529
+ const originalContent = fs.readFileSync(agentInfo.yamlFile, 'utf8');
530
+ const agentYaml = yamlLib.parse(originalContent);
531
+
532
+ // Add saved_answers section with user's choices
533
+ addSavedAnswers(agentYaml, answers);
534
+
535
+ fs.writeFileSync(targetYaml, yamlLib.stringify(agentYaml), 'utf8');
536
+ return { type: 'simple', path: targetYaml };
537
+ } else {
538
+ // Expert agent with sidecar: copy entire folder with saved_answers
539
+ const targetFolder = path.join(customAgentsCfgDir, agentName);
540
+ if (!fs.existsSync(targetFolder)) {
541
+ fs.mkdirSync(targetFolder, { recursive: true });
542
+ }
543
+
544
+ // Copy YAML and entire sidecar structure
545
+ const sourceDir = agentInfo.path;
546
+ const copied = [];
547
+
548
+ function copyDir(src, dest) {
549
+ if (!fs.existsSync(dest)) {
550
+ fs.mkdirSync(dest, { recursive: true });
551
+ }
552
+
553
+ const entries = fs.readdirSync(src, { withFileTypes: true });
554
+ for (const entry of entries) {
555
+ const srcPath = path.join(src, entry.name);
556
+ const destPath = path.join(dest, entry.name);
557
+
558
+ if (entry.isDirectory()) {
559
+ copyDir(srcPath, destPath);
560
+ } else if (entry.name.endsWith('.agent.yaml')) {
561
+ // For the agent YAML, add saved_answers section
562
+ const originalContent = fs.readFileSync(srcPath, 'utf8');
563
+ const agentYaml = yamlLib.parse(originalContent);
564
+ addSavedAnswers(agentYaml, answers);
565
+ // Rename YAML to match final agent name
566
+ const newYamlPath = path.join(dest, `${agentName}.agent.yaml`);
567
+ fs.writeFileSync(newYamlPath, yamlLib.stringify(agentYaml), 'utf8');
568
+ copied.push(newYamlPath);
569
+ } else {
570
+ fs.copyFileSync(srcPath, destPath);
571
+ copied.push(destPath);
572
+ }
573
+ }
574
+ }
575
+
576
+ copyDir(sourceDir, targetFolder);
577
+ return { type: 'expert', path: targetFolder, files: copied };
578
+ }
579
+ }
580
+
581
+ /**
582
+ * Create IDE slash command wrapper for agent
583
+ * Leverages IdeManager to dispatch to IDE-specific handlers
584
+ * @param {string} projectRoot - Project root path
585
+ * @param {string} agentName - Agent name (e.g., "commit-poet")
586
+ * @param {string} agentPath - Path to compiled agent (relative to project root)
587
+ * @param {Object} metadata - Agent metadata
588
+ * @returns {Promise<Object>} Info about created slash commands
589
+ */
590
+ async function createIdeSlashCommands(projectRoot, agentName, agentPath, metadata) {
591
+ // Read manifest.yaml to get installed IDEs
592
+ const manifestPath = path.join(projectRoot, '.bmad', '_cfg', 'manifest.yaml');
593
+ let installedIdes = ['claude-code']; // Default to Claude Code if no manifest
594
+
595
+ if (fs.existsSync(manifestPath)) {
596
+ const yamlLib = require('yaml');
597
+ const manifestContent = fs.readFileSync(manifestPath, 'utf8');
598
+ const manifest = yamlLib.parse(manifestContent);
599
+ if (manifest.ides && Array.isArray(manifest.ides)) {
600
+ installedIdes = manifest.ides;
601
+ }
602
+ }
603
+
604
+ // Use IdeManager to install custom agent launchers for all configured IDEs
605
+ const { IdeManager } = require('../../installers/lib/ide/manager');
606
+ const ideManager = new IdeManager();
607
+
608
+ const results = await ideManager.installCustomAgentLaunchers(installedIdes, projectRoot, agentName, agentPath, metadata);
609
+
610
+ return results;
611
+ }
612
+
613
+ /**
614
+ * Update manifest.yaml to track custom agent
615
+ * @param {string} manifestPath - Path to manifest.yaml
616
+ * @param {string} agentName - Agent name
617
+ * @param {string} agentType - Agent type (source name)
618
+ * @returns {boolean} Success
619
+ */
620
+ function updateManifestYaml(manifestPath, agentName, agentType) {
621
+ if (!fs.existsSync(manifestPath)) {
622
+ return false;
623
+ }
624
+
625
+ const yamlLib = require('yaml');
626
+ const content = fs.readFileSync(manifestPath, 'utf8');
627
+ const manifest = yamlLib.parse(content);
628
+
629
+ // Initialize custom_agents array if not exists
630
+ if (!manifest.custom_agents) {
631
+ manifest.custom_agents = [];
632
+ }
633
+
634
+ // Check if this agent is already registered
635
+ const existingIndex = manifest.custom_agents.findIndex((a) => a.name === agentName || (typeof a === 'string' && a === agentName));
636
+
637
+ const agentEntry = {
638
+ name: agentName,
639
+ type: agentType,
640
+ installed: new Date().toISOString(),
641
+ };
642
+
643
+ if (existingIndex === -1) {
644
+ // Add new entry
645
+ manifest.custom_agents.push(agentEntry);
646
+ } else {
647
+ // Update existing entry
648
+ manifest.custom_agents[existingIndex] = agentEntry;
649
+ }
650
+
651
+ // Update lastUpdated timestamp
652
+ if (manifest.installation) {
653
+ manifest.installation.lastUpdated = new Date().toISOString();
654
+ }
655
+
656
+ // Write back
657
+ const newContent = yamlLib.stringify(manifest);
658
+ fs.writeFileSync(manifestPath, newContent, 'utf8');
659
+
660
+ return true;
661
+ }
662
+
663
+ /**
664
+ * Extract manifest data from compiled agent XML
665
+ * @param {string} xmlContent - Compiled agent XML
666
+ * @param {Object} metadata - Agent metadata from YAML
667
+ * @param {string} agentPath - Relative path to agent file
668
+ * @param {string} moduleName - Module name (default: 'custom')
669
+ * @returns {Object} Manifest row data
670
+ */
671
+ function extractManifestData(xmlContent, metadata, agentPath, moduleName = 'custom') {
672
+ // Extract data from XML using regex (simple parsing)
673
+ const extractTag = (tag) => {
674
+ const match = xmlContent.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`));
675
+ if (!match) return '';
676
+ // Collapse multiple lines into single line, normalize whitespace
677
+ return match[1].trim().replaceAll(/\n+/g, ' ').replaceAll(/\s+/g, ' ').trim();
678
+ };
679
+
680
+ const extractPrinciples = () => {
681
+ const match = xmlContent.match(/<principles>([\s\S]*?)<\/principles>/);
682
+ if (!match) return '';
683
+ // Extract individual principle lines
684
+ const principles = match[1]
685
+ .split('\n')
686
+ .map((l) => l.trim())
687
+ .filter((l) => l.length > 0)
688
+ .join(' ');
689
+ return principles;
690
+ };
691
+
692
+ return {
693
+ name: metadata.id ? path.basename(metadata.id, '.md') : metadata.name.toLowerCase().replaceAll(/\s+/g, '-'),
694
+ displayName: metadata.name || '',
695
+ title: metadata.title || '',
696
+ icon: metadata.icon || '',
697
+ role: extractTag('role'),
698
+ identity: extractTag('identity'),
699
+ communicationStyle: extractTag('communication_style'),
700
+ principles: extractPrinciples(),
701
+ module: moduleName,
702
+ path: agentPath,
703
+ };
704
+ }
705
+
706
+ module.exports = {
707
+ findBmadConfig,
708
+ resolvePath,
709
+ discoverAgents,
710
+ loadAgentConfig,
711
+ promptInstallQuestions,
712
+ installAgent,
713
+ copySidecarFiles,
714
+ updateAgentId,
715
+ detectBmadProject,
716
+ addToManifest,
717
+ extractManifestData,
718
+ escapeCsvField,
719
+ checkManifestForAgent,
720
+ checkManifestForPath,
721
+ updateManifestEntry,
722
+ saveAgentSource,
723
+ createIdeSlashCommands,
724
+ updateManifestYaml,
725
+ };