bmad-method 4.44.0 → 4.44.2

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 (68) hide show
  1. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  2. package/.github/workflows/manual-release.yaml +1 -1
  3. package/PR-opencode-agents-generator.md +40 -0
  4. package/README.md +38 -44
  5. package/bmad-core/agents/analyst.md +1 -1
  6. package/bmad-core/agents/architect.md +1 -1
  7. package/bmad-core/agents/bmad-master.md +7 -7
  8. package/bmad-core/agents/bmad-orchestrator.md +2 -2
  9. package/bmad-core/agents/dev.md +1 -1
  10. package/bmad-core/agents/pm.md +1 -1
  11. package/bmad-core/agents/po.md +1 -1
  12. package/bmad-core/agents/qa.md +2 -6
  13. package/bmad-core/agents/sm.md +1 -1
  14. package/bmad-core/agents/ux-expert.md +1 -1
  15. package/bmad-core/checklists/po-master-checklist.md +3 -3
  16. package/bmad-core/data/bmad-kb.md +1 -1
  17. package/bmad-core/tasks/apply-qa-fixes.md +4 -4
  18. package/bmad-core/tasks/nfr-assess.md +3 -3
  19. package/bmad-core/tasks/qa-gate.md +2 -2
  20. package/bmad-core/tasks/review-story.md +1 -1
  21. package/bmad-core/templates/brownfield-architecture-tmpl.yaml +6 -6
  22. package/dist/agents/analyst.txt +3 -11
  23. package/dist/agents/architect.txt +1 -7
  24. package/dist/agents/bmad-master.txt +7 -31
  25. package/dist/agents/bmad-orchestrator.txt +1 -8
  26. package/dist/agents/dev.txt +5 -10
  27. package/dist/agents/pm.txt +0 -10
  28. package/dist/agents/po.txt +4 -10
  29. package/dist/agents/qa.txt +7 -15
  30. package/dist/agents/sm.txt +0 -4
  31. package/dist/agents/ux-expert.txt +0 -4
  32. package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-designer.txt +0 -6
  33. package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-developer.txt +0 -3
  34. package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-sm.txt +0 -3
  35. package/dist/expansion-packs/bmad-2d-phaser-game-dev/teams/phaser-2d-nodejs-game-team.txt +2 -25
  36. package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-architect.txt +0 -9
  37. package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-designer.txt +0 -8
  38. package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-developer.txt +1 -4
  39. package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-sm.txt +0 -4
  40. package/dist/expansion-packs/bmad-2d-unity-game-dev/teams/unity-2d-game-team.txt +3 -35
  41. package/dist/expansion-packs/bmad-creative-writing/agents/beta-reader.txt +0 -9
  42. package/dist/expansion-packs/bmad-creative-writing/agents/character-psychologist.txt +0 -8
  43. package/dist/expansion-packs/bmad-creative-writing/agents/dialog-specialist.txt +0 -7
  44. package/dist/expansion-packs/bmad-creative-writing/agents/editor.txt +0 -8
  45. package/dist/expansion-packs/bmad-creative-writing/agents/genre-specialist.txt +0 -10
  46. package/dist/expansion-packs/bmad-creative-writing/agents/narrative-designer.txt +0 -8
  47. package/dist/expansion-packs/bmad-creative-writing/agents/plot-architect.txt +0 -7
  48. package/dist/expansion-packs/bmad-creative-writing/agents/world-builder.txt +0 -9
  49. package/dist/expansion-packs/bmad-creative-writing/teams/agent-team.txt +0 -85
  50. package/dist/expansion-packs/bmad-godot-game-dev/agents/game-developer.txt +2 -2
  51. package/dist/expansion-packs/bmad-godot-game-dev/agents/game-qa.txt +1 -2
  52. package/dist/expansion-packs/bmad-godot-game-dev/teams/godot-game-team.txt +5 -6
  53. package/dist/expansion-packs/bmad-infrastructure-devops/agents/infra-devops-platform.txt +0 -5
  54. package/dist/teams/team-all.txt +34 -69
  55. package/dist/teams/team-fullstack.txt +23 -46
  56. package/dist/teams/team-ide-minimal.txt +16 -42
  57. package/dist/teams/team-no-ui.txt +12 -34
  58. package/docs/user-guide.md +48 -1
  59. package/expansion-packs/bmad-godot-game-dev/agents/bmad-orchestrator.md +1 -1
  60. package/expansion-packs/bmad-godot-game-dev/agents/game-qa.md +1 -2
  61. package/expansion-packs/bmad-godot-game-dev/tasks/apply-qa-fixes.md +2 -2
  62. package/package.json +2 -1
  63. package/tools/installer/bin/bmad.js +46 -1
  64. package/tools/installer/config/install.config.yaml +18 -7
  65. package/tools/installer/lib/ide-setup.js +709 -77
  66. package/tools/installer/lib/installer.js +17 -4
  67. package/tools/installer/package.json +1 -1
  68. package/release_notes.md +0 -48
@@ -3,6 +3,7 @@ const fs = require('fs-extra');
3
3
  const yaml = require('js-yaml');
4
4
  const chalk = require('chalk');
5
5
  const inquirer = require('inquirer');
6
+ const cjson = require('comment-json');
6
7
  const fileManager = require('./file-manager');
7
8
  const configLoader = require('./config-loader');
8
9
  const { extractYamlFromAgent } = require('../../lib/yaml-utils');
@@ -44,6 +45,9 @@ class IdeSetup extends BaseIdeSetup {
44
45
  case 'cursor': {
45
46
  return this.setupCursor(installDir, selectedAgent);
46
47
  }
48
+ case 'opencode': {
49
+ return this.setupOpenCode(installDir, selectedAgent, spinner, preConfiguredSettings);
50
+ }
47
51
  case 'claude-code': {
48
52
  return this.setupClaudeCode(installDir, selectedAgent);
49
53
  }
@@ -93,6 +97,643 @@ class IdeSetup extends BaseIdeSetup {
93
97
  }
94
98
  }
