ma-agents 3.13.0 → 3.13.2-skillname.0
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/bin/cli.js +67 -13
- package/lib/.bmad-extension-plugin.build-37364-1782300865129/.claude-plugin/marketplace.json +109 -0
- package/lib/.bmad-extension-plugin.build-37364-1782300865129/skills/module-help.csv +62 -0
- package/lib/.bmad-extension-plugin.build-37364-1782300865129/skills/module.yaml +20 -0
- package/lib/bmad-extension-plugin/.claude-plugin/marketplace.json +1 -1
- package/lib/bmad.js +270 -11
- package/lib/installer.js +276 -23
- package/lib/profile.js +96 -5
- package/lib/templates/instruction-block-git.template.md +25 -25
- package/lib/templates/instruction-block-onprem.template.md +86 -86
- package/lib/templates/instruction-block-universal.template.md +29 -29
- package/package.json +2 -2
- package/skills/git-workflow-skill/skill.json +21 -21
package/lib/installer.js
CHANGED
|
@@ -166,7 +166,12 @@ function composeInstructionBlock({ profile, projectRoot } = {}) {
|
|
|
166
166
|
composed += '\n\n' + onprem.replace(/\s+$/, '');
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
// Bug 27.10 — the source templates are authored with CRLF line endings, but the
|
|
170
|
+
// segment seams and terminator above are hand-written LF. That mix yields a block
|
|
171
|
+
// whose body lines are CRLF while the joins are LF. Normalize to pure LF once here
|
|
172
|
+
// so the composed block is EOL-consistent regardless of how templates were checked
|
|
173
|
+
// out on Windows, and so downstream marker-merge (which assumes LF) stays stable.
|
|
174
|
+
return composed.replace(/\r\n/g, '\n') + '\n';
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
/**
|
|
@@ -245,6 +250,17 @@ function resolveBmadOutputDirs(projectRoot) {
|
|
|
245
250
|
const CLAUDE_CODE_HOOK_ID = 'ma-agents-verify-manifest';
|
|
246
251
|
const CLAUDE_CODE_SETTINGS_FILE = '.claude/settings.json';
|
|
247
252
|
|
|
253
|
+
// Bug 27.8 — the SessionStart hook must point at a path that exists in a real
|
|
254
|
+
// (npx/node_modules) install, not at the package's own `lib/` which is never
|
|
255
|
+
// copied into the target. The dependency-free hook script is copied into the
|
|
256
|
+
// target's `.claude/hooks/` and the command resolves it via $CLAUDE_PROJECT_DIR
|
|
257
|
+
// (the target project root). The legacy `lib/hooks/...` command string is kept
|
|
258
|
+
// here only so existing installs get it removed/migrated on the next run.
|
|
259
|
+
const CLAUDE_CODE_HOOK_SOURCE = path.join(__dirname, 'hooks', 'claude-code', 'verify-manifest.js');
|
|
260
|
+
const CLAUDE_CODE_HOOK_TARGET_REL = '.claude/hooks/verify-manifest.js';
|
|
261
|
+
const CLAUDE_CODE_HOOK_COMMAND = `node "$CLAUDE_PROJECT_DIR/${CLAUDE_CODE_HOOK_TARGET_REL}"`;
|
|
262
|
+
const CLAUDE_CODE_HOOK_LEGACY_COMMAND = `node "$CLAUDE_PROJECT_DIR/lib/hooks/claude-code/verify-manifest.js"`;
|
|
263
|
+
|
|
248
264
|
function getPackageVersion() {
|
|
249
265
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
250
266
|
return pkg.version;
|
|
@@ -397,13 +413,91 @@ function ensurePluginStageIgnored(projectRoot) {
|
|
|
397
413
|
);
|
|
398
414
|
}
|
|
399
415
|
|
|
416
|
+
// Bug 27.11 — the installer writes the `_bmad/` module tree which is
|
|
417
|
+
// "Installer-managed. Regenerated on every install — treat as read-only."
|
|
418
|
+
// Leaving it tracked guarantees churn and merge conflicts on every reinstall.
|
|
419
|
+
// This sister-policy to PLUGIN_STAGE_PATTERNS ensures `_bmad/` is gitignored.
|
|
420
|
+
//
|
|
421
|
+
// IMPORTANT: this targets ONLY the regenerated `_bmad/` tree. The
|
|
422
|
+
// `_bmad-output/` directory holds tracked project knowledge and is
|
|
423
|
+
// deliberately kept OUT of .gitignore (see ensureBmadOutputTracked) — the
|
|
424
|
+
// patterns below never match it because git treats `_bmad/` (trailing-slash
|
|
425
|
+
// anchored) and `_bmad-output` as distinct entries.
|
|
426
|
+
const BMAD_DIR_NAME = '_bmad';
|
|
427
|
+
const BMAD_DIR_PATTERNS = [
|
|
428
|
+
BMAD_DIR_NAME, // _bmad
|
|
429
|
+
`${BMAD_DIR_NAME}/`, // _bmad/
|
|
430
|
+
`/${BMAD_DIR_NAME}`, // /_bmad
|
|
431
|
+
`/${BMAD_DIR_NAME}/`, // /_bmad/
|
|
432
|
+
];
|
|
433
|
+
const BMAD_DIR_CANONICAL_ENTRY = `${BMAD_DIR_NAME}/`;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Ensure `_bmad/` is present in the target project's `.gitignore`. Mirrors the
|
|
437
|
+
* append-only, idempotent, EOL-preserving contract of `ensurePluginStageIgnored`.
|
|
438
|
+
*
|
|
439
|
+
* - If `.gitignore` is absent, create it with the single canonical entry.
|
|
440
|
+
* - If any active (non-comment) line already matches any BMAD_DIR_PATTERNS
|
|
441
|
+
* variant, do nothing.
|
|
442
|
+
* - Otherwise, append `BMAD_DIR_CANONICAL_ENTRY` preserving existing EOL style.
|
|
443
|
+
*
|
|
444
|
+
* Defensive: no-op on falsy/non-string projectRoot, matching the sister helper.
|
|
445
|
+
*
|
|
446
|
+
* @param {string} projectRoot - Absolute path to the target project root.
|
|
447
|
+
*/
|
|
448
|
+
function ensureBmadDirIgnored(projectRoot) {
|
|
449
|
+
if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
454
|
+
|
|
455
|
+
let content = '';
|
|
456
|
+
let fileExists = true;
|
|
457
|
+
try {
|
|
458
|
+
content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
459
|
+
} catch (err) {
|
|
460
|
+
if (err.code !== 'ENOENT') throw err;
|
|
461
|
+
fileExists = false;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (fileExists) {
|
|
465
|
+
const alreadyPresent = content.split(/\r?\n/).some(rawLine => {
|
|
466
|
+
const line = rawLine.trim();
|
|
467
|
+
if (!line || line.startsWith('#')) return false; // skip blanks & comments
|
|
468
|
+
return BMAD_DIR_PATTERNS.includes(line);
|
|
469
|
+
});
|
|
470
|
+
if (alreadyPresent) return; // idempotent no-op
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const usesCrlf = fileExists && /\r\n/.test(content);
|
|
474
|
+
const eol = usesCrlf ? '\r\n' : '\n';
|
|
475
|
+
|
|
476
|
+
let next = content;
|
|
477
|
+
if (next && !next.endsWith('\n') && !next.endsWith('\r\n')) {
|
|
478
|
+
next += eol; // ensure the append lands on its own line
|
|
479
|
+
}
|
|
480
|
+
next += BMAD_DIR_CANONICAL_ENTRY + eol;
|
|
481
|
+
|
|
482
|
+
fs.writeFileSync(gitignorePath, next, 'utf-8');
|
|
483
|
+
console.log(
|
|
484
|
+
chalk.green(
|
|
485
|
+
`${BMAD_DIR_CANONICAL_ENTRY} added to .gitignore (installer-managed BMAD tree — regenerated on every install)`
|
|
486
|
+
)
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
400
490
|
function ensureManifest(installPath, agentId, scope) {
|
|
401
491
|
let manifest = readManifest(installPath);
|
|
402
492
|
if (!manifest) {
|
|
493
|
+
// Bug 27.9 — when bootstrapped before any agent is known (e.g. profile
|
|
494
|
+
// bootstrap via setProfile → ensureManifest(root, null, 'project')), agentId
|
|
495
|
+
// is null. Mirror the migrate-branch guard so we never write the malformed
|
|
496
|
+
// `agent: null` / `agents: [null]` shape; an empty agents array is valid.
|
|
403
497
|
manifest = {
|
|
404
498
|
manifestVersion: MANIFEST_VERSION,
|
|
405
|
-
agent: agentId,
|
|
406
|
-
agents: [agentId],
|
|
499
|
+
agent: agentId || null,
|
|
500
|
+
agents: agentId ? [agentId] : [],
|
|
407
501
|
scope: scope,
|
|
408
502
|
skills: {}
|
|
409
503
|
};
|
|
@@ -1228,10 +1322,14 @@ async function performInstall(skillId, skill, agent, installPath) {
|
|
|
1228
1322
|
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
|
1229
1323
|
content = content.replace(frontmatterRegex, '');
|
|
1230
1324
|
|
|
1231
|
-
// Inject YAML frontmatter
|
|
1325
|
+
// Inject YAML frontmatter. The Agent Skills spec (and strict validators such
|
|
1326
|
+
// as cline) require the frontmatter `name` to be the kebab-case skill id that
|
|
1327
|
+
// matches the SKILL.md's containing directory — NOT the human-readable display
|
|
1328
|
+
// name. We therefore inject `skillId` (the directory basename) here and keep
|
|
1329
|
+
// `skill.name` from skill.json as the display name used by listings/wizard UI.
|
|
1232
1330
|
const frontmatter = [
|
|
1233
1331
|
'---',
|
|
1234
|
-
`name: ${
|
|
1332
|
+
`name: ${skillId}`,
|
|
1235
1333
|
`description: ${skill.description}`,
|
|
1236
1334
|
'---',
|
|
1237
1335
|
''
|
|
@@ -1265,6 +1363,116 @@ async function performInstall(skillId, skill, agent, installPath) {
|
|
|
1265
1363
|
return true;
|
|
1266
1364
|
}
|
|
1267
1365
|
|
|
1366
|
+
// --- Bug 27.16 — .agents/skills pipeline-consistency reconciliation ----------
|
|
1367
|
+
//
|
|
1368
|
+
// Two pipelines write skills: ma-agents' own standalone loop (installSkill →
|
|
1369
|
+
// performInstall + generateSkillsManifest — the ONLY writer of MANIFEST.yaml),
|
|
1370
|
+
// and bmad-method's PluginResolver (via --custom-source) which routes
|
|
1371
|
+
// copilot/roo/kilo to the shared `.agents/skills/` dir. The standalone loop only
|
|
1372
|
+
// targets `.agents/skills` when a copilot/kilocode/roo-code agent is selected.
|
|
1373
|
+
//
|
|
1374
|
+
// When the user selects only claude-code/cline/opencode, BMAD still writes the
|
|
1375
|
+
// BMAD-family skills into `.agents/skills` (175 of them in the Workshop repro)
|
|
1376
|
+
// but the standalone catalog and MANIFEST.yaml are never written there — leaving
|
|
1377
|
+
// a dir that looks installed but is silently partial AND missing the
|
|
1378
|
+
// MANIFEST.yaml the stamped instruction block points agents at.
|
|
1379
|
+
//
|
|
1380
|
+
// This pass makes `.agents/skills` pipeline-consistent: if BMAD populated it but
|
|
1381
|
+
// no `.agents`-targeting agent was selected (so the standalone loop skipped it),
|
|
1382
|
+
// install the full standalone catalog there and generate MANIFEST.yaml. The set
|
|
1383
|
+
// of `.agents`-targeting agent ids is derived from agents whose project skills
|
|
1384
|
+
// dir resolves to `.agents/skills`.
|
|
1385
|
+
|
|
1386
|
+
const AGENTS_SHARED_SKILLS_DIRNAME = '.agents';
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* Return the list of agent ids whose project-level skills dir is the shared
|
|
1390
|
+
* `.agents/skills/` directory (copilot, kilocode, roo-code). Derived from the
|
|
1391
|
+
* agent registry's `skillsDir` so a future registry change surfaces here.
|
|
1392
|
+
*/
|
|
1393
|
+
function getAgentsSharedSkillAgentIds() {
|
|
1394
|
+
return listAgents()
|
|
1395
|
+
.filter(a => a.skillsDir === path.join(AGENTS_SHARED_SKILLS_DIRNAME, 'skills') ||
|
|
1396
|
+
a.skillsDir === `${AGENTS_SHARED_SKILLS_DIRNAME}/skills`)
|
|
1397
|
+
.map(a => a.id);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* Bug 27.16 — reconcile a BMAD-populated `.agents/skills` directory that the
|
|
1402
|
+
* standalone loop never targeted. No-op unless ALL of:
|
|
1403
|
+
* - scope === 'project'
|
|
1404
|
+
* - `<projectRoot>/.agents/skills` exists on disk (BMAD created it)
|
|
1405
|
+
* - no `.agents`-targeting agent was among the selected agents
|
|
1406
|
+
* - the dir has no MANIFEST.yaml (the standalone-loop signature)
|
|
1407
|
+
*
|
|
1408
|
+
* When triggered, installs the full standalone catalog into `.agents/skills`
|
|
1409
|
+
* (using a representative `.agents`-targeting agent for template resolution),
|
|
1410
|
+
* records the skills into that dir's `.ma-agents.json`, and generates
|
|
1411
|
+
* MANIFEST.yaml — so the dir is never BMAD-only without a manifest.
|
|
1412
|
+
*
|
|
1413
|
+
* Idempotent: once MANIFEST.yaml exists the early-return fires.
|
|
1414
|
+
*
|
|
1415
|
+
* @param {string} projectRoot
|
|
1416
|
+
* @param {string[]} selectedAgentIds - agent ids the user selected this run
|
|
1417
|
+
* @param {string} scope - 'project' | 'global'
|
|
1418
|
+
* @returns {Promise<{reconciled: boolean, installed: number}>}
|
|
1419
|
+
*/
|
|
1420
|
+
async function reconcileAgentsSharedSkills(projectRoot, selectedAgentIds = [], scope = 'project') {
|
|
1421
|
+
const result = { reconciled: false, installed: 0 };
|
|
1422
|
+
if (scope !== 'project') return result;
|
|
1423
|
+
if (typeof projectRoot !== 'string' || !projectRoot) return result;
|
|
1424
|
+
|
|
1425
|
+
const sharedDir = path.join(projectRoot, AGENTS_SHARED_SKILLS_DIRNAME, 'skills');
|
|
1426
|
+
if (!fs.existsSync(sharedDir)) return result; // BMAD never populated it
|
|
1427
|
+
|
|
1428
|
+
const sharedAgentIds = getAgentsSharedSkillAgentIds();
|
|
1429
|
+
const selectedSet = new Set(selectedAgentIds || []);
|
|
1430
|
+
const anySharedSelected = sharedAgentIds.some(id => selectedSet.has(id));
|
|
1431
|
+
if (anySharedSelected) return result; // standalone loop already targeted it
|
|
1432
|
+
|
|
1433
|
+
// If a MANIFEST.yaml already exists, the dir is not standalone-orphaned.
|
|
1434
|
+
if (fs.existsSync(path.join(sharedDir, 'MANIFEST.yaml'))) return result;
|
|
1435
|
+
|
|
1436
|
+
// Pick a representative `.agents`-targeting agent for template resolution.
|
|
1437
|
+
const repAgent = sharedAgentIds.map(id => getAgent(id)).find(Boolean);
|
|
1438
|
+
if (!repAgent) return result; // registry has no such agent — nothing to do
|
|
1439
|
+
|
|
1440
|
+
console.log(chalk.yellow(
|
|
1441
|
+
` Note: ${sharedDir} was populated by BMAD but carries no MANIFEST.yaml ` +
|
|
1442
|
+
`(no .agents-targeting agent selected) — backfilling the standalone catalog (bug 27.16).`
|
|
1443
|
+
));
|
|
1444
|
+
|
|
1445
|
+
const skills = listSkills();
|
|
1446
|
+
const manifest = ensureManifest(sharedDir, repAgent.id, scope);
|
|
1447
|
+
const now = new Date().toISOString();
|
|
1448
|
+
|
|
1449
|
+
for (const skill of skills) {
|
|
1450
|
+
try {
|
|
1451
|
+
const ok = await performInstall(skill.id, skill, repAgent, sharedDir);
|
|
1452
|
+
if (!ok) continue;
|
|
1453
|
+
const existing = manifest.skills[skill.id];
|
|
1454
|
+
manifest.skills[skill.id] = {
|
|
1455
|
+
version: skill.version,
|
|
1456
|
+
installedAt: existing ? existing.installedAt : now,
|
|
1457
|
+
updatedAt: now,
|
|
1458
|
+
installerVersion: getPackageVersion(),
|
|
1459
|
+
agentVersion: repAgent.version
|
|
1460
|
+
};
|
|
1461
|
+
result.installed++;
|
|
1462
|
+
} catch (err) {
|
|
1463
|
+
console.log(chalk.yellow(` x ${skill.id} skipped in .agents/skills backfill: ${err.message}`));
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
writeManifest(sharedDir, manifest);
|
|
1468
|
+
await generateSkillsManifest(sharedDir);
|
|
1469
|
+
result.reconciled = true;
|
|
1470
|
+
console.log(chalk.green(
|
|
1471
|
+
` Backfilled ${result.installed} standalone skill(s) + MANIFEST.yaml into ${sharedDir}`
|
|
1472
|
+
));
|
|
1473
|
+
return result;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1268
1476
|
// --- Install with upgrade detection ---
|
|
1269
1477
|
|
|
1270
1478
|
async function installSkill(skillId, agentIds, customPath = '', scope = 'project', options = {}) {
|
|
@@ -1882,7 +2090,12 @@ function getStatus(agentIds, customPath = '', scope = 'project') {
|
|
|
1882
2090
|
|
|
1883
2091
|
for (const { path: checkPath, scope: checkScope } of pathsToCheck) {
|
|
1884
2092
|
const manifest = readManifest(checkPath);
|
|
1885
|
-
|
|
2093
|
+
// Bug 27.12 — never treat the PROFILE-ONLY project-root manifest as a
|
|
2094
|
+
// skill inventory. The per-agent skills manifests are authoritative; a
|
|
2095
|
+
// manifest carrying the `project-root` scope discriminator is a
|
|
2096
|
+
// profile/settings store and must be skipped here.
|
|
2097
|
+
if (!manifest || manifest.scope === 'project-root' ||
|
|
2098
|
+
!manifest.skills || Object.keys(manifest.skills).length === 0) {
|
|
1886
2099
|
continue;
|
|
1887
2100
|
}
|
|
1888
2101
|
|
|
@@ -1924,27 +2137,49 @@ async function deployClaudeCodeHook(projectRoot) {
|
|
|
1924
2137
|
settings.hooks.SessionStart = [];
|
|
1925
2138
|
}
|
|
1926
2139
|
|
|
1927
|
-
//
|
|
1928
|
-
|
|
2140
|
+
// Bug 27.8 — copy the self-contained hook into the target so the command
|
|
2141
|
+
// resolves in any install (npx/node_modules/dev-repo). The legacy command
|
|
2142
|
+
// string (which pointed at the never-copied package `lib/`) is migrated to
|
|
2143
|
+
// the new command on every run.
|
|
2144
|
+
const hookTargetPath = path.join(projectRoot, CLAUDE_CODE_HOOK_TARGET_REL);
|
|
2145
|
+
await fs.ensureDir(path.dirname(hookTargetPath));
|
|
2146
|
+
await fs.copy(CLAUDE_CODE_HOOK_SOURCE, hookTargetPath, { overwrite: true });
|
|
2147
|
+
|
|
2148
|
+
const hookCommand = CLAUDE_CODE_HOOK_COMMAND;
|
|
2149
|
+
let migrated = false;
|
|
2150
|
+
for (const group of settings.hooks.SessionStart) {
|
|
2151
|
+
if (!group.hooks) continue;
|
|
2152
|
+
for (const h of group.hooks) {
|
|
2153
|
+
if (h.command === CLAUDE_CODE_HOOK_LEGACY_COMMAND || h._id === CLAUDE_CODE_HOOK_ID) {
|
|
2154
|
+
h.command = hookCommand;
|
|
2155
|
+
h._id = CLAUDE_CODE_HOOK_ID;
|
|
2156
|
+
migrated = true;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
|
|
1929
2161
|
const alreadyInstalled = settings.hooks.SessionStart.some(group =>
|
|
1930
2162
|
group.hooks && group.hooks.some(h => h.command === hookCommand)
|
|
1931
2163
|
);
|
|
1932
2164
|
|
|
1933
|
-
if (alreadyInstalled) {
|
|
1934
|
-
|
|
2165
|
+
if (!alreadyInstalled) {
|
|
2166
|
+
settings.hooks.SessionStart.push({
|
|
2167
|
+
matcher: 'startup',
|
|
2168
|
+
hooks: [
|
|
2169
|
+
{
|
|
2170
|
+
type: 'command',
|
|
2171
|
+
command: hookCommand,
|
|
2172
|
+
_id: CLAUDE_CODE_HOOK_ID
|
|
2173
|
+
}
|
|
2174
|
+
]
|
|
2175
|
+
});
|
|
2176
|
+
} else if (!migrated) {
|
|
2177
|
+
// Hook entry already current and file copied — still ensure settings exist.
|
|
2178
|
+
await fs.ensureDir(path.dirname(settingsPath));
|
|
2179
|
+
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
2180
|
+
return;
|
|
1935
2181
|
}
|
|
1936
2182
|
|
|
1937
|
-
settings.hooks.SessionStart.push({
|
|
1938
|
-
matcher: 'startup',
|
|
1939
|
-
hooks: [
|
|
1940
|
-
{
|
|
1941
|
-
type: 'command',
|
|
1942
|
-
command: hookCommand,
|
|
1943
|
-
_id: CLAUDE_CODE_HOOK_ID
|
|
1944
|
-
}
|
|
1945
|
-
]
|
|
1946
|
-
});
|
|
1947
|
-
|
|
1948
2183
|
await fs.ensureDir(path.dirname(settingsPath));
|
|
1949
2184
|
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
1950
2185
|
console.log(chalk.cyan(' + Deployed Claude Code verify-manifest hook'));
|
|
@@ -1956,6 +2191,15 @@ async function deployClaudeCodeHook(projectRoot) {
|
|
|
1956
2191
|
async function removeClaudeCodeHook(projectRoot) {
|
|
1957
2192
|
const settingsPath = path.join(projectRoot, CLAUDE_CODE_SETTINGS_FILE);
|
|
1958
2193
|
|
|
2194
|
+
// Bug 27.8 — remove the copied hook file first, before any settings.json
|
|
2195
|
+
// early-return, so a missing/corrupt settings.json never orphans the file.
|
|
2196
|
+
const hookTargetPath = path.join(projectRoot, CLAUDE_CODE_HOOK_TARGET_REL);
|
|
2197
|
+
try {
|
|
2198
|
+
await fs.remove(hookTargetPath);
|
|
2199
|
+
} catch {
|
|
2200
|
+
// best-effort cleanup — never fail uninstall over a missing file
|
|
2201
|
+
}
|
|
2202
|
+
|
|
1959
2203
|
if (!fs.existsSync(settingsPath)) return;
|
|
1960
2204
|
|
|
1961
2205
|
let settings;
|
|
@@ -1967,10 +2211,15 @@ async function removeClaudeCodeHook(projectRoot) {
|
|
|
1967
2211
|
|
|
1968
2212
|
if (!settings.hooks || !settings.hooks.SessionStart) return;
|
|
1969
2213
|
|
|
1970
|
-
|
|
2214
|
+
// Match both the current command and the legacy package-relative command so
|
|
2215
|
+
// older installs get cleaned up too.
|
|
1971
2216
|
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(group => {
|
|
1972
2217
|
if (!group.hooks) return true;
|
|
1973
|
-
group.hooks = group.hooks.filter(h =>
|
|
2218
|
+
group.hooks = group.hooks.filter(h =>
|
|
2219
|
+
h.command !== CLAUDE_CODE_HOOK_COMMAND &&
|
|
2220
|
+
h.command !== CLAUDE_CODE_HOOK_LEGACY_COMMAND &&
|
|
2221
|
+
h._id !== CLAUDE_CODE_HOOK_ID
|
|
2222
|
+
);
|
|
1974
2223
|
return group.hooks.length > 0;
|
|
1975
2224
|
});
|
|
1976
2225
|
|
|
@@ -1997,6 +2246,7 @@ module.exports = {
|
|
|
1997
2246
|
listSkills,
|
|
1998
2247
|
listAgents,
|
|
1999
2248
|
installSkill,
|
|
2249
|
+
performInstall,
|
|
2000
2250
|
uninstallSkill,
|
|
2001
2251
|
getStatus,
|
|
2002
2252
|
readManifest,
|
|
@@ -2009,6 +2259,9 @@ module.exports = {
|
|
|
2009
2259
|
removeClaudeCodeHook,
|
|
2010
2260
|
ensureBmadOutputTracked,
|
|
2011
2261
|
ensurePluginStageIgnored,
|
|
2262
|
+
ensureBmadDirIgnored,
|
|
2263
|
+
reconcileAgentsSharedSkills,
|
|
2264
|
+
getAgentsSharedSkillAgentIds,
|
|
2012
2265
|
generateSkillsManifest,
|
|
2013
2266
|
generateProjectContext,
|
|
2014
2267
|
generateRepoLayoutSection,
|
package/lib/profile.js
CHANGED
|
@@ -23,6 +23,29 @@ const path = require('path');
|
|
|
23
23
|
const MANIFEST_FILE = '.ma-agents.json';
|
|
24
24
|
const VALID_PROFILES = ['on-prem', 'standard'];
|
|
25
25
|
|
|
26
|
+
// Bug 27.12 — the project-root .ma-agents.json is a PROFILE/settings store, not
|
|
27
|
+
// a skill inventory. Per-agent skills dirs (e.g. .claude/skills/.ma-agents.json)
|
|
28
|
+
// own the installed-skill inventory and remain the authoritative source read by
|
|
29
|
+
// getStatus/uninstall/reconcile. The root bootstrap therefore emits ONLY the
|
|
30
|
+
// fields it owns (manifestVersion + a scope discriminator) and NOT a `skills`/
|
|
31
|
+
// `agents` shape it would never back-fill — so the root file can no longer
|
|
32
|
+
// masquerade as an empty skill inventory (`skills: {}`).
|
|
33
|
+
const ROOT_MANIFEST_VERSION = '1.2.0';
|
|
34
|
+
const ROOT_SCOPE = 'project-root';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Bootstrap a fresh PROFILE-ONLY root manifest object (not persisted here).
|
|
38
|
+
* Intentionally omits `skills` and `agents`: those belong to per-agent skills
|
|
39
|
+
* manifests. The `scope: 'project-root'` discriminator makes the file's role
|
|
40
|
+
* explicit for any reader.
|
|
41
|
+
*/
|
|
42
|
+
function bootstrapRootManifest() {
|
|
43
|
+
return {
|
|
44
|
+
manifestVersion: ROOT_MANIFEST_VERSION,
|
|
45
|
+
scope: ROOT_SCOPE,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
/**
|
|
27
50
|
* Reads the persisted profile from .ma-agents.json at the given project root.
|
|
28
51
|
* Returns undefined if the file does not exist, is malformed, or the "profile"
|
|
@@ -73,10 +96,12 @@ function setProfile(projectRoot, value) {
|
|
|
73
96
|
}
|
|
74
97
|
|
|
75
98
|
if (!manifest || typeof manifest !== 'object') {
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
// Bug 27.12 — bootstrap a PROFILE-ONLY root manifest. Previously this used
|
|
100
|
+
// ensureManifest(root, null, 'project'), which stamped an empty `skills: {}`
|
|
101
|
+
// / `agents: []` shape the root file never owns or back-fills, making it
|
|
102
|
+
// look like an (empty) skill inventory. The per-agent skills manifests are
|
|
103
|
+
// the authoritative inventory; the root file stores profile/settings only.
|
|
104
|
+
manifest = bootstrapRootManifest();
|
|
80
105
|
}
|
|
81
106
|
|
|
82
107
|
// Migrate 1.1.0 (or missing version) → 1.2.0, since the "profile" field is 1.2.0-only.
|
|
@@ -127,4 +152,70 @@ function clearProfile(projectRoot) {
|
|
|
127
152
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
128
153
|
}
|
|
129
154
|
|
|
130
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Bug 27.13 — durable BMAD-module selection.
|
|
157
|
+
*
|
|
158
|
+
* Reads the persisted BMAD module set from .ma-agents.json (the `bmadModules`
|
|
159
|
+
* field). Returns undefined when the file is absent/malformed or the field is
|
|
160
|
+
* missing, so callers can distinguish "never persisted" (→ install default set)
|
|
161
|
+
* from an explicit empty selection. A persisted value is always returned as an
|
|
162
|
+
* array of strings.
|
|
163
|
+
*/
|
|
164
|
+
function getBmadModules(projectRoot) {
|
|
165
|
+
const manifestPath = path.join(projectRoot, MANIFEST_FILE);
|
|
166
|
+
if (!fs.existsSync(manifestPath)) return undefined;
|
|
167
|
+
let manifest;
|
|
168
|
+
try {
|
|
169
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
170
|
+
} catch {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
if (!manifest || typeof manifest !== 'object') return undefined;
|
|
174
|
+
if (!Object.prototype.hasOwnProperty.call(manifest, 'bmadModules')) return undefined;
|
|
175
|
+
const value = manifest.bmadModules;
|
|
176
|
+
if (!Array.isArray(value)) return undefined;
|
|
177
|
+
// Normalize to a clean, de-duplicated array of non-empty strings.
|
|
178
|
+
return [...new Set(value.filter(m => typeof m === 'string' && m.length > 0))];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Bug 27.13 — persists the selected BMAD module set into .ma-agents.json,
|
|
183
|
+
* preserving all other fields. Creates the file via the standard ensureManifest
|
|
184
|
+
* bootstrap path if absent (mirroring setProfile). The choice then survives
|
|
185
|
+
* subsequent installs/reconfigures that do not re-pass --bmad-modules.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} projectRoot
|
|
188
|
+
* @param {string[]} modules - module ids to persist (e.g. ['bmm','tea','bmb'])
|
|
189
|
+
*/
|
|
190
|
+
function setBmadModules(projectRoot, modules) {
|
|
191
|
+
if (!Array.isArray(modules)) {
|
|
192
|
+
throw new Error(`setBmadModules expects an array of module ids (got ${JSON.stringify(modules)})`);
|
|
193
|
+
}
|
|
194
|
+
const cleaned = [...new Set(modules.filter(m => typeof m === 'string' && m.length > 0))];
|
|
195
|
+
|
|
196
|
+
const manifestPath = path.join(projectRoot, MANIFEST_FILE);
|
|
197
|
+
let manifest;
|
|
198
|
+
if (fs.existsSync(manifestPath)) {
|
|
199
|
+
try {
|
|
200
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
201
|
+
} catch {
|
|
202
|
+
manifest = null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
206
|
+
// Bug 27.12 — profile-only root bootstrap (see setProfile / bootstrapRootManifest).
|
|
207
|
+
manifest = bootstrapRootManifest();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
manifest.bmadModules = cleaned;
|
|
211
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = {
|
|
215
|
+
getProfile,
|
|
216
|
+
setProfile,
|
|
217
|
+
resolveProfile,
|
|
218
|
+
clearProfile,
|
|
219
|
+
getBmadModules,
|
|
220
|
+
setBmadModules,
|
|
221
|
+
};
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
## Git Workflow (ALL file-changing tasks)
|
|
2
|
-
|
|
3
|
-
These rules apply to EVERY task that modifies a tracked file. There is no
|
|
4
|
-
"small fix exemption" — a one-line edit follows the same workflow as a feature.
|
|
5
|
-
|
|
6
|
-
- **Worktree first.** Before changing any file, create a fresh isolated worktree
|
|
7
|
-
on a new feature branch: `git worktree add ../<project>-<branch> -b <branch>`.
|
|
8
|
-
The scripted form is `skills/git-workflow-skill/scripts/start-feature.sh`. All
|
|
9
|
-
file-writing happens inside that worktree — never in the main working tree.
|
|
10
|
-
- **Never commit directly to the trunk.** Do NOT commit to `main`, `dev`,
|
|
11
|
-
`master`, or whatever the trunk branch is. Commits land only on your feature
|
|
12
|
-
branch inside the worktree.
|
|
13
|
-
- **Conventional Commits.** Every commit message uses a Conventional Commits
|
|
14
|
-
prefix. The allowed prefixes are: `feat`, `fix`, `chore`, `docs`, `refactor`,
|
|
15
|
-
`test`, `ci`.
|
|
16
|
-
- **PR is the only path into the trunk.** Integrate exclusively by opening a pull
|
|
17
|
-
request with `gh pr create`. Never merge directly and never auto-merge a PR
|
|
18
|
-
without explicit human approval.
|
|
19
|
-
- **Self-review before opening the PR.** Run the `code-review` skill on your own
|
|
20
|
-
changes and address its findings before you open the PR.
|
|
21
|
-
- **Clean up after merge.** Once the PR is merged, remove the worktree with
|
|
22
|
-
`git worktree remove` and delete the feature branch.
|
|
23
|
-
|
|
24
|
-
Full procedure: see `skills/git-workflow-skill/SKILL.md`, and the helper scripts
|
|
25
|
-
`scripts/start-feature.sh` / `scripts/finish-feature.sh`.
|
|
1
|
+
## Git Workflow (ALL file-changing tasks)
|
|
2
|
+
|
|
3
|
+
These rules apply to EVERY task that modifies a tracked file. There is no
|
|
4
|
+
"small fix exemption" — a one-line edit follows the same workflow as a feature.
|
|
5
|
+
|
|
6
|
+
- **Worktree first.** Before changing any file, create a fresh isolated worktree
|
|
7
|
+
on a new feature branch: `git worktree add ../<project>-<branch> -b <branch>`.
|
|
8
|
+
The scripted form is `skills/git-workflow-skill/scripts/start-feature.sh`. All
|
|
9
|
+
file-writing happens inside that worktree — never in the main working tree.
|
|
10
|
+
- **Never commit directly to the trunk.** Do NOT commit to `main`, `dev`,
|
|
11
|
+
`master`, or whatever the trunk branch is. Commits land only on your feature
|
|
12
|
+
branch inside the worktree.
|
|
13
|
+
- **Conventional Commits.** Every commit message uses a Conventional Commits
|
|
14
|
+
prefix. The allowed prefixes are: `feat`, `fix`, `chore`, `docs`, `refactor`,
|
|
15
|
+
`test`, `ci`.
|
|
16
|
+
- **PR is the only path into the trunk.** Integrate exclusively by opening a pull
|
|
17
|
+
request with `gh pr create`. Never merge directly and never auto-merge a PR
|
|
18
|
+
without explicit human approval.
|
|
19
|
+
- **Self-review before opening the PR.** Run the `code-review` skill on your own
|
|
20
|
+
changes and address its findings before you open the PR.
|
|
21
|
+
- **Clean up after merge.** Once the PR is merged, remove the worktree with
|
|
22
|
+
`git worktree remove` and delete the feature branch.
|
|
23
|
+
|
|
24
|
+
Full procedure: see `skills/git-workflow-skill/SKILL.md`, and the helper scripts
|
|
25
|
+
`scripts/start-feature.sh` / `scripts/finish-feature.sh`.
|