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.
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/workflows/manual-release.yaml +1 -1
- package/PR-opencode-agents-generator.md +40 -0
- package/README.md +38 -44
- package/bmad-core/agents/analyst.md +1 -1
- package/bmad-core/agents/architect.md +1 -1
- package/bmad-core/agents/bmad-master.md +7 -7
- package/bmad-core/agents/bmad-orchestrator.md +2 -2
- package/bmad-core/agents/dev.md +1 -1
- package/bmad-core/agents/pm.md +1 -1
- package/bmad-core/agents/po.md +1 -1
- package/bmad-core/agents/qa.md +2 -6
- package/bmad-core/agents/sm.md +1 -1
- package/bmad-core/agents/ux-expert.md +1 -1
- package/bmad-core/checklists/po-master-checklist.md +3 -3
- package/bmad-core/data/bmad-kb.md +1 -1
- package/bmad-core/tasks/apply-qa-fixes.md +4 -4
- package/bmad-core/tasks/nfr-assess.md +3 -3
- package/bmad-core/tasks/qa-gate.md +2 -2
- package/bmad-core/tasks/review-story.md +1 -1
- package/bmad-core/templates/brownfield-architecture-tmpl.yaml +6 -6
- package/dist/agents/analyst.txt +3 -11
- package/dist/agents/architect.txt +1 -7
- package/dist/agents/bmad-master.txt +7 -31
- package/dist/agents/bmad-orchestrator.txt +1 -8
- package/dist/agents/dev.txt +5 -10
- package/dist/agents/pm.txt +0 -10
- package/dist/agents/po.txt +4 -10
- package/dist/agents/qa.txt +7 -15
- package/dist/agents/sm.txt +0 -4
- package/dist/agents/ux-expert.txt +0 -4
- package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-designer.txt +0 -6
- package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-developer.txt +0 -3
- package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-sm.txt +0 -3
- package/dist/expansion-packs/bmad-2d-phaser-game-dev/teams/phaser-2d-nodejs-game-team.txt +2 -25
- package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-architect.txt +0 -9
- package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-designer.txt +0 -8
- package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-developer.txt +1 -4
- package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-sm.txt +0 -4
- package/dist/expansion-packs/bmad-2d-unity-game-dev/teams/unity-2d-game-team.txt +3 -35
- package/dist/expansion-packs/bmad-creative-writing/agents/beta-reader.txt +0 -9
- package/dist/expansion-packs/bmad-creative-writing/agents/character-psychologist.txt +0 -8
- package/dist/expansion-packs/bmad-creative-writing/agents/dialog-specialist.txt +0 -7
- package/dist/expansion-packs/bmad-creative-writing/agents/editor.txt +0 -8
- package/dist/expansion-packs/bmad-creative-writing/agents/genre-specialist.txt +0 -10
- package/dist/expansion-packs/bmad-creative-writing/agents/narrative-designer.txt +0 -8
- package/dist/expansion-packs/bmad-creative-writing/agents/plot-architect.txt +0 -7
- package/dist/expansion-packs/bmad-creative-writing/agents/world-builder.txt +0 -9
- package/dist/expansion-packs/bmad-creative-writing/teams/agent-team.txt +0 -85
- package/dist/expansion-packs/bmad-godot-game-dev/agents/game-developer.txt +2 -2
- package/dist/expansion-packs/bmad-godot-game-dev/agents/game-qa.txt +1 -2
- package/dist/expansion-packs/bmad-godot-game-dev/teams/godot-game-team.txt +5 -6
- package/dist/expansion-packs/bmad-infrastructure-devops/agents/infra-devops-platform.txt +0 -5
- package/dist/teams/team-all.txt +34 -69
- package/dist/teams/team-fullstack.txt +23 -46
- package/dist/teams/team-ide-minimal.txt +16 -42
- package/dist/teams/team-no-ui.txt +12 -34
- package/docs/user-guide.md +48 -1
- package/expansion-packs/bmad-godot-game-dev/agents/bmad-orchestrator.md +1 -1
- package/expansion-packs/bmad-godot-game-dev/agents/game-qa.md +1 -2
- package/expansion-packs/bmad-godot-game-dev/tasks/apply-qa-fixes.md +2 -2
- package/package.json +2 -1
- package/tools/installer/bin/bmad.js +46 -1
- package/tools/installer/config/install.config.yaml +18 -7
- package/tools/installer/lib/ide-setup.js +709 -77
- package/tools/installer/lib/installer.js +17 -4
- package/tools/installer/package.json +1 -1
- 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
|
|
1420
|
-
const
|
|
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
|
-
|
|
1424
|
-
const
|
|
1425
|
-
|
|
1426
|
-
|
|
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
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
console.
|
|
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
|
-
|
|
1452
|
-
|
|
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
|
-
|
|
1459
|
-
|
|
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
|
-
|
|
1463
|
-
|
|
1464
|
-
const agentPath = await this.findAgentPath(agentId, installDir);
|
|
2082
|
+
// Read the agent content
|
|
2083
|
+
const agentContent = await fileManager.readFile(agentPath);
|
|
1465
2084
|
|
|
1466
|
-
|
|
1467
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
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
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
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
|
}
|