95
99
 
100
+ async setupOpenCode(installDir, selectedAgent, spinner = null, preConfiguredSettings = null) {
101
+ // Minimal JSON-only integration per plan:
102
+ // - If opencode.json or opencode.jsonc exists: only ensure instructions include .bmad-core/core-config.yaml
103
+ // - If none exists: create minimal opencode.jsonc with $schema and instructions array including that file
104
+
105
+ const jsonPath = path.join(installDir, 'opencode.json');
106
+ const jsoncPath = path.join(installDir, 'opencode.jsonc');
107
+ const hasJson = await fileManager.pathExists(jsonPath);
108
+ const hasJsonc = await fileManager.pathExists(jsoncPath);
109
+
110
+ // Determine key prefix preferences (with sensible defaults)
111
+ // Defaults: non-prefixed (agents = "dev", commands = "create-doc")
112
+ let useAgentPrefix = false;
113
+ let useCommandPrefix = false;
114
+
115
+ // Allow pre-configuration (if passed) to skip prompts
116
+ const pre = preConfiguredSettings && preConfiguredSettings.opencode;
117
+ if (pre && typeof pre.useAgentPrefix === 'boolean') useAgentPrefix = pre.useAgentPrefix;
118
+ if (pre && typeof pre.useCommandPrefix === 'boolean') useCommandPrefix = pre.useCommandPrefix;
119
+
120
+ // If no pre-config and in interactive mode, prompt the user
121
+ if (!pre) {
122
+ // Pause spinner during prompts if active
123
+ let spinnerWasActive = false;
124
+ if (spinner && spinner.isSpinning) {
125
+ spinner.stop();
126
+ spinnerWasActive = true;
127
+ }
128
+
129
+ try {
130
+ const resp = await inquirer.prompt([
131
+ {
132
+ type: 'confirm',
133
+ name: 'useAgentPrefix',
134
+ message:
135
+ "Prefix agent keys with 'bmad-'? (Recommended to avoid collisions, e.g., 'bmad-dev')",
136
+ default: true,
137
+ },
138
+ {
139
+ type: 'confirm',
140
+ name: 'useCommandPrefix',
141
+ message:
142
+ "Prefix command keys with 'bmad:tasks:'? (Recommended, e.g., 'bmad:tasks:create-doc')",
143
+ default: true,
144
+ },
145
+ ]);
146
+ useAgentPrefix = resp.useAgentPrefix;
147
+ useCommandPrefix = resp.useCommandPrefix;
148
+ } catch {
149
+ // Keep defaults if prompt fails or is not interactive
150
+ } finally {
151
+ if (spinner && spinnerWasActive) spinner.start();
152
+ }
153
+ }
154
+
155
+ const ensureInstructionRef = (obj) => {
156
+ const preferred = '.bmad-core/core-config.yaml';
157
+ const alt = './.bmad-core/core-config.yaml';
158
+ if (!obj.instructions) obj.instructions = [];
159
+ if (!Array.isArray(obj.instructions)) obj.instructions = [obj.instructions];
160
+ // Normalize alternative form (with './') to preferred without './'
161
+ obj.instructions = obj.instructions.map((it) =>
162
+ typeof it === 'string' && it === alt ? preferred : it,
163
+ );
164
+ const hasPreferred = obj.instructions.some(
165
+ (it) => typeof it === 'string' && it === preferred,
166
+ );
167
+ if (!hasPreferred) obj.instructions.push(preferred);
168
+ return obj;
169
+ };
170
+
171
+ const mergeBmadAgentsAndCommands = async (configObj) => {
172
+ // Ensure objects exist
173
+ if (!configObj.agent || typeof configObj.agent !== 'object') configObj.agent = {};
174
+ if (!configObj.command || typeof configObj.command !== 'object') configObj.command = {};
175
+ if (!configObj.instructions) configObj.instructions = [];
176
+ if (!Array.isArray(configObj.instructions)) configObj.instructions = [configObj.instructions];
177
+
178
+ // Track a concise summary of changes
179
+ const summary = {
180
+ target: null,
181
+ created: false,
182
+ agentsAdded: 0,
183
+ agentsUpdated: 0,
184
+ agentsSkipped: 0,
185
+ commandsAdded: 0,
186
+ commandsUpdated: 0,
187
+ commandsSkipped: 0,
188
+ };
189
+
190
+ // Determine package scope: previously SELECTED packages in installer UI
191
+ const selectedPackages = preConfiguredSettings?.selectedPackages || {
192
+ includeCore: true,
193
+ packs: [],
194
+ };
195
+
196
+ // Helper: ensure an instruction path is present without './' prefix, de-duplicating './' variants
197
+ const ensureInstructionPath = (pathNoDot) => {
198
+ const withDot = `./${pathNoDot}`;
199
+ // Normalize any existing './' variant to non './'
200
+ configObj.instructions = configObj.instructions.map((it) =>
201
+ typeof it === 'string' && it === withDot ? pathNoDot : it,
202
+ );
203
+ const has = configObj.instructions.some((it) => typeof it === 'string' && it === pathNoDot);
204
+ if (!has) configObj.instructions.push(pathNoDot);
205
+ };
206
+
207
+ // Helper: detect orchestrator agents to set as primary mode
208
+ const isOrchestratorAgent = (agentId) => /(^|-)orchestrator$/i.test(agentId);
209
+
210
+ // Helper: extract whenToUse string from an agent markdown file
211
+ const extractWhenToUseFromFile = async (absPath) => {
212
+ try {
213
+ const raw = await fileManager.readFile(absPath);
214
+ const yamlMatch = raw.match(/```ya?ml\r?\n([\s\S]*?)```/);
215
+ const yamlBlock = yamlMatch ? yamlMatch[1].trim() : null;
216
+ if (!yamlBlock) return null;
217
+ // Try quoted first, then unquoted
218
+ const quoted = yamlBlock.match(/whenToUse:\s*"([^"]+)"/i);
219
+ if (quoted && quoted[1]) return quoted[1].trim();
220
+ const unquoted = yamlBlock.match(/whenToUse:\s*([^\n\r]+)/i);
221
+ if (unquoted && unquoted[1]) return unquoted[1].trim();
222
+ } catch {
223
+ // ignore
224
+ }
225
+ return null;
226
+ };
227
+
228
+ // Helper: extract Purpose string from a task file (YAML fenced block, Markdown heading, or inline 'Purpose:')
229
+ const extractTaskPurposeFromFile = async (absPath) => {
230
+ const cleanupAndSummarize = (text) => {
231
+ if (!text) return null;
232
+ let t = String(text);
233
+ // Drop code fences and HTML comments
234
+ t = t.replaceAll(/```[\s\S]*?```/g, '');
235
+ t = t.replaceAll(/<!--([\s\S]*?)-->/g, '');
236
+ // Normalize line endings
237
+ t = t.replaceAll(/\r\n?/g, '\n');
238
+ // Take the first non-empty paragraph
239
+ const paragraphs = t.split(/\n\s*\n/g).map((p) => p.trim());
240
+ let first = paragraphs.find((p) => p.length > 0) || '';
241
+ // Remove leading list markers, quotes, and headings remnants
242
+ first = first.replaceAll(/^\s*[>*-]\s+/gm, '');
243
+ first = first.replaceAll(/^#{1,6}\s+/gm, '');
244
+ // Strip simple Markdown formatting
245
+ first = first.replaceAll(/\*\*([^*]+)\*\*/g, '$1').replaceAll(/\*([^*]+)\*/g, '$1');
246
+ first = first.replaceAll(/`([^`]+)`/g, '$1');
247
+ // Collapse whitespace
248
+ first = first.replaceAll(/\s+/g, ' ').trim();
249
+ if (!first) return null;
250
+ // Prefer ending at a sentence boundary if long
251
+ const maxLen = 320;
252
+ if (first.length > maxLen) {
253
+ const boundary = first.slice(0, maxLen + 40).match(/^[\s\S]*?[.!?](\s|$)/);
254
+ const cut = boundary ? boundary[0] : first.slice(0, maxLen);
255
+ return cut.trim();
256
+ }
257
+ return first;
258
+ };
259
+
260
+ try {
261
+ const raw = await fileManager.readFile(absPath);
262
+ // 1) YAML fenced block: look for Purpose fields
263
+ const yamlMatch = raw.match(/```ya?ml\r?\n([\s\S]*?)```/);
264
+ const yamlBlock = yamlMatch ? yamlMatch[1].trim() : null;
265
+ if (yamlBlock) {
266
+ try {
267
+ const data = yaml.load(yamlBlock);
268
+ if (data) {
269
+ let val = data.Purpose ?? data.purpose;
270
+ if (!val && data.task && (data.task.Purpose || data.task.purpose)) {
271
+ val = data.task.Purpose ?? data.task.purpose;
272
+ }
273
+ if (typeof val === 'string') {
274
+ const cleaned = cleanupAndSummarize(val);
275
+ if (cleaned) return cleaned;
276
+ }
277
+ }
278
+ } catch {
279
+ // ignore YAML parse errors
280
+ }
281
+ // Fallback regex inside YAML block
282
+ const quoted = yamlBlock.match(/(?:^|\n)\s*(?:Purpose|purpose):\s*"([^"]+)"/);
283
+ if (quoted && quoted[1]) {
284
+ const cleaned = cleanupAndSummarize(quoted[1]);
285
+ if (cleaned) return cleaned;
286
+ }
287
+ const unquoted = yamlBlock.match(/(?:^|\n)\s*(?:Purpose|purpose):\s*([^\n\r]+)/);
288
+ if (unquoted && unquoted[1]) {
289
+ const cleaned = cleanupAndSummarize(unquoted[1]);
290
+ if (cleaned) return cleaned;
291
+ }
292
+ }
293
+
294
+ // 2) Markdown heading section: ## Purpose (any level >= 2)
295
+ const headingRe = /^(#{2,6})\s*Purpose\s*$/im;
296
+ const headingMatch = headingRe.exec(raw);
297
+ if (headingMatch) {
298
+ const headingLevel = headingMatch[1].length;
299
+ const sectionStart = headingMatch.index + headingMatch[0].length;
300
+ const rest = raw.slice(sectionStart);
301
+ // Next heading of same or higher level ends the section
302
+ const nextHeadingRe = new RegExp(`^#{1,${headingLevel}}\\s+[^\n]+`, 'im');
303
+ const nextMatch = nextHeadingRe.exec(rest);
304
+ const section = nextMatch ? rest.slice(0, nextMatch.index) : rest;
305
+ const cleaned = cleanupAndSummarize(section);
306
+ if (cleaned) return cleaned;
307
+ }
308
+
309
+ // 3) Inline single-line fallback: Purpose: ...
310
+ const inline = raw.match(/(?:^|\n)\s*Purpose\s*:\s*([^\n\r]+)/i);
311
+ if (inline && inline[1]) {
312
+ const cleaned = cleanupAndSummarize(inline[1]);
313
+ if (cleaned) return cleaned;
314
+ }
315
+ } catch {
316
+ // ignore
317
+ }
318
+ return null;
319
+ };
320
+
321
+ // Build core sets
322
+ const coreAgentIds = new Set();
323
+ const coreTaskIds = new Set();
324
+ if (selectedPackages.includeCore) {
325
+ for (const id of await this.getCoreAgentIds(installDir)) coreAgentIds.add(id);
326
+ for (const id of await this.getCoreTaskIds(installDir)) coreTaskIds.add(id);
327
+ }
328
+
329
+ // Build packs info: { packId, packPath, packKey, agents:Set, tasks:Set }
330
+ const packsInfo = [];
331
+ if (Array.isArray(selectedPackages.packs)) {
332
+ for (const packId of selectedPackages.packs) {
333
+ const dotPackPath = path.join(installDir, `.${packId}`);
334
+ const altPackPath = path.join(installDir, 'expansion-packs', packId);
335
+ const packPath = (await fileManager.pathExists(dotPackPath))
336
+ ? dotPackPath
337
+ : (await fileManager.pathExists(altPackPath))
338
+ ? altPackPath
339
+ : null;
340
+ if (!packPath) continue;
341
+
342
+ // Ensure pack config.yaml is added to instructions (relative path, no './')
343
+ const packConfigAbs = path.join(packPath, 'config.yaml');
344
+ if (await fileManager.pathExists(packConfigAbs)) {
345
+ const relCfg = path.relative(installDir, packConfigAbs).replaceAll('\\', '/');
346
+ ensureInstructionPath(relCfg);
347
+ }
348
+
349
+ const packKey = packId.replace(/^bmad-/, '').replaceAll('/', '-');
350
+ const info = { packId, packPath, packKey, agents: new Set(), tasks: new Set() };
351
+
352
+ const glob = require('glob');
353
+ const agentsDir = path.join(packPath, 'agents');
354
+ if (await fileManager.pathExists(agentsDir)) {
355
+ const files = glob.sync('*.md', { cwd: agentsDir });
356
+ for (const f of files) info.agents.add(path.basename(f, '.md'));
357
+ }
358
+ const tasksDir = path.join(packPath, 'tasks');
359
+ if (await fileManager.pathExists(tasksDir)) {
360
+ const files = glob.sync('*.md', { cwd: tasksDir });
361
+ for (const f of files) info.tasks.add(path.basename(f, '.md'));
362
+ }
363
+ packsInfo.push(info);
364
+ }
365
+ }
366
+
367
+ // Generate agents - core first (respect optional agent prefix)
368
+ for (const agentId of coreAgentIds) {
369
+ const p = await this.findAgentPath(agentId, installDir); // prefers core
370
+ if (!p) continue;
371
+ const rel = path.relative(installDir, p).replaceAll('\\', '/');
372
+ const fileRef = `{file:./${rel}}`;
373
+ const baseKey = agentId;
374
+ const key = useAgentPrefix
375
+ ? baseKey.startsWith('bmad-')
376
+ ? baseKey
377
+ : `bmad-${baseKey}`
378
+ : baseKey;
379
+ const existing = configObj.agent[key];
380
+ const whenToUse = await extractWhenToUseFromFile(p);
381
+ const agentDef = {
382
+ prompt: fileRef,
383
+ mode: isOrchestratorAgent(agentId) ? 'primary' : 'all',
384
+ tools: { write: true, edit: true, bash: true },
385
+ ...(whenToUse ? { description: whenToUse } : {}),
386
+ };
387
+ if (!existing) {
388
+ configObj.agent[key] = agentDef;
389
+ summary.agentsAdded++;
390
+ } else if (
391
+ existing &&
392
+ typeof existing === 'object' &&
393
+ typeof existing.prompt === 'string' &&
394
+ existing.prompt.includes(rel)
395
+ ) {
396
+ existing.prompt = agentDef.prompt;
397
+ existing.mode = agentDef.mode;
398
+ if (whenToUse) existing.description = whenToUse;
399
+ existing.tools = { write: true, edit: true, bash: true };
400
+ configObj.agent[key] = existing;
401
+ summary.agentsUpdated++;
402
+ } else {
403
+ summary.agentsSkipped++;
404
+ // Collision warning: key exists but does not appear BMAD-managed (different prompt path)
405
+ console.log(
406
+ chalk.yellow(
407
+ `⚠︎ Skipped agent key '${key}' (existing entry not BMAD-managed). Tip: enable agent prefixes to avoid collisions.`,
408
+ ),
409
+ );
410
+ }
411
+ }
412
+
413
+ // Generate agents - expansion packs (forced pack-specific prefix)
414
+ for (const pack of packsInfo) {
415
+ for (const agentId of pack.agents) {
416
+ const p = path.join(pack.packPath, 'agents', `${agentId}.md`);
417
+ if (!(await fileManager.pathExists(p))) continue;
418
+ const rel = path.relative(installDir, p).replaceAll('\\', '/');
419
+ const fileRef = `{file:./${rel}}`;
420
+ const prefixedKey = `bmad-${pack.packKey}-${agentId}`;
421
+ const existing = configObj.agent[prefixedKey];
422
+ const whenToUse = await extractWhenToUseFromFile(p);
423
+ const agentDef = {
424
+ prompt: fileRef,
425
+ mode: isOrchestratorAgent(agentId) ? 'primary' : 'all',
426
+ tools: { write: true, edit: true, bash: true },
427
+ ...(whenToUse ? { description: whenToUse } : {}),
428
+ };
429
+ if (!existing) {
430
+ configObj.agent[prefixedKey] = agentDef;
431
+ summary.agentsAdded++;
432
+ } else if (
433
+ existing &&
434
+ typeof existing === 'object' &&
435
+ typeof existing.prompt === 'string' &&
436
+ existing.prompt.includes(rel)
437
+ ) {
438
+ existing.prompt = agentDef.prompt;
439
+ existing.mode = agentDef.mode;
440
+ if (whenToUse) existing.description = whenToUse;
441
+ existing.tools = { write: true, edit: true, bash: true };
442
+ configObj.agent[prefixedKey] = existing;
443
+ summary.agentsUpdated++;
444
+ } else {
445
+ summary.agentsSkipped++;
446
+ console.log(
447
+ chalk.yellow(
448
+ `⚠︎ Skipped agent key '${prefixedKey}' (existing entry not BMAD-managed). Tip: enable agent prefixes to avoid collisions.`,
449
+ ),
450
+ );
451
+ }
452
+ }
453
+ }
454
+
455
+ // Generate commands - core first (respect optional command prefix)
456
+ for (const taskId of coreTaskIds) {
457
+ const p = await this.findTaskPath(taskId, installDir); // prefers core/common
458
+ if (!p) continue;
459
+ const rel = path.relative(installDir, p).replaceAll('\\', '/');
460
+ const fileRef = `{file:./${rel}}`;
461
+ const key = useCommandPrefix ? `bmad:tasks:${taskId}` : `${taskId}`;
462
+ const existing = configObj.command[key];
463
+ const purpose = await extractTaskPurposeFromFile(p);
464
+ const cmdDef = { template: fileRef, ...(purpose ? { description: purpose } : {}) };
465
+ if (!existing) {
466
+ configObj.command[key] = cmdDef;
467
+ summary.commandsAdded++;
468
+ } else if (
469
+ existing &&
470
+ typeof existing === 'object' &&
471
+ typeof existing.template === 'string' &&
472
+ existing.template.includes(rel)
473
+ ) {
474
+ existing.template = cmdDef.template;
475
+ if (purpose) existing.description = purpose;
476
+ configObj.command[key] = existing;
477
+ summary.commandsUpdated++;
478
+ } else {
479
+ summary.commandsSkipped++;
480
+ console.log(
481
+ chalk.yellow(
482
+ `⚠︎ Skipped command key '${key}' (existing entry not BMAD-managed). Tip: enable command prefixes to avoid collisions.`,
483
+ ),
484
+ );
485
+ }
486
+ }
487
+
488
+ // Generate commands - expansion packs (forced pack-specific prefix)
489
+ for (const pack of packsInfo) {
490
+ for (const taskId of pack.tasks) {
491
+ const p = path.join(pack.packPath, 'tasks', `${taskId}.md`);
492
+ if (!(await fileManager.pathExists(p))) continue;
493
+ const rel = path.relative(installDir, p).replaceAll('\\', '/');
494
+ const fileRef = `{file:./${rel}}`;
495
+ const prefixedKey = `bmad:${pack.packKey}:${taskId}`;
496
+ const existing = configObj.command[prefixedKey];
497
+ const purpose = await extractTaskPurposeFromFile(p);
498
+ const cmdDef = { template: fileRef, ...(purpose ? { description: purpose } : {}) };
499
+ if (!existing) {
500
+ configObj.command[prefixedKey] = cmdDef;
501
+ summary.commandsAdded++;
502
+ } else if (
503
+ existing &&
504
+ typeof existing === 'object' &&
505
+ typeof existing.template === 'string' &&
506
+ existing.template.includes(rel)
507
+ ) {
508
+ existing.template = cmdDef.template;
509
+ if (purpose) existing.description = purpose;
510
+ configObj.command[prefixedKey] = existing;
511
+ summary.commandsUpdated++;
512
+ } else {
513
+ summary.commandsSkipped++;
514
+ console.log(
515
+ chalk.yellow(
516
+ `⚠︎ Skipped command key '${prefixedKey}' (existing entry not BMAD-managed). Tip: enable command prefixes to avoid collisions.`,
517
+ ),
518
+ );
519
+ }
520
+ }
521
+ }
522
+
523
+ return { configObj, summary };
524
+ };
525
+
526
+ // Helper: generate AGENTS.md section for OpenCode (acts as system prompt memory)
527
+ const generateOpenCodeAgentsMd = async () => {
528
+ try {
529
+ const filePath = path.join(installDir, 'AGENTS.md');
530
+ const startMarker = '<!-- BEGIN: BMAD-AGENTS-OPENCODE -->';
531
+ const endMarker = '<!-- END: BMAD-AGENTS-OPENCODE -->';
532
+
533
+ const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);
534
+ const tasks = await this.getAllTaskIds(installDir);
535
+
536
+ let section = '';
537
+ section += `${startMarker}\n`;
538
+ section += `# BMAD-METHOD Agents and Tasks (OpenCode)\n\n`;
539
+ 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`;
540
+ section += `## How To Use With OpenCode\n\n`;
541
+ section += `- Run \`opencode\` in this project. OpenCode will read \`AGENTS.md\` and your OpenCode config (opencode.json[c]).\n`;
542
+ section += `- Reference a role naturally, e.g., "As dev, implement ..." or use commands defined in your BMAD tasks.\n`;
543
+ section += `- Commit \`.bmad-core\` and \`AGENTS.md\` if you want teammates to share the same configuration.\n`;
544
+ section += `- Refresh this section after BMAD updates: \`npx bmad-method install -f -i opencode\`.\n\n`;
545
+
546
+ section += `### Helpful Commands\n\n`;
547
+ section += `- List agents: \`npx bmad-method list:agents\`\n`;
548
+ section += `- Reinstall BMAD core and regenerate this section: \`npx bmad-method install -f -i opencode\`\n`;
549
+ section += `- Validate configuration: \`npx bmad-method validate\`\n\n`;
550
+
551
+ // Brief context note for modes and tools
552
+ section += `Note\n`;
553
+ section += `- Orchestrators run as mode: primary; other agents as all.\n`;
554
+ section += `- All agents have tools enabled: write, edit, bash.\n\n`;
555
+
556
+ section += `## Agents\n\n`;
557
+ section += `### Directory\n\n`;
558
+ section += `| Title | ID | When To Use |\n|---|---|---|\n`;
559
+
560
+ // Fallback descriptions for core agents (used if whenToUse is missing)
561
+ const fallbackDescriptions = {
562
+ 'ux-expert':
563
+ 'Use for UI/UX design, wireframes, prototypes, front-end specs, and user experience optimization',
564
+ sm: 'Use for story creation, epic management, retrospectives in party-mode, and agile process guidance',
565
+ qa: 'Ensure quality strategy, test design, risk profiling, and QA gates across features',
566
+ po: 'Backlog management, story refinement, acceptance criteria, sprint planning, prioritization decisions',
567
+ pm: 'PRDs, product strategy, feature prioritization, roadmap planning, and stakeholder communication',
568
+ dev: 'Code implementation, debugging, refactoring, and development best practices',
569
+ 'bmad-orchestrator':
570
+ 'Workflow coordination, multi-agent tasks, role switching guidance, and when unsure which specialist to consult',
571
+ 'bmad-master':
572
+ 'Comprehensive cross-domain execution for tasks that do not require a specific persona',
573
+ architect:
574
+ 'System design, architecture docs, technology selection, API design, and infrastructure planning',
575
+ analyst:
576
+ 'Discovery/research, competitive analysis, project briefs, initial discovery, and brownfield documentation',
577
+ };
578
+
579
+ const sanitizeDesc = (s) => {
580
+ if (!s) return '';
581
+ let t = String(s).trim();
582
+ // Drop surrounding single/double/backtick quotes
583
+ t = t.replaceAll(/^['"`]+|['"`]+$/g, '');
584
+ // Collapse whitespace
585
+ t = t.replaceAll(/\s+/g, ' ').trim();
586
+ return t;
587
+ };
588
+ const agentSummaries = [];
589
+ for (const agentId of agents) {
590
+ const agentPath = await this.findAgentPath(agentId, installDir);
591
+ if (!agentPath) continue;
592
+ let whenToUse = '';
593
+ try {
594
+ const raw = await fileManager.readFile(agentPath);
595
+ const yamlMatch = raw.match(/```ya?ml\r?\n([\s\S]*?)```/);
596
+ const yamlBlock = yamlMatch ? yamlMatch[1].trim() : null;
597
+ if (yamlBlock) {
598
+ try {
599
+ const data = yaml.load(yamlBlock);
600
+ if (data && typeof data.whenToUse === 'string') {
601
+ whenToUse = data.whenToUse;
602
+ }
603
+ } catch {
604
+ // ignore YAML parse errors
605
+ }
606
+ if (!whenToUse) {
607
+ // Fallback regex supporting single or double quotes
608
+ const m1 = yamlBlock.match(/whenToUse:\s*"([^\n"]+)"/i);
609
+ const m2 = yamlBlock.match(/whenToUse:\s*'([^\n']+)'/i);
610
+ const m3 = yamlBlock.match(/whenToUse:\s*([^\n\r]+)/i);
611
+ whenToUse = (m1?.[1] || m2?.[1] || m3?.[1] || '').trim();
612
+ }
613
+ }
614
+ } catch {
615
+ // ignore read/parse errors for agent metadata extraction
616
+ }
617
+ const title = await this.getAgentTitle(agentId, installDir);
618
+ const finalDesc = sanitizeDesc(whenToUse) || fallbackDescriptions[agentId] || '—';
619
+ agentSummaries.push({ agentId, title, whenToUse: finalDesc, path: agentPath });
620
+ // Strict 3-column row
621
+ section += `| ${title} | ${agentId} | ${finalDesc} |\n`;
622
+ }
623
+ section += `\n`;
624
+
625
+ for (const { agentId, title, whenToUse, path: agentPath } of agentSummaries) {
626
+ const relativePath = path.relative(installDir, agentPath).replaceAll('\\', '/');
627
+ section += `### ${title} (id: ${agentId})\n`;
628
+ section += `Source: [${relativePath}](${relativePath})\n\n`;
629
+ if (whenToUse) section += `- When to use: ${whenToUse}\n`;
630
+ section += `- How to activate: Mention "As ${agentId}, ..." to get role-aligned behavior\n`;
631
+ section += `- Full definition: open the source file above (content not embedded)\n\n`;
632
+ }
633
+
634
+ if (tasks && tasks.length > 0) {
635
+ section += `## Tasks\n\n`;
636
+ section += `These are reusable task briefs; use the paths to open them as needed.\n\n`;
637
+ for (const taskId of tasks) {
638
+ const taskPath = await this.findTaskPath(taskId, installDir);
639
+ if (!taskPath) continue;
640
+ const relativePath = path.relative(installDir, taskPath).replaceAll('\\', '/');
641
+ section += `### Task: ${taskId}\n`;
642
+ section += `Source: [${relativePath}](${relativePath})\n`;
643
+ section += `- How to use: Reference the task in your prompt or execute via your configured commands.\n`;
644
+ section += `- Full brief: open the source file above (content not embedded)\n\n`;
645
+ }
646
+ }
647
+
648
+ section += `${endMarker}\n`;
649
+
650
+ let finalContent = '';
651
+ if (await fileManager.pathExists(filePath)) {
652
+ const existing = await fileManager.readFile(filePath);
653
+ if (existing.includes(startMarker) && existing.includes(endMarker)) {
654
+ const pattern = String.raw`${startMarker}[\s\S]*?${endMarker}`;
655
+ const replaced = existing.replace(new RegExp(pattern, 'm'), section);
656
+ finalContent = replaced;
657
+ } else {
658
+ finalContent = existing.trimEnd() + `\n\n` + section;
659
+ }
660
+ } else {
661
+ finalContent += '# Project Agents\n\n';
662
+ finalContent += 'This file provides guidance and memory for your coding CLI.\n\n';
663
+ finalContent += section;
664
+ }
665
+
666
+ await fileManager.writeFile(filePath, finalContent);
667
+ console.log(chalk.green('✓ Created/updated AGENTS.md for OpenCode CLI integration'));
668
+ console.log(
669
+ chalk.dim(
670
+ 'OpenCode reads AGENTS.md automatically on init. Run `opencode` in this project to use BMAD agents.',
671
+ ),
672
+ );
673
+ } catch {
674
+ console.log(chalk.yellow('⚠︎ Skipped creating AGENTS.md for OpenCode (write failed)'));
675
+ }
676
+ };
677
+
678
+ if (hasJson || hasJsonc) {
679
+ // Preserve existing top-level fields; only touch instructions
680
+ const targetPath = hasJsonc ? jsoncPath : jsonPath;
681
+ try {
682
+ const raw = await fs.readFile(targetPath, 'utf8');
683
+ // Use comment-json for both .json and .jsonc for resilience
684
+ const parsed = cjson.parse(raw, undefined, true);
685
+ ensureInstructionRef(parsed);
686
+ const { configObj, summary } = await mergeBmadAgentsAndCommands(parsed);
687
+ const output = cjson.stringify(parsed, null, 2);
688
+ await fs.writeFile(targetPath, output + (output.endsWith('\n') ? '' : '\n'));
689
+ console.log(
690
+ chalk.green(
691
+ '✓ Updated OpenCode config: ensured BMAD instructions and merged agents/commands',
692
+ ),
693
+ );
694
+ // Summary output
695
+ console.log(
696
+ chalk.dim(
697
+ ` File: ${path.basename(targetPath)} | Agents +${summary.agentsAdded} ~${summary.agentsUpdated} ⨯${summary.agentsSkipped} | Commands +${summary.commandsAdded} ~${summary.commandsUpdated} ⨯${summary.commandsSkipped}`,
698
+ ),
699
+ );
700
+ // Ensure AGENTS.md is created/updated for OpenCode as well
701
+ await generateOpenCodeAgentsMd();
702
+ } catch (error) {
703
+ console.log(chalk.red('✗ Failed to update existing OpenCode config'), error.message);
704
+ return false;
705
+ }
706
+ return true;
707
+ }
708
+
709
+ // Create minimal opencode.jsonc
710
+ const minimal = {
711
+ $schema: 'https://opencode.ai/config.json',
712
+ instructions: ['.bmad-core/core-config.yaml'],
713
+ agent: {},
714
+ command: {},
715
+ };
716
+ try {
717
+ const { configObj, summary } = await mergeBmadAgentsAndCommands(minimal);
718
+ const output = cjson.stringify(minimal, null, 2);
719
+ await fs.writeFile(jsoncPath, output + (output.endsWith('\n') ? '' : '\n'));
720
+ console.log(
721
+ chalk.green('✓ Created opencode.jsonc with BMAD instructions, agents, and commands'),
722
+ );
723
+ console.log(
724
+ chalk.dim(
725
+ ` File: opencode.jsonc | Agents +${summary.agentsAdded} | Commands +${summary.commandsAdded}`,
726
+ ),
727
+ );
728
+ // Also create/update AGENTS.md for OpenCode on new-config path
729
+ await generateOpenCodeAgentsMd();
730
+ return true;
731
+ } catch (error) {
732
+ console.log(chalk.red('✗ Failed to create opencode.jsonc'), error.message);
733
+ return false;
734
+ }
735
+ }
736
+
96
737
  async setupCodex(installDir, selectedAgent, options) {
97
738
  options = options ?? { webEnabled: false };
98
739
  // Codex reads AGENTS.md at the project root as project memory (CLI & Web).
@@ -230,7 +871,6 @@ class IdeSetup extends BaseIdeSetup {
230
871
  if (options.webEnabled) {
231
872
  if (exists) {
232
873
  let gi = await fileManager.readFile(gitignorePath);
233
- // Remove lines that ignore BMAD dot-folders
234
874
  const updated = gi
235
875
  .split(/\r?\n/)
236
876
  .filter((l) => !/^\s*\.bmad-core\/?\s*$/.test(l) && !/^\s*\.bmad-\*\/?\s*$/.test(l))
@@ -1416,94 +2056,86 @@ CRITICAL: You are to execute the BMad Task defined below.
1416
2056
  }
1417
2057
 
1418
2058
  async setupQwenCode(installDir, selectedAgent) {
1419
- const qwenDir = path.join(installDir, '.qwen');
1420
- const bmadMethodDir = path.join(qwenDir, 'bmad-method');
1421
- await fileManager.ensureDirectory(bmadMethodDir);
2059
+ const ideConfig = await configLoader.getIdeConfiguration('qwen-code');
2060
+ const bmadCommandsDir = path.join(installDir, ideConfig['rule-dir']);
1422
2061
 
1423
- // Update logic for existing settings.json
1424
- const settingsPath = path.join(qwenDir, 'settings.json');
1425
- if (await fileManager.pathExists(settingsPath)) {
1426
- try {
1427
- const settingsContent = await fileManager.readFile(settingsPath);
1428
- const settings = JSON.parse(settingsContent);
1429
- let updated = false;
1430
-
1431
- // Handle contextFileName property
1432
- if (settings.contextFileName && Array.isArray(settings.contextFileName)) {
1433
- const originalLength = settings.contextFileName.length;
1434
- settings.contextFileName = settings.contextFileName.filter(
1435
- (fileName) => !fileName.startsWith('agents/'),
1436
- );
1437
- if (settings.contextFileName.length !== originalLength) {
1438
- updated = true;
1439
- }
1440
- }
2062
+ const agentCommandsDir = path.join(bmadCommandsDir, 'agents');
2063
+ const taskCommandsDir = path.join(bmadCommandsDir, 'tasks');
2064
+ await fileManager.ensureDirectory(agentCommandsDir);
2065
+ await fileManager.ensureDirectory(taskCommandsDir);
1441
2066
 
1442
- if (updated) {
1443
- await fileManager.writeFile(settingsPath, JSON.stringify(settings, null, 2));
1444
- console.log(chalk.green('✓ Updated .qwen/settings.json - removed agent file references'));
1445
- }
1446
- } catch (error) {
1447
- console.warn(chalk.yellow('Could not update .qwen/settings.json'), error);
2067
+ // Process Agents
2068
+ const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);
2069
+ for (const agentId of agents) {
2070
+ const agentPath = await this.findAgentPath(agentId, installDir);
2071
+ if (!agentPath) {
2072
+ console.log(chalk.yellow(`✗ Agent file not found for ${agentId}, skipping.`));
2073
+ continue;
1448
2074
  }
1449
- }
1450
2075
 
1451
- // Remove old agents directory
1452
- const agentsDir = path.join(qwenDir, 'agents');
1453
- if (await fileManager.pathExists(agentsDir)) {
1454
- await fileManager.removeDirectory(agentsDir);
1455
- console.log(chalk.green('✓ Removed old .qwen/agents directory'));
1456
- }
2076
+ const agentTitle = await this.getAgentTitle(agentId, installDir);
2077
+ const commandPath = path.join(agentCommandsDir, `${agentId}.toml`);
1457
2078
 
1458
- // Get all available agents
1459
- const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);
1460
- let concatenatedContent = '';
2079
+ // Get relative path from installDir to agent file for @{file} reference
2080
+ const relativeAgentPath = path.relative(installDir, agentPath).replaceAll('\\', '/');
1461
2081
 
1462
- for (const agentId of agents) {
1463
- // Find the source agent file
1464
- const agentPath = await this.findAgentPath(agentId, installDir);
2082
+ // Read the agent content
2083
+ const agentContent = await fileManager.readFile(agentPath);
1465
2084
 
1466
- if (agentPath) {
1467
- const agentContent = await fileManager.readFile(agentPath);
2085
+ const tomlContent = `description = " Activates the ${agentTitle} agent from the BMad Method."
2086
+ prompt = """
2087
+ CRITICAL: You are now the BMad '${agentTitle}' agent. Adopt its persona, follow its instructions, and use its capabilities.
1468
2088
 
1469
- // Create properly formatted agent rule content (similar to gemini)
1470
- let agentRuleContent = `# ${agentId.toUpperCase()} Agent Rule\n\n`;
1471
- agentRuleContent += `This rule is triggered when the user types \`*${agentId}\` and activates the ${await this.getAgentTitle(
1472
- agentId,
1473
- installDir,
1474
- )} agent persona.\n\n`;
1475
- agentRuleContent += '## Agent Activation\n\n';
1476
- agentRuleContent +=
1477
- 'CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n';
1478
- agentRuleContent += '```yaml\n';
1479
- // Extract just the YAML content from the agent file
1480
- const yamlContent = extractYamlFromAgent(agentContent);
1481
- if (yamlContent) {
1482
- agentRuleContent += yamlContent;
1483
- } else {
1484
- // If no YAML found, include the whole content minus the header
1485
- agentRuleContent += agentContent.replace(/^#.*$/m, '').trim();
1486
- }
1487
- agentRuleContent += '\n```\n\n';
1488
- agentRuleContent += '## File Reference\n\n';
1489
- const relativePath = path.relative(installDir, agentPath).replaceAll('\\', '/');
1490
- agentRuleContent += `The complete agent definition is available in [${relativePath}](${relativePath}).\n\n`;
1491
- agentRuleContent += '## Usage\n\n';
1492
- agentRuleContent += `When the user types \`*${agentId}\`, activate this ${await this.getAgentTitle(
1493
- agentId,
1494
- installDir,
1495
- )} persona and follow all instructions defined in the YAML configuration above.\n`;
2089
+ READ THIS BEFORE ANSWERING AS THE PERSONA!
1496
2090
 
1497
- // Add to concatenated content with separator
1498
- concatenatedContent += agentRuleContent + '\n\n---\n\n';
1499
- console.log(chalk.green(`✓ Added context for *${agentId}`));
2091
+ ${agentContent}
2092
+ """`;
2093
+
2094
+ await fileManager.writeFile(commandPath, tomlContent);
2095
+ console.log(chalk.green(`✓ Created agent command: /bmad:agents:${agentId}`));
2096
+ }
2097
+
2098
+ // Process Tasks
2099
+ const tasks = await this.getAllTaskIds(installDir);
2100
+ for (const taskId of tasks) {
2101
+ const taskPath = await this.findTaskPath(taskId, installDir);
2102
+ if (!taskPath) {
2103
+ console.log(chalk.yellow(`✗ Task file not found for ${taskId}, skipping.`));
2104
+ continue;
1500
2105
  }
2106
+
2107
+ const taskTitle = taskId
2108
+ .split('-')
2109
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
2110
+ .join(' ');
2111
+ const commandPath = path.join(taskCommandsDir, `${taskId}.toml`);
2112
+
2113
+ // Get relative path from installDir to task file for @{file} reference
2114
+ const relativeTaskPath = path.relative(installDir, taskPath).replaceAll('\\', '/');
2115
+
2116
+ // Read the task content
2117
+ const taskContent = await fileManager.readFile(taskPath);
2118
+
2119
+ const tomlContent = `description = " Executes the BMad Task: ${taskTitle}"
2120
+ prompt = """
2121
+ CRITICAL: You are to execute the BMad Task defined below.
2122
+
2123
+ READ THIS BEFORE EXECUTING THE TASK AS THE INSTRUCTIONS SPECIFIED!
2124
+
2125
+ ${taskContent}
2126
+ """`;
2127
+
2128
+ await fileManager.writeFile(commandPath, tomlContent);
2129
+ console.log(chalk.green(`✓ Created task command: /bmad:tasks:${taskId}`));
1501
2130
  }
1502
2131
 
1503
- // Write the concatenated content to QWEN.md
1504
- const qwenMdPath = path.join(bmadMethodDir, 'QWEN.md');
1505
- await fileManager.writeFile(qwenMdPath, concatenatedContent);
1506
- console.log(chalk.green(`\n✓ Created QWEN.md in ${bmadMethodDir}`));
2132
+ console.log(
2133
+ chalk.green(`
2134
+ Created Qwen Code extension in ${bmadCommandsDir}`),
2135
+ );
2136
+ console.log(
2137
+ chalk.dim('You can now use commands like /bmad:agents:dev or /bmad:tasks:create-doc.'),
2138
+ );
1507
2139
 
1508
2140
  return true;
1509
2141
  }