ma-agents 3.18.0-beta.1 → 3.18.0-beta.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/README.md +15 -0
- package/bin/cli.js +80 -27
- package/docs/technical-notes/enforcement-hooks-research.md +10 -1
- package/lib/agents.js +81 -3
- package/lib/bmad-extension/skills/ma-agent-cyber/SKILL.md +2 -2
- package/lib/bmad-extension/skills/ma-agent-devops/SKILL.md +2 -2
- package/lib/bmad-extension/skills/ma-agent-sqa/SKILL.md +2 -2
- package/lib/bmad-extension/skills/ma-agent-sre/SKILL.md +2 -2
- package/lib/bmad-extension-plugin/.claude-plugin/marketplace.json +1 -1
- package/lib/bmad-extension-plugin/skills/ma-agent-cyber/SKILL.md +2 -2
- package/lib/bmad-extension-plugin/skills/ma-agent-devops/SKILL.md +2 -2
- package/lib/bmad-extension-plugin/skills/ma-agent-sqa/SKILL.md +2 -2
- package/lib/bmad-extension-plugin/skills/ma-agent-sre/SKILL.md +2 -2
- package/lib/bmad.js +263 -15
- package/lib/installer.js +132 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -277,6 +277,21 @@ Both messages end by stating that nothing was read or written on your behalf.
|
|
|
277
277
|
These are stated here, in the shipped documentation, rather than only in the
|
|
278
278
|
repository's changelog — which is **not** part of the npm package.
|
|
279
279
|
|
|
280
|
+
- **Versions 3.7.0 through 3.17.1 could silently delete hand-authored skills.**
|
|
281
|
+
When a tool's skills directory moved between versions, the installer removed
|
|
282
|
+
the old directory recursively once the new one had content, destroying any
|
|
283
|
+
skill you had written there yourself and reporting it as a successful
|
|
284
|
+
cleanup. This affected `.github/copilot/skills`, `.github/skills`,
|
|
285
|
+
`.roo/skills` and `.kilocode/skills`. **It is fixed in this version** — the
|
|
286
|
+
sweep now removes only what ma-agents installed and preserves the directory
|
|
287
|
+
if anything else remains — but deletions already made are not recoverable
|
|
288
|
+
from ma-agents: no backup was written and nothing but the directory name was
|
|
289
|
+
recorded. Check your version control if you kept such skills.
|
|
290
|
+
- **A file placed inside a skill directory ma-agents installed is still removed
|
|
291
|
+
with it.** The sweep's unit is the skill directory, not the file. If you have
|
|
292
|
+
edited a shipped skill in place, that edit is lost when the directory is
|
|
293
|
+
swept. Keep your own skills in their own directories, which are preserved.
|
|
294
|
+
|
|
280
295
|
- **Every claim in this release about behaviour with a Confluence MCP *present*
|
|
281
296
|
is mock-verified, not live.** No authorised Confluence MCP was available to
|
|
282
297
|
the implementation or to any review, so the MCP-present branches were
|
package/bin/cli.js
CHANGED
|
@@ -157,9 +157,13 @@ ${chalk.bold('Install options:')}
|
|
|
157
157
|
${chalk.cyan('--yes')} Skip all prompts, use defaults (for CI/CD)
|
|
158
158
|
${chalk.cyan('--agent <name>')} Target a specific agent (skip agent selection)
|
|
159
159
|
${chalk.cyan('--bmad-modules [csv]')} Control BMAD module selection.
|
|
160
|
-
Bare: open a multiselect prompt
|
|
160
|
+
Bare: open a multiselect prompt (offers every
|
|
161
|
+
bundled module, incl. gds and wds)
|
|
161
162
|
With CSV: install exactly those (e.g. tea,bmb)
|
|
162
|
-
Omitted: install
|
|
163
|
+
Omitted: install bmm,tea,bmb,cis (default). The
|
|
164
|
+
design-studio modules gds and wds are
|
|
165
|
+
bundled but not installed by default —
|
|
166
|
+
select them to install them.
|
|
163
167
|
'bmm' is always force-included.
|
|
164
168
|
${chalk.cyan('--bmad-custom-source <ref>')} Enumerate extensions from ONE custom
|
|
165
169
|
marketplace repo (local path or Git URL containing
|
|
@@ -347,7 +351,7 @@ function parseFlags(args) {
|
|
|
347
351
|
// --bmad-modules — controls BMad module selection. Optional-value flag:
|
|
348
352
|
// bare → ask the user via multiselect prompt
|
|
349
353
|
// with CSV → install exactly those (bmm force-prepended, unknowns rejected)
|
|
350
|
-
// not present → install
|
|
354
|
+
// not present → install FR123's default set, bmm+tea+bmb+cis (no prompt)
|
|
351
355
|
// Force-prepending of `bmm` and validation of unknown ids is handled
|
|
352
356
|
// centrally by lib/bmad.resolveBmadModules.
|
|
353
357
|
let bmadModulesFlag = '';
|
|
@@ -3671,18 +3675,30 @@ async function collectSprintManagementLocation(concern, existingRecord) {
|
|
|
3671
3675
|
*
|
|
3672
3676
|
* Three branches:
|
|
3673
3677
|
* 1. `--bmad-modules <csv>` → install exactly those (bmm force-prepended,
|
|
3674
|
-
* unknowns rejected with a clear error)
|
|
3675
|
-
*
|
|
3676
|
-
*
|
|
3677
|
-
*
|
|
3678
|
-
*
|
|
3679
|
-
*
|
|
3680
|
-
*
|
|
3678
|
+
* unknowns rejected with a clear error). An explicit selection always
|
|
3679
|
+
* wins, including one that names `gds` or `wds`.
|
|
3680
|
+
* 2. `--bmad-modules` bare → multiselect prompt; EVERY bundled module is
|
|
3681
|
+
* offered, bmm is required and pre-checked, and the remaining initial
|
|
3682
|
+
* check state is the FR123 default set — so a no-op confirmation lands
|
|
3683
|
+
* on exactly the same modules as branch 3.
|
|
3684
|
+
* 3. (no flag) → no prompt. In order: a persisted selection
|
|
3685
|
+
* from a prior run (bug 27.13), else the modules already present under
|
|
3686
|
+
* `_bmad/`, else `bmad.getDefaultBmadModules()` — FR123's amended
|
|
3687
|
+
* default set, bmm + tea + bmb + cis.
|
|
3688
|
+
*
|
|
3689
|
+
* WHY BRANCH 3 IS NO LONGER "EVERY NON-RETIRED MODULE" (FR123, amended
|
|
3690
|
+
* 2026-08-02): that default is what put a five-folder `design-artifacts/`
|
|
3691
|
+
* tree at the root of a Copilot+Cline project that had never asked for UX
|
|
3692
|
+
* tooling — `wds` installed unasked and materialised its `directories:`
|
|
3693
|
+
* block. `gds` and `wds` are still BUNDLED and still installable offline;
|
|
3694
|
+
* they are simply no longer installed without being asked for. Bundling and
|
|
3695
|
+
* installing are different decisions made at different times, and only the
|
|
3696
|
+
* second one moved.
|
|
3681
3697
|
*
|
|
3682
3698
|
* `bmm` is force-included in every path because the BMad core agents
|
|
3683
3699
|
* (analyst, PM, dev, architect, ...) live there and downstream skills
|
|
3684
|
-
* break without them. Retired modules (
|
|
3685
|
-
*
|
|
3700
|
+
* break without them. Retired modules (none at v6.10.0) are filtered out
|
|
3701
|
+
* unconditionally.
|
|
3686
3702
|
*/
|
|
3687
3703
|
async function selectBmadModules({ bmadModulesFlag, bmadModulesPrompt, projectRoot } = {}) {
|
|
3688
3704
|
const installable = bmad.getInstallableBmadModules().filter(m => !m.retired);
|
|
@@ -3712,15 +3728,22 @@ async function selectBmadModules({ bmadModulesFlag, bmadModulesPrompt, projectRo
|
|
|
3712
3728
|
|
|
3713
3729
|
// Branch 2: bare --bmad-modules → multiselect prompt.
|
|
3714
3730
|
if (bmadModulesPrompt) {
|
|
3731
|
+
// FR123: every bundled module is OFFERED — this prompt is the documented
|
|
3732
|
+
// route by which `gds`/`wds` remain reachable offline — but the initial
|
|
3733
|
+
// check state is the default INSTALL set, so pressing Enter without
|
|
3734
|
+
// touching anything lands on the same modules as the no-flag branch below.
|
|
3735
|
+
// Checking them all here would silently reintroduce the very "installs
|
|
3736
|
+
// everything" default this change removes.
|
|
3737
|
+
const defaultIds = new Set(bmad.getDefaultBmadModules(installable));
|
|
3715
3738
|
const choices = installable.map(m => {
|
|
3716
3739
|
const isCore = m.id === 'bmm';
|
|
3717
3740
|
const requiredTag = isCore ? chalk.yellow(' (required)') : '';
|
|
3718
3741
|
return {
|
|
3719
3742
|
title: chalk.white(m.id) + requiredTag + chalk.gray(` — ${m.description}`),
|
|
3720
3743
|
value: m.id,
|
|
3721
|
-
//
|
|
3722
|
-
//
|
|
3723
|
-
selected:
|
|
3744
|
+
// bmm is force-included downstream regardless; pre-checking it keeps
|
|
3745
|
+
// the prompt honest about that.
|
|
3746
|
+
selected: isCore || defaultIds.has(m.id),
|
|
3724
3747
|
};
|
|
3725
3748
|
});
|
|
3726
3749
|
|
|
@@ -3740,22 +3763,52 @@ async function selectBmadModules({ bmadModulesFlag, bmadModulesPrompt, projectRo
|
|
|
3740
3763
|
return persist(bmad.resolveBmadModules({ requested: chosenModules, available: installable }));
|
|
3741
3764
|
}
|
|
3742
3765
|
|
|
3743
|
-
// Branch 3: no flag
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
//
|
|
3747
|
-
//
|
|
3766
|
+
// Branch 3: no flag. Three fallbacks, most-specific first.
|
|
3767
|
+
const availableIds = new Set(installable.map(m => m.id));
|
|
3768
|
+
|
|
3769
|
+
// 3a — a persisted selection from a prior run (bug 27.13): a previous
|
|
3770
|
+
// deselection must survive. Intersected with the currently-installable set so
|
|
3771
|
+
// a since-retired module does not resurface; bmm is force-included via
|
|
3772
|
+
// resolveBmadModules.
|
|
3748
3773
|
const persisted = getBmadModules(root);
|
|
3749
3774
|
if (persisted && persisted.length > 0) {
|
|
3750
|
-
const availableIds = new Set(installable.map(m => m.id));
|
|
3751
3775
|
const stillInstallable = persisted.filter(id => availableIds.has(id));
|
|
3752
3776
|
try {
|
|
3753
3777
|
return persist(bmad.resolveBmadModules({ requested: stillInstallable, available: installable }));
|
|
3754
3778
|
} catch {
|
|
3755
|
-
// If the persisted set somehow fails validation, fall through
|
|
3779
|
+
// If the persisted set somehow fails validation, fall through.
|
|
3756
3780
|
}
|
|
3757
3781
|
}
|
|
3758
|
-
|
|
3782
|
+
|
|
3783
|
+
// 3b — NON-DESTRUCTIVE MIGRATION. Narrowing the default (FR123, amended
|
|
3784
|
+
// 2026-08-02) is behaviour-visible to projects that already installed under
|
|
3785
|
+
// the old every-module default, and an update run passes this same resolved
|
|
3786
|
+
// list to `bmad.updateBmad`. A project that already has `wds` under `_bmad/`
|
|
3787
|
+
// must not have it dropped out of its own module list just because the
|
|
3788
|
+
// default moved — narrowing applies to NEW installs, not to existing ones.
|
|
3789
|
+
//
|
|
3790
|
+
// 3a covers projects installed since bug 27.13 landed persistence; this
|
|
3791
|
+
// covers the ones installed before it, where `.ma-agents.json` carries no
|
|
3792
|
+
// `bmadModules` field at all and disk is the only record of what was chosen.
|
|
3793
|
+
// A fresh project has no `_bmad/`, so this is inert for new installs — which
|
|
3794
|
+
// is exactly the "existing installs unchanged, only NEW installs get the
|
|
3795
|
+
// narrower default" posture NFR49 / Story 30.4 established for Demerzel.
|
|
3796
|
+
// Persisting the result also migrates the project onto 27.13's durable
|
|
3797
|
+
// record, so this disk probe is needed at most once per project.
|
|
3798
|
+
const alreadyInstalled = bmad.getInstalledBmadModules(root).filter(id => availableIds.has(id));
|
|
3799
|
+
if (alreadyInstalled.length > 0) {
|
|
3800
|
+
try {
|
|
3801
|
+
return persist(bmad.resolveBmadModules({ requested: alreadyInstalled, available: installable }));
|
|
3802
|
+
} catch {
|
|
3803
|
+
// Fall through to the default set.
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3807
|
+
// 3c — a genuinely fresh install: FR123's amended default set.
|
|
3808
|
+
return persist(bmad.resolveBmadModules({
|
|
3809
|
+
requested: bmad.getDefaultBmadModules(installable),
|
|
3810
|
+
available: installable,
|
|
3811
|
+
}));
|
|
3759
3812
|
}
|
|
3760
3813
|
|
|
3761
3814
|
// --- Custom marketplace source (Story 30.6a) ---
|
|
@@ -4408,10 +4461,10 @@ async function installWizard(preselectedSkill, preselectedAgents, customPath, fo
|
|
|
4408
4461
|
// stage the plugin and invoke the installer in one pass.
|
|
4409
4462
|
// v6.6.0: --tools none is rejected for fresh installs; skip BMAD entirely when no IDE tools selected.
|
|
4410
4463
|
if (bmadTools.length > 0) {
|
|
4411
|
-
// Resolve module set once per BMAD invocation. Default
|
|
4412
|
-
//
|
|
4413
|
-
//
|
|
4414
|
-
//
|
|
4464
|
+
// Resolve module set once per BMAD invocation. Default (FR123, amended
|
|
4465
|
+
// 2026-08-02) is bmm+tea+bmb+cis; the bundled design-studio modules gds
|
|
4466
|
+
// and wds install only when asked for, via --bmad-modules <csv> or the
|
|
4467
|
+
// multiselect that bare --bmad-modules opens.
|
|
4415
4468
|
const bmadModules = await selectBmadModules({
|
|
4416
4469
|
bmadModulesFlag,
|
|
4417
4470
|
bmadModulesPrompt,
|
|
@@ -294,11 +294,20 @@ All 11 BMAD agents have enforcement via `critical_actions` in `.customize.yaml`
|
|
|
294
294
|
**Mechanism:**
|
|
295
295
|
```yaml
|
|
296
296
|
critical_actions:
|
|
297
|
-
1: "Read the skills MANIFEST at {project-root}/_bmad/skills
|
|
297
|
+
1: "Read the skills MANIFEST at {project-root}/_bmad/skills/<agent-slug>/MANIFEST.yaml if it exists"
|
|
298
298
|
2: "For each skill marked always_load: true, read the skill file completely"
|
|
299
299
|
3: "Follow all skill directives during this session"
|
|
300
300
|
```
|
|
301
301
|
|
|
302
|
+
> **Note (2026-09-12):** this snippet previously showed a bare
|
|
303
|
+
> `{project-root}/_bmad/skills/MANIFEST.yaml`. That path is not what Story 8.3
|
|
304
|
+
> specified — the manifest is per-agent-host, at
|
|
305
|
+
> `{project-root}/_bmad/skills/<agent-slug>/MANIFEST.yaml`, where `<agent-slug>`
|
|
306
|
+
> matches the `skillsDir` in `lib/agents.js` (`sre`, `devops`, `cyber`, `sqa`).
|
|
307
|
+
> The bare form leaked from here into `ma-agent-sqa`'s Critical Actions block and
|
|
308
|
+
> survived there until it was corrected. Keep the slug segment in any copy of this
|
|
309
|
+
> snippet.
|
|
310
|
+
|
|
302
311
|
**Deployment:** Extension module at `_bmad/extensions/ma-agents-skills/` deployed during BMAD customization pipeline (Stage 3 in `bmad.js`).
|
|
303
312
|
|
|
304
313
|
**Coverage:**
|
package/lib/agents.js
CHANGED
|
@@ -242,8 +242,27 @@ const agents = [
|
|
|
242
242
|
version: '1.0.0',
|
|
243
243
|
category: 'ide',
|
|
244
244
|
description: 'Google Deepmind Antigravity Agent',
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
// Bug `antigravity-path-divergence`: this used to read `.antigravity/skills`,
|
|
246
|
+
// a path ma-agents invented from its own per-tool naming convention. Upstream
|
|
247
|
+
// bmad-method maps the SAME tool to `.agent/skills`
|
|
248
|
+
// (node_modules/bmad-method/tools/installer/ide/platform-codes.yaml, platform
|
|
249
|
+
// `antigravity`, in a file whose header records the paths as "verified against
|
|
250
|
+
// each tool's primary docs"; its global counterpart `~/.gemini/antigravity/skills`
|
|
251
|
+
// shows the entry was researched rather than guessed). Because ma-agents
|
|
252
|
+
// delegates part of the install to that installer, a single run produced TWO
|
|
253
|
+
// trees — and `.agent/skills`, the one Antigravity actually reads, was named by
|
|
254
|
+
// neither this registry nor lib/bmad.js, so every ma-agents post-deploy patch
|
|
255
|
+
// (the on-prem phase prefix among them) silently skipped it. Measured in a real
|
|
256
|
+
// on-prem project: .claude/.agents/.cline each carried the prefix on 6 personas,
|
|
257
|
+
// .agent/skills on 0.
|
|
258
|
+
// Upstream is the authority on which directory a tool reads, so we align —
|
|
259
|
+
// exactly as copilot/roo-code/kilocode were aligned to `.agents/skills` when
|
|
260
|
+
// bmad-method 6.5.0 moved them (see the copilot entry above). This is a
|
|
261
|
+
// per-tool decision on per-tool evidence: the other three divergences in the
|
|
262
|
+
// table are recorded in PLATFORM_PATH_DIVERGENCES below, deliberately unchanged.
|
|
263
|
+
// Existing installs migrate via OBSOLETE_TOOL_SKILL_DIRS in lib/installer.js.
|
|
264
|
+
skillsDir: '.agent/skills',
|
|
265
|
+
getProjectPath: () => path.join(process.cwd(), '.agent', 'skills'),
|
|
247
266
|
getGlobalPath: () => {
|
|
248
267
|
const platform = os.platform();
|
|
249
268
|
if (platform === 'win32') {
|
|
@@ -356,6 +375,64 @@ const agents = [
|
|
|
356
375
|
// Add them here in a dedicated future story if/when demand appears.
|
|
357
376
|
];
|
|
358
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Declared, deliberate divergences between this registry's project `skillsDir`
|
|
380
|
+
* and bmad-method's `installer.target_dir` for the SAME tool
|
|
381
|
+
* (`node_modules/bmad-method/tools/installer/ide/platform-codes.yaml`).
|
|
382
|
+
* Keyed by bmad platform code (see `getBmadPlatformCode`).
|
|
383
|
+
*
|
|
384
|
+
* Why this exists (bug `antigravity-path-divergence`): the two tables had drifted
|
|
385
|
+
* apart for four tools and nothing compared them outside the five flagships, so a
|
|
386
|
+
* whole second skill tree appeared in real projects with no owner and no patcher.
|
|
387
|
+
* `test/routing-parity.test.js` (section 5) now enumerates EVERY tool in BOTH
|
|
388
|
+
* tables and fails on any divergence that is not declared here — and on any
|
|
389
|
+
* declaration whose recorded paths have gone stale, so an entry cannot rot into
|
|
390
|
+
* cover for a different divergence.
|
|
391
|
+
*
|
|
392
|
+
* Scope note — PROJECT paths only. `getGlobalPath()` follows a per-OS
|
|
393
|
+
* application-data convention that differs from upstream's `global_target_dir`
|
|
394
|
+
* for EVERY tool, claude-code included (`%APPDATA%/Claude/skills` vs
|
|
395
|
+
* `~/.claude/skills`). That is a whole-table convention difference, not a
|
|
396
|
+
* per-tool divergence, and is deliberately out of scope here.
|
|
397
|
+
*
|
|
398
|
+
* To add an entry you must record BOTH live paths and a real reason. To remove
|
|
399
|
+
* one, align the paths — the test fails on a declaration whose paths now agree.
|
|
400
|
+
*/
|
|
401
|
+
const PLATFORM_PATH_DIVERGENCES = Object.freeze({
|
|
402
|
+
gemini: Object.freeze({
|
|
403
|
+
maSkillsDir: '.gemini/skills',
|
|
404
|
+
bmadTargetDir: '.agents/skills',
|
|
405
|
+
reason:
|
|
406
|
+
"Upstream's `gemini` platform is the Gemini CLI, which reads the shared " +
|
|
407
|
+
'`.agents/skills` tree; this registry entry targets Google Gemini Code Assist ' +
|
|
408
|
+
'(the IDE extension — note its instruction file `.gemini/gemini.md`), a ' +
|
|
409
|
+
'different product sharing the same code. Contained: upstream writes ' +
|
|
410
|
+
'`.agents/skills`, which is already an IDE skillsDir in this registry, already ' +
|
|
411
|
+
'a seed in lib/bmad.js TOOL_SKILL_DIRS, and already reconciled by bug 27.16 — ' +
|
|
412
|
+
'so no tree goes unpatched. Realigning is a compatibility call for its own story.'
|
|
413
|
+
}),
|
|
414
|
+
cursor: Object.freeze({
|
|
415
|
+
maSkillsDir: '.cursor/skills',
|
|
416
|
+
bmadTargetDir: '.agents/skills',
|
|
417
|
+
reason:
|
|
418
|
+
'Cursor reads its own `.cursor/` workspace directory (this registry also stamps ' +
|
|
419
|
+
'`.cursor/cursor.md` there); upstream routes it to the shared `.agents/skills` ' +
|
|
420
|
+
'cross-tool tree instead. Contained for the same reason as `gemini`: upstream ' +
|
|
421
|
+
"'s target is already a known, patched and reconciled tree. Kept deliberately " +
|
|
422
|
+
'so this bug fix changes the write path of exactly one tool, on that tool’s evidence.'
|
|
423
|
+
}),
|
|
424
|
+
opencode: Object.freeze({
|
|
425
|
+
maSkillsDir: '.opencode/skills',
|
|
426
|
+
bmadTargetDir: '.agents/skills',
|
|
427
|
+
reason:
|
|
428
|
+
'OpenCode is wired here through `opencode.json::instructions[]` plus AGENTS.md ' +
|
|
429
|
+
'(Story 21.4) against its own `.opencode/` tree; upstream routes it to the shared ' +
|
|
430
|
+
'`.agents/skills`. Contained for the same reason as `gemini` and `cursor`: ' +
|
|
431
|
+
"upstream's target is already a known, patched and reconciled tree. Realignment " +
|
|
432
|
+
'would also have to move the instruction wiring, so it belongs in its own story.'
|
|
433
|
+
})
|
|
434
|
+
});
|
|
435
|
+
|
|
359
436
|
function getAgent(agentId) {
|
|
360
437
|
return agents.find(a => a.id === agentId);
|
|
361
438
|
}
|
|
@@ -383,5 +460,6 @@ module.exports = {
|
|
|
383
460
|
getAgent,
|
|
384
461
|
getAllAgents,
|
|
385
462
|
getAgentsByCategory,
|
|
386
|
-
getBmadPlatformCode
|
|
463
|
+
getBmadPlatformCode,
|
|
464
|
+
PLATFORM_PATH_DIVERGENCES
|
|
387
465
|
};
|
|
@@ -43,7 +43,7 @@ You must fully embody this agent's persona and follow all activation instruction
|
|
|
43
43
|
| 6 | DA | Dismiss Agent | "dismiss", "exit", "quit" | _(built-in)_ |
|
|
44
44
|
|
|
45
45
|
## Critical Actions
|
|
46
|
-
1. Read the skills MANIFEST at {project-root}/_bmad/skills/cyber/MANIFEST.yaml
|
|
47
|
-
2. For each skill marked always_load: true, read the skill file completely
|
|
46
|
+
1. Read the skills MANIFEST at {project-root}/_bmad/skills/cyber/MANIFEST.yaml if it exists
|
|
47
|
+
2. For each skill marked always_load: true, read the skill file completely (only if that MANIFEST was read; if it was not, skip this step)
|
|
48
48
|
3. If _bmad-output/project-context.md exists, read it completely
|
|
49
49
|
4. Follow all skill directives and project-context rules during this session
|
|
@@ -43,7 +43,7 @@ You must fully embody this agent's persona and follow all activation instruction
|
|
|
43
43
|
| 6 | DA | Dismiss Agent | "dismiss", "exit", "quit" | _(built-in)_ |
|
|
44
44
|
|
|
45
45
|
## Critical Actions
|
|
46
|
-
1. Read the skills MANIFEST at {project-root}/_bmad/skills/devops/MANIFEST.yaml
|
|
47
|
-
2. For each skill marked always_load: true, read the skill file completely
|
|
46
|
+
1. Read the skills MANIFEST at {project-root}/_bmad/skills/devops/MANIFEST.yaml if it exists
|
|
47
|
+
2. For each skill marked always_load: true, read the skill file completely (only if that MANIFEST was read; if it was not, skip this step)
|
|
48
48
|
3. If _bmad-output/project-context.md exists, read it completely
|
|
49
49
|
4. Follow all skill directives and project-context rules during this session
|
|
@@ -53,7 +53,7 @@ You must fully embody this agent's persona and follow all activation instruction
|
|
|
53
53
|
| 14 | DA | Dismiss Agent | "dismiss", "exit", "quit" | _(built-in)_ |
|
|
54
54
|
|
|
55
55
|
## Critical Actions
|
|
56
|
-
1. Read the skills MANIFEST at {project-root}/_bmad/skills/MANIFEST.yaml
|
|
57
|
-
2. For each skill marked always_load: true, read the skill file completely
|
|
56
|
+
1. Read the skills MANIFEST at {project-root}/_bmad/skills/sqa/MANIFEST.yaml if it exists
|
|
57
|
+
2. For each skill marked always_load: true, read the skill file completely (only if that MANIFEST was read; if it was not, skip this step)
|
|
58
58
|
3. If _bmad-output/project-context.md exists, read it completely
|
|
59
59
|
4. Follow all skill directives and project-context rules during this session
|
|
@@ -43,7 +43,7 @@ You must fully embody this agent's persona and follow all activation instruction
|
|
|
43
43
|
| 6 | DA | Dismiss Agent | "dismiss", "exit", "quit" | _(built-in)_ |
|
|
44
44
|
|
|
45
45
|
## Critical Actions
|
|
46
|
-
1. Read the skills MANIFEST at {project-root}/_bmad/skills/sre/MANIFEST.yaml
|
|
47
|
-
2. For each skill marked always_load: true, read the skill file completely
|
|
46
|
+
1. Read the skills MANIFEST at {project-root}/_bmad/skills/sre/MANIFEST.yaml if it exists
|
|
47
|
+
2. For each skill marked always_load: true, read the skill file completely (only if that MANIFEST was read; if it was not, skip this step)
|
|
48
48
|
3. If _bmad-output/project-context.md exists, read it completely
|
|
49
49
|
4. Follow all skill directives and project-context rules during this session
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"name": "ma-skills",
|
|
38
38
|
"source": "./",
|
|
39
39
|
"description": "ma-agents extension module providing enterprise SDLC personas and operational workflow skills.",
|
|
40
|
-
"version": "3.18.0-beta.
|
|
40
|
+
"version": "3.18.0-beta.2",
|
|
41
41
|
"author": {
|
|
42
42
|
"name": "Alon Mayaffit"
|
|
43
43
|
},
|
|
@@ -43,7 +43,7 @@ You must fully embody this agent's persona and follow all activation instruction
|
|
|
43
43
|
| 6 | DA | Dismiss Agent | "dismiss", "exit", "quit" | _(built-in)_ |
|
|
44
44
|
|
|
45
45
|
## Critical Actions
|
|
46
|
-
1. Read the skills MANIFEST at {project-root}/_bmad/skills/cyber/MANIFEST.yaml
|
|
47
|
-
2. For each skill marked always_load: true, read the skill file completely
|
|
46
|
+
1. Read the skills MANIFEST at {project-root}/_bmad/skills/cyber/MANIFEST.yaml if it exists
|
|
47
|
+
2. For each skill marked always_load: true, read the skill file completely (only if that MANIFEST was read; if it was not, skip this step)
|
|
48
48
|
3. If _bmad-output/project-context.md exists, read it completely
|
|
49
49
|
4. Follow all skill directives and project-context rules during this session
|
|
@@ -43,7 +43,7 @@ You must fully embody this agent's persona and follow all activation instruction
|
|
|
43
43
|
| 6 | DA | Dismiss Agent | "dismiss", "exit", "quit" | _(built-in)_ |
|
|
44
44
|
|
|
45
45
|
## Critical Actions
|
|
46
|
-
1. Read the skills MANIFEST at {project-root}/_bmad/skills/devops/MANIFEST.yaml
|
|
47
|
-
2. For each skill marked always_load: true, read the skill file completely
|
|
46
|
+
1. Read the skills MANIFEST at {project-root}/_bmad/skills/devops/MANIFEST.yaml if it exists
|
|
47
|
+
2. For each skill marked always_load: true, read the skill file completely (only if that MANIFEST was read; if it was not, skip this step)
|
|
48
48
|
3. If _bmad-output/project-context.md exists, read it completely
|
|
49
49
|
4. Follow all skill directives and project-context rules during this session
|
|
@@ -53,7 +53,7 @@ You must fully embody this agent's persona and follow all activation instruction
|
|
|
53
53
|
| 14 | DA | Dismiss Agent | "dismiss", "exit", "quit" | _(built-in)_ |
|
|
54
54
|
|
|
55
55
|
## Critical Actions
|
|
56
|
-
1. Read the skills MANIFEST at {project-root}/_bmad/skills/MANIFEST.yaml
|
|
57
|
-
2. For each skill marked always_load: true, read the skill file completely
|
|
56
|
+
1. Read the skills MANIFEST at {project-root}/_bmad/skills/sqa/MANIFEST.yaml if it exists
|
|
57
|
+
2. For each skill marked always_load: true, read the skill file completely (only if that MANIFEST was read; if it was not, skip this step)
|
|
58
58
|
3. If _bmad-output/project-context.md exists, read it completely
|
|
59
59
|
4. Follow all skill directives and project-context rules during this session
|
|
@@ -43,7 +43,7 @@ You must fully embody this agent's persona and follow all activation instruction
|
|
|
43
43
|
| 6 | DA | Dismiss Agent | "dismiss", "exit", "quit" | _(built-in)_ |
|
|
44
44
|
|
|
45
45
|
## Critical Actions
|
|
46
|
-
1. Read the skills MANIFEST at {project-root}/_bmad/skills/sre/MANIFEST.yaml
|
|
47
|
-
2. For each skill marked always_load: true, read the skill file completely
|
|
46
|
+
1. Read the skills MANIFEST at {project-root}/_bmad/skills/sre/MANIFEST.yaml if it exists
|
|
47
|
+
2. For each skill marked always_load: true, read the skill file completely (only if that MANIFEST was read; if it was not, skip this step)
|
|
48
48
|
3. If _bmad-output/project-context.md exists, read it completely
|
|
49
49
|
4. Follow all skill directives and project-context rules during this session
|
package/lib/bmad.js
CHANGED
|
@@ -527,6 +527,69 @@ const BMAD_MODULE_DESCRIPTIONS = Object.freeze({
|
|
|
527
527
|
wds: 'Whiteport Design Studio — strategic UX and design-first planning',
|
|
528
528
|
});
|
|
529
529
|
|
|
530
|
+
/**
|
|
531
|
+
* FR123, as amended 2026-08-02 — the DEFAULT BMAD install set.
|
|
532
|
+
*
|
|
533
|
+
* "the default install set is bmm plus the non-design bundled modules —
|
|
534
|
+
* tea, bmb, cis — from the bundled cache manifest. The two design-studio
|
|
535
|
+
* modules gds (Game Design Studio) and wds (Whiteport Design Studio) are
|
|
536
|
+
* bundled but not installed by default."
|
|
537
|
+
*
|
|
538
|
+
* BUNDLING AND INSTALLING ARE DIFFERENT THINGS and this constant only speaks
|
|
539
|
+
* about the second one. `gds` and `wds` stay in `lib/bmad-cache/`, stay in
|
|
540
|
+
* `cache-manifest.json`, stay in `KNOWN_BUNDLED_MODULES`, and stay offered by
|
|
541
|
+
* the `--bmad-modules` multiselect — a `--bmad-modules wds` install still
|
|
542
|
+
* resolves entirely from the bundled cache with no network access (NFR2 /
|
|
543
|
+
* NFR50 / NFR57 are untouched). What changes is only that they no longer
|
|
544
|
+
* arrive uninvited.
|
|
545
|
+
*
|
|
546
|
+
* WHY THESE TWO AND NOT OTHERS: they are the only bundled modules that
|
|
547
|
+
* declare a `directories:` block scaffolding `design-artifacts/` at the
|
|
548
|
+
* project root, which is how a Copilot+Cline user with no interest in UX
|
|
549
|
+
* tooling ended up with an unexplained five-folder `design-artifacts/` tree
|
|
550
|
+
* (bug B2). Upstream itself declares `default_selected: false` on every
|
|
551
|
+
* bundled module and `true` only on `bmm`, so narrowing here moves ma-agents
|
|
552
|
+
* TOWARDS upstream's stated intent rather than away from it.
|
|
553
|
+
*
|
|
554
|
+
* DELIBERATELY A LITERAL, NOT DERIVED FROM THE MANIFEST. The previous
|
|
555
|
+
* behaviour — "every non-retired key of the cache manifest" — is precisely
|
|
556
|
+
* what made the set grow silently every time a module was bundled. Naming the
|
|
557
|
+
* members means the next bundled module has to be added here on purpose.
|
|
558
|
+
*/
|
|
559
|
+
const DEFAULT_BMAD_MODULE_IDS = Object.freeze(['bmm', 'tea', 'bmb', 'cis']);
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* The counterpart to DEFAULT_BMAD_MODULE_IDS: bundled, fully installable, but
|
|
563
|
+
* only when explicitly asked for. Exported so callers (and tests) can state
|
|
564
|
+
* "bundled but not default" without re-deriving it by subtraction.
|
|
565
|
+
*/
|
|
566
|
+
const NON_DEFAULT_BMAD_MODULE_IDS = Object.freeze(['gds', 'wds']);
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* The FR123 default set, intersected with what this bundle can actually
|
|
570
|
+
* install.
|
|
571
|
+
*
|
|
572
|
+
* The intersection is not ceremony: `getInstallableBmadModules()` degrades to
|
|
573
|
+
* `[bmm]` alone when the cache manifest is missing or unreadable, and a
|
|
574
|
+
* default set containing `tea` would then be handed to `resolveBmadModules`,
|
|
575
|
+
* which throws on unknown ids. A broken cache must degrade to a minimal
|
|
576
|
+
* install, not to a crash in the module-selection step.
|
|
577
|
+
*
|
|
578
|
+
* Order follows DEFAULT_BMAD_MODULE_IDS, so the resolved `--modules` value is
|
|
579
|
+
* deterministic (NFR21) regardless of manifest key order.
|
|
580
|
+
*
|
|
581
|
+
* @param {Array<{id: string}>|string[]} [available] - installable modules;
|
|
582
|
+
* defaults to the non-retired installable set.
|
|
583
|
+
* @returns {string[]} module ids, `bmm` first
|
|
584
|
+
*/
|
|
585
|
+
function getDefaultBmadModules(available) {
|
|
586
|
+
const pool = Array.isArray(available)
|
|
587
|
+
? available
|
|
588
|
+
: getInstallableBmadModules().filter(m => !m.retired);
|
|
589
|
+
const ids = new Set(pool.map(m => (m && typeof m === 'object' ? m.id : m)));
|
|
590
|
+
return DEFAULT_BMAD_MODULE_IDS.filter(id => ids.has(id));
|
|
591
|
+
}
|
|
592
|
+
|
|
530
593
|
/**
|
|
531
594
|
* Return the BMAD modules this ma-agents bundle can install. `bmm` (the
|
|
532
595
|
* BMad core) is always first because it ships inside the `bmad-method`
|
|
@@ -567,7 +630,18 @@ function getInstallableBmadModules() {
|
|
|
567
630
|
/**
|
|
568
631
|
* Detect which BMad modules are already installed under a given project
|
|
569
632
|
* root by checking for `_bmad/<id>/` directories. Used by the install
|
|
570
|
-
* wizard's update path to pre-check the user's existing module set
|
|
633
|
+
* wizard's update path to pre-check the user's existing module set, and by
|
|
634
|
+
* `selectBmadModules` branch 3b as the non-destructive migration probe for
|
|
635
|
+
* projects that predate bug 27.13's persisted selection.
|
|
636
|
+
*
|
|
637
|
+
* DIRECTORIES, not merely entries. `fs.existsSync` answers yes for a regular
|
|
638
|
+
* file, so a stray `_bmad/wds` FILE was enough to make this report wds
|
|
639
|
+
* installed — and because branch 3b PERSISTS what it finds, that misreading
|
|
640
|
+
* became a durable "explicit user selection" the project could never shed.
|
|
641
|
+
* `prePopulateBmadCache` checks `stat.isDirectory()` in the analogous place
|
|
642
|
+
* (the cache-source walk); this matches it. `statSync` is wrapped because the
|
|
643
|
+
* entry can disappear between listing and stat and because a broken symlink
|
|
644
|
+
* throws ENOENT — both mean "not an installed module".
|
|
571
645
|
*
|
|
572
646
|
* @param {string} projectRoot - Absolute or relative project root
|
|
573
647
|
* @returns {string[]} Module ids that have a corresponding `_bmad/<id>/` dir
|
|
@@ -577,7 +651,13 @@ function getInstalledBmadModules(projectRoot) {
|
|
|
577
651
|
const bmadDir = path.join(projectRoot, BMAD_DIR);
|
|
578
652
|
if (!fs.existsSync(bmadDir)) return [];
|
|
579
653
|
const installable = getInstallableBmadModules().map(m => m.id);
|
|
580
|
-
return installable.filter(id =>
|
|
654
|
+
return installable.filter(id => {
|
|
655
|
+
try {
|
|
656
|
+
return fs.statSync(path.join(bmadDir, id)).isDirectory();
|
|
657
|
+
} catch {
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
});
|
|
581
661
|
}
|
|
582
662
|
|
|
583
663
|
/**
|
|
@@ -1271,23 +1351,166 @@ const PERSONA_SKILL_MAP = Object.freeze({
|
|
|
1271
1351
|
'bmm-analyst': 'bmad-agent-analyst',
|
|
1272
1352
|
'bmm-tech-writer': 'bmad-agent-tech-writer',
|
|
1273
1353
|
'bmm-ux-designer': 'bmad-agent-ux-designer',
|
|
1354
|
+
// INERT — these two never deploy. `bmad-agent-sm` and `bmad-agent-qa` were
|
|
1355
|
+
// retired upstream (merged into Amelia) and ma-agents itself deletes them on
|
|
1356
|
+
// every install via RETIRED_SKILL_IDS (lib/installer.js:2224-2229). Both
|
|
1357
|
+
// `bmm-sm.customize.yaml` and `bmm-qa.customize.yaml` nevertheless author an
|
|
1358
|
+
// `on_prem_phase_prefix`, so an authored behaviour is routed to a directory
|
|
1359
|
+
// nothing writes: measured on a real on-prem install, six of these eight
|
|
1360
|
+
// entries carry the prefix and these two are absent from all four trees.
|
|
1361
|
+
// Left AS IS deliberately — repointing `bmm-qa` at `ma-agent-sqa` (the
|
|
1362
|
+
// persona that file actually describes: Gadi, trigger `skill:ma-agent-sqa`)
|
|
1363
|
+
// would start injecting into a shipped persona that has never carried it,
|
|
1364
|
+
// which is a product decision, not a drive-by on an unrelated path fix.
|
|
1365
|
+
// Filed as bug-on-prem-phase-prefix-is-authored-for-two-personas-that-are-
|
|
1366
|
+
// never-deployed, with both readings and the measurement.
|
|
1274
1367
|
'bmm-sm': 'bmad-agent-sm',
|
|
1275
1368
|
'bmm-qa': 'bmad-agent-qa',
|
|
1276
1369
|
});
|
|
1277
1370
|
|
|
1278
|
-
// Tool skill-dir roots
|
|
1279
|
-
//
|
|
1280
|
-
//
|
|
1281
|
-
//
|
|
1282
|
-
//
|
|
1283
|
-
//
|
|
1284
|
-
//
|
|
1371
|
+
// Tool skill-dir roots declared by `lib/agents.js` (category `ide`), deduplicated
|
|
1372
|
+
// — duplicated here rather than imported to avoid pulling the agents module into
|
|
1373
|
+
// install-time code paths that don't need it. `test/routing-parity.test.js` 5.4
|
|
1374
|
+
// asserts set equality against the IDE registry, so drift fails at build time.
|
|
1375
|
+
//
|
|
1376
|
+
// Bug `antigravity-path-divergence`: this list is a SEED, no longer the patch
|
|
1377
|
+
// target set. A hard-coded list can only ever name the trees ma-agents knows
|
|
1378
|
+
// about, and the defect was precisely a tree it did not — bmad-method wrote
|
|
1379
|
+
// Antigravity's skills to `.agent/skills` while this registry said
|
|
1380
|
+
// `.antigravity/skills`, so the on-prem phase-prefix pass walked straight past a
|
|
1381
|
+
// populated 166-skill tree and nothing failed. `discoverDeployedSkillTrees()`
|
|
1382
|
+
// below derives the real target set from what is on disk; the seed only survives
|
|
1383
|
+
// so a future registry entry outside the dot-container convention is still honoured.
|
|
1285
1384
|
const TOOL_SKILL_DIRS = Object.freeze([
|
|
1286
1385
|
path.join('.claude', 'skills'),
|
|
1386
|
+
path.join('.gemini', 'skills'),
|
|
1287
1387
|
path.join('.agents', 'skills'), // copilot, roo-code, kilocode (bmad-method 6.5.0)
|
|
1288
1388
|
path.join('.cline', 'skills'),
|
|
1389
|
+
path.join('.cursor', 'skills'),
|
|
1390
|
+
path.join('.agent', 'skills'), // antigravity (aligned to bmad-method upstream)
|
|
1391
|
+
path.join('.opencode', 'skills'),
|
|
1289
1392
|
]);
|
|
1290
1393
|
|
|
1394
|
+
// Directories never descended into while scanning for deployed skill trees.
|
|
1395
|
+
const DEPLOYED_TREE_SCAN_SKIP = Object.freeze(['.git', 'node_modules']);
|
|
1396
|
+
|
|
1397
|
+
// Skill directory names whose presence marks a `<container>/skills` directory as
|
|
1398
|
+
// a DEPLOYED persona tree rather than an ordinary folder called "skills". Both
|
|
1399
|
+
// persona families are listed: the `bmad-agent-*` set BMAD deploys and the
|
|
1400
|
+
// `ma-agent-*` set this package ships, so a tree carrying either is discovered.
|
|
1401
|
+
const DEPLOYED_TREE_PERSONA_MARKERS = Object.freeze([
|
|
1402
|
+
...Object.values(PERSONA_SKILL_MAP),
|
|
1403
|
+
'ma-agent-cyber',
|
|
1404
|
+
'ma-agent-devops',
|
|
1405
|
+
'ma-agent-sre',
|
|
1406
|
+
'ma-agent-sqa',
|
|
1407
|
+
]);
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* Derive the set of deployed skill trees present under `projectRoot` FROM DISK.
|
|
1411
|
+
*
|
|
1412
|
+
* A deployed tool skill tree is a directory `<container>/skills` (or
|
|
1413
|
+
* `<container>/<sub>/skills`, e.g. `.github/copilot/skills`) that contains at
|
|
1414
|
+
* least one persona skill directory with a `SKILL.md`. Any writer can create
|
|
1415
|
+
* one — ma-agents' own standalone loop, bmad-method's PluginResolver, or a tool
|
|
1416
|
+
* we have never registered — and post-deploy patches must reach all of them.
|
|
1417
|
+
*
|
|
1418
|
+
* The walk is deliberately confined to DOT-PREFIXED containers. Every
|
|
1419
|
+
* `target_dir` in bmad-method's `platform-codes.yaml` is dot-prefixed, so this
|
|
1420
|
+
* covers the whole upstream surface; it also guarantees the patcher can never
|
|
1421
|
+
* reach a SOURCE checkout's own skill directories (`lib/bmad-extension/skills/`,
|
|
1422
|
+
* `skills/`), which are shipped artifacts with pinned digests, not deployments.
|
|
1423
|
+
*
|
|
1424
|
+
* @param {string} projectRoot Absolute project root to scan
|
|
1425
|
+
* @param {string[]} [personaDirNames] Skill directory names that mark a tree as
|
|
1426
|
+
* a deployment target. Defaults to
|
|
1427
|
+
* DEPLOYED_TREE_PERSONA_MARKERS (both the
|
|
1428
|
+
* `bmad-agent-*` and `ma-agent-*` families);
|
|
1429
|
+
* callers may narrow it to the personas they
|
|
1430
|
+
* actually intend to patch.
|
|
1431
|
+
*
|
|
1432
|
+
* Known limit: the level-1 scan uses `Dirent.isDirectory()`, which is false for
|
|
1433
|
+
* a symlink. A SYMLINKED, UNREGISTERED tool container is therefore not
|
|
1434
|
+
* discovered; registered ones still are, because the seed pass stats (and so
|
|
1435
|
+
* follows) their paths.
|
|
1436
|
+
* @returns {string[]} Sorted tree paths relative to `projectRoot`, deduplicated
|
|
1437
|
+
* case-insensitively on win32 (where `.Claude/skills` and
|
|
1438
|
+
* `.claude/skills` are one physical directory) and
|
|
1439
|
+
* case-sensitively everywhere else
|
|
1440
|
+
*/
|
|
1441
|
+
function discoverDeployedSkillTrees(projectRoot, personaDirNames = DEPLOYED_TREE_PERSONA_MARKERS) {
|
|
1442
|
+
if (!projectRoot || typeof projectRoot !== 'string') return [];
|
|
1443
|
+
if (!Array.isArray(personaDirNames) || personaDirNames.length === 0) return [];
|
|
1444
|
+
|
|
1445
|
+
const isDeployedTree = (relDir) => {
|
|
1446
|
+
const abs = path.join(projectRoot, relDir);
|
|
1447
|
+
try {
|
|
1448
|
+
if (!fs.statSync(abs).isDirectory()) return false;
|
|
1449
|
+
} catch {
|
|
1450
|
+
return false;
|
|
1451
|
+
}
|
|
1452
|
+
// `isFile()`, not `existsSync()`: a DIRECTORY named `SKILL.md` would
|
|
1453
|
+
// otherwise mark the tree as a deployment target, and every consumer
|
|
1454
|
+
// then fails on EISDIR when it tries to read it. The read is guarded
|
|
1455
|
+
// (the error is caught and warned), so this is not a crash — but the
|
|
1456
|
+
// warning is misleading and the tree was never a deployment at all.
|
|
1457
|
+
return personaDirNames.some(name => {
|
|
1458
|
+
try {
|
|
1459
|
+
return fs.statSync(path.join(abs, name, 'SKILL.md')).isFile();
|
|
1460
|
+
} catch {
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
const subdirs = (rel) => {
|
|
1467
|
+
try {
|
|
1468
|
+
return fs.readdirSync(rel ? path.join(projectRoot, rel) : projectRoot, { withFileTypes: true })
|
|
1469
|
+
.filter(e => e.isDirectory())
|
|
1470
|
+
.map(e => e.name);
|
|
1471
|
+
} catch {
|
|
1472
|
+
return []; // unreadable dir — never fatal for a post-install pass
|
|
1473
|
+
}
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
// Keyed by the case-folded path on case-insensitive filesystems, holding the
|
|
1477
|
+
// FIRST spelling seen. On Windows `.Claude/skills` and `.claude/skills` are
|
|
1478
|
+
// ONE physical directory: the seed pass stats the registry spelling and
|
|
1479
|
+
// succeeds, then the on-disk scan reports whatever casing is actually on
|
|
1480
|
+
// disk, so a plain Set enumerates the same tree twice. The second pass is
|
|
1481
|
+
// idempotent (the marker block is replaced byte-for-byte), so this was never
|
|
1482
|
+
// a correctness bug — but it double-counts in every caller's result list.
|
|
1483
|
+
// Seeding runs first, so the registry's canonical spelling wins.
|
|
1484
|
+
// Confined to win32 deliberately: macOS is case-insensitive by default but
|
|
1485
|
+
// can be formatted case-sensitive, and we have no evidence to change
|
|
1486
|
+
// behaviour there.
|
|
1487
|
+
const found = new Map();
|
|
1488
|
+
const caseKey = (rel) => (process.platform === 'win32' ? rel.toLowerCase() : rel);
|
|
1489
|
+
const addTree = (rel) => {
|
|
1490
|
+
const key = caseKey(rel);
|
|
1491
|
+
if (!found.has(key)) found.set(key, rel);
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
// 1. Registry seed — honours any declared dir outside the dot convention.
|
|
1495
|
+
for (const rel of TOOL_SKILL_DIRS) {
|
|
1496
|
+
if (isDeployedTree(rel)) addTree(rel);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// 2. On-disk scan of dot-prefixed containers, one level of nesting deep.
|
|
1500
|
+
for (const lvl1 of subdirs('')) {
|
|
1501
|
+
if (!lvl1.startsWith('.') || DEPLOYED_TREE_SCAN_SKIP.includes(lvl1)) continue;
|
|
1502
|
+
const direct = path.join(lvl1, 'skills');
|
|
1503
|
+
if (isDeployedTree(direct)) addTree(direct);
|
|
1504
|
+
for (const lvl2 of subdirs(lvl1)) {
|
|
1505
|
+
if (lvl2 === 'skills' || DEPLOYED_TREE_SCAN_SKIP.includes(lvl2)) continue;
|
|
1506
|
+
const nested = path.join(lvl1, lvl2, 'skills');
|
|
1507
|
+
if (isDeployedTree(nested)) addTree(nested);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
return [...found.values()].sort();
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1291
1514
|
/**
|
|
1292
1515
|
* Build the marker-wrapped block that is injected into a persona SKILL.md.
|
|
1293
1516
|
* Kept as a single-source helper so the test assertions and the production
|
|
@@ -1420,10 +1643,16 @@ async function applyOnPremPhasePrefixToDeployedSkills(
|
|
|
1420
1643
|
}
|
|
1421
1644
|
if (personaPrefix.size === 0) return [];
|
|
1422
1645
|
|
|
1646
|
+
// Bug `antigravity-path-divergence` — the target set is derived FROM DISK,
|
|
1647
|
+
// not from TOOL_SKILL_DIRS. Walking a hard-coded registry list is exactly
|
|
1648
|
+
// how a populated `.agent/skills` tree went unpatched in an on-prem project
|
|
1649
|
+
// while `.claude`, `.agents` and `.cline` were patched: our registry had
|
|
1650
|
+
// never heard of it. Discovery is keyed on the personas we actually have a
|
|
1651
|
+
// prefix for, so a tree with none of them is not a target at all.
|
|
1423
1652
|
const results = [];
|
|
1424
|
-
|
|
1653
|
+
const targetTrees = discoverDeployedSkillTrees(projectRoot, [...personaPrefix.keys()]);
|
|
1654
|
+
for (const toolRel of targetTrees) {
|
|
1425
1655
|
const toolSkillsDir = path.join(projectRoot, toolRel);
|
|
1426
|
-
if (!fs.existsSync(toolSkillsDir)) continue; // tool not installed
|
|
1427
1656
|
|
|
1428
1657
|
for (const [skillDirName, prefix] of personaPrefix) {
|
|
1429
1658
|
const skillDir = path.join(toolSkillsDir, skillDirName);
|
|
@@ -2767,10 +2996,18 @@ function getCandidateSkillDirNames(customizeFilename) {
|
|
|
2767
2996
|
|
|
2768
2997
|
/**
|
|
2769
2998
|
* Enumerate the IDE/BMAD tool skill directories that may contain deployed
|
|
2770
|
-
* persona skills.
|
|
2771
|
-
*
|
|
2772
|
-
*
|
|
2773
|
-
*
|
|
2999
|
+
* persona skills. Returns absolute paths to directories that actually exist on
|
|
3000
|
+
* disk; non-existent tool dirs are silently skipped (the tool was not selected
|
|
3001
|
+
* for install).
|
|
3002
|
+
*
|
|
3003
|
+
* Two sources, unioned — bug `antigravity-path-divergence`:
|
|
3004
|
+
* 1. `lib/agents.js`, so the list stays in lockstep with the installer's own
|
|
3005
|
+
* fan-out (Story 22.10), including the `_bmad/skills/*` BMAD-persona roots
|
|
3006
|
+
* that the dot-container scan deliberately does not reach.
|
|
3007
|
+
* 2. `discoverDeployedSkillTrees()`, so a tree written by another pipeline that
|
|
3008
|
+
* our registry has never heard of is patched too. This is the same blind
|
|
3009
|
+
* spot that left a populated `.agent/skills` unpatched: a registry-only
|
|
3010
|
+
* derivation can only ever find the directories we already named.
|
|
2774
3011
|
*
|
|
2775
3012
|
* @param {string} projectRoot - Absolute path to the project root
|
|
2776
3013
|
* @returns {string[]} Absolute skills-dir paths that exist
|
|
@@ -2785,6 +3022,9 @@ function getDeployedSkillsRoots(projectRoot) {
|
|
|
2785
3022
|
roots.add(abs);
|
|
2786
3023
|
}
|
|
2787
3024
|
}
|
|
3025
|
+
for (const rel of discoverDeployedSkillTrees(projectRoot)) {
|
|
3026
|
+
roots.add(path.resolve(projectRoot, rel));
|
|
3027
|
+
}
|
|
2788
3028
|
return [...roots];
|
|
2789
3029
|
}
|
|
2790
3030
|
|
|
@@ -3479,6 +3719,9 @@ module.exports = {
|
|
|
3479
3719
|
buildOnPremPrefixBlock,
|
|
3480
3720
|
PERSONA_SKILL_MAP,
|
|
3481
3721
|
TOOL_SKILL_DIRS,
|
|
3722
|
+
// Bug `antigravity-path-divergence` — disk-derived patch-target set
|
|
3723
|
+
discoverDeployedSkillTrees,
|
|
3724
|
+
DEPLOYED_TREE_PERSONA_MARKERS,
|
|
3482
3725
|
ON_PREM_PREFIX_MARKER_BEGIN,
|
|
3483
3726
|
ON_PREM_PREFIX_MARKER_END,
|
|
3484
3727
|
// Offline-safe recompile helpers (exported for testing — see bug
|
|
@@ -3498,6 +3741,11 @@ module.exports = {
|
|
|
3498
3741
|
getInstallableBmadModules,
|
|
3499
3742
|
getInstalledBmadModules,
|
|
3500
3743
|
resolveBmadModules,
|
|
3744
|
+
// FR123 (amended 2026-08-02) — the default INSTALL set, distinct from the
|
|
3745
|
+
// bundled set. See the long note on DEFAULT_BMAD_MODULE_IDS.
|
|
3746
|
+
DEFAULT_BMAD_MODULE_IDS,
|
|
3747
|
+
NON_DEFAULT_BMAD_MODULE_IDS,
|
|
3748
|
+
getDefaultBmadModules,
|
|
3501
3749
|
// 3.12.2 — offline-fallback marker for bmad-method's cloneExternalModule
|
|
3502
3750
|
ensureCacheMarker,
|
|
3503
3751
|
};
|
package/lib/installer.js
CHANGED
|
@@ -2261,9 +2261,28 @@ const OBSOLETE_TOOL_SKILL_DIRS = Object.freeze([
|
|
|
2261
2261
|
// tool-specific paths to the shared .agents/skills/ directory.
|
|
2262
2262
|
{ obsolete: '.github/skills', replacedBy: '.agents/skills', sinceVersion: '3.8.1' },
|
|
2263
2263
|
{ obsolete: '.roo/skills', replacedBy: '.agents/skills', sinceVersion: '3.8.1' },
|
|
2264
|
-
{ obsolete: '.kilocode/skills', replacedBy: '.agents/skills', sinceVersion: '3.8.1' }
|
|
2264
|
+
{ obsolete: '.kilocode/skills', replacedBy: '.agents/skills', sinceVersion: '3.8.1' },
|
|
2265
|
+
// Bug `antigravity-path-divergence`: ma-agents declared `.antigravity/skills`
|
|
2266
|
+
// while bmad-method writes Antigravity's skills to `.agent/skills`
|
|
2267
|
+
// (platform-codes.yaml, platform `antigravity`). A single install therefore
|
|
2268
|
+
// produced two trees, and the one the tool actually reads was patched by
|
|
2269
|
+
// nobody. lib/agents.js is now aligned to upstream; this sweeps the tree
|
|
2270
|
+
// ma-agents itself created on the old path — and only once `.agent/skills`
|
|
2271
|
+
// has content, per migrateObsoleteToolDirs' preserve-on-empty rule.
|
|
2272
|
+
// APPEND-ONLY: entries above are positional in test/obsolete-tool-dirs.test.js
|
|
2273
|
+
// (results[0] must stay the copilot entry).
|
|
2274
|
+
{ obsolete: '.antigravity/skills', replacedBy: '.agent/skills', sinceVersion: '3.18.0' }
|
|
2265
2275
|
]);
|
|
2266
2276
|
|
|
2277
|
+
// Story 31.16 — the two bookkeeping files ma-agents itself writes into a skills
|
|
2278
|
+
// directory: `.ma-agents.json` (the per-install record, `writeManifest`) and
|
|
2279
|
+
// `MANIFEST.yaml` (derived from it, `generateSkillsManifest`). The obsolete-dir
|
|
2280
|
+
// sweep excludes these from its residue calculation, because neither is user
|
|
2281
|
+
// content and treating them as residue would make every manifested tree preserve
|
|
2282
|
+
// forever. Deliberately NOT exported: a test that took its expectation from this
|
|
2283
|
+
// constant would be asserting the implementation against itself.
|
|
2284
|
+
const OBSOLETE_SWEEP_MANIFEST_FILES = new Set([MANIFEST_FILE, 'MANIFEST.yaml']);
|
|
2285
|
+
|
|
2267
2286
|
async function migrateRetiredSkills(options = {}) {
|
|
2268
2287
|
const { scope = 'project', customPath = '' } = options;
|
|
2269
2288
|
const results = [];
|
|
@@ -2360,11 +2379,20 @@ async function migrateRetiredSkills(options = {}) {
|
|
|
2360
2379
|
* legacy directory exists under `projectRoot`. Behaviour per entry:
|
|
2361
2380
|
*
|
|
2362
2381
|
* - Obsolete dir absent → no-op (idempotent)
|
|
2363
|
-
* - Obsolete dir present AND
|
|
2364
|
-
* replacedBy dir has content → remove obsolete dir, log info message
|
|
2365
2382
|
* - Obsolete dir present BUT
|
|
2366
2383
|
* replacedBy dir is empty/gone → preserve obsolete dir, log warning
|
|
2367
2384
|
* (user may have local edits; safer to keep)
|
|
2385
|
+
* - Obsolete dir present AND replacedBy dir has content → Story 31.16 decides
|
|
2386
|
+
* per child, using the obsolete dir's OWN `.ma-agents.json::skills` as the
|
|
2387
|
+
* record of what ma-agents installed there:
|
|
2388
|
+
* owned = manifest skill ids (each id IS a directory basename)
|
|
2389
|
+
* residue = readdir(obsolete) − owned − {.ma-agents.json, MANIFEST.yaml}
|
|
2390
|
+
* residue empty → remove the whole dir → action 'removed'
|
|
2391
|
+
* residue non-empty → remove owned children, KEEP → action 'preserved-partial'
|
|
2392
|
+
* the dir, and name the residue
|
|
2393
|
+
* No manifest, an unparseable one, or `skills: {}` all yield an empty
|
|
2394
|
+
* `owned`, so the whole directory is residue and is preserved. Unknown
|
|
2395
|
+
* provenance fails safe; nothing is special-cased.
|
|
2368
2396
|
*
|
|
2369
2397
|
* @param {string} projectRoot — absolute path to the project root (process.cwd() in production)
|
|
2370
2398
|
* @returns {Promise<Array<{entry, action, obsoletePath, replacedByPath, error}>>}
|
|
@@ -2391,13 +2419,101 @@ async function migrateObsoleteToolDirs(projectRoot) {
|
|
|
2391
2419
|
}
|
|
2392
2420
|
|
|
2393
2421
|
if (replacedByHasContent) {
|
|
2394
|
-
//
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2422
|
+
// Story 31.16 — the sweep is manifest-aware. Removing the whole
|
|
2423
|
+
// directory here destroyed hand-authored skills that ma-agents never
|
|
2424
|
+
// installed and never migrated anywhere (bug
|
|
2425
|
+
// `obsolete-tool-dir-sweep-deletes-user-authored-skills`).
|
|
2426
|
+
//
|
|
2427
|
+
// The directory's own `.ma-agents.json::skills` is ma-agents' record of
|
|
2428
|
+
// exactly what it installed there, and a manifest key IS the on-disk
|
|
2429
|
+
// directory basename (`performInstall` installs to
|
|
2430
|
+
// `path.join(installPath, skillId)`). It is a POSITIVE record only:
|
|
2431
|
+
// manifest-recorded implies ma-agents installed it, but the converse
|
|
2432
|
+
// does NOT hold — many real trees carry no per-tree manifest at all,
|
|
2433
|
+
// because bmad-method's PluginResolver also writes skills into these
|
|
2434
|
+
// trees and writes no manifest. So anything unrecorded is content of
|
|
2435
|
+
// unknown provenance and is preserved. This deliberately
|
|
2436
|
+
// over-preserves: a stale directory costs the user the `rm -rf` we
|
|
2437
|
+
// already print, a deleted hand-written skill costs them the file.
|
|
2438
|
+
const obsoleteManifest = readManifest(obsoletePath);
|
|
2439
|
+
const owned = obsoleteManifest && obsoleteManifest.skills
|
|
2440
|
+
&& typeof obsoleteManifest.skills === 'object'
|
|
2441
|
+
? Object.keys(obsoleteManifest.skills)
|
|
2442
|
+
: [];
|
|
2443
|
+
const ownedSet = new Set(owned);
|
|
2444
|
+
|
|
2445
|
+
// The two manifest files are ma-agents' own bookkeeping. They are
|
|
2446
|
+
// EXCLUDED from residue rather than counted as owned: counting them as
|
|
2447
|
+
// residue would make every manifested tree preserve forever and kill
|
|
2448
|
+
// the cleanup outright. They are deleted only as part of a
|
|
2449
|
+
// whole-directory removal — on the preserve path they are left
|
|
2450
|
+
// byte-unchanged, so a second run recomputes the same `owned` set and
|
|
2451
|
+
// behaves identically to the first.
|
|
2452
|
+
//
|
|
2453
|
+
// `entries` is the single source of truth for what is actually on
|
|
2454
|
+
// disk here, and BOTH halves of the decision below are taken against
|
|
2455
|
+
// it. A manifest key is file CONTENT, never a path: it is only ever
|
|
2456
|
+
// matched against this listing, never joined onto `obsoletePath` and
|
|
2457
|
+
// trusted. See the removal loop below for why that matters.
|
|
2458
|
+
const entries = fs.readdirSync(obsoletePath);
|
|
2459
|
+
const residue = entries
|
|
2460
|
+
.filter(name => !ownedSet.has(name) && !OBSOLETE_SWEEP_MANIFEST_FILES.has(name));
|
|
2461
|
+
|
|
2462
|
+
if (residue.length === 0) {
|
|
2463
|
+
// Everything present is ma-agents' own — safe to remove, skills have
|
|
2464
|
+
// been migrated to the new location.
|
|
2465
|
+
await fs.remove(obsoletePath);
|
|
2466
|
+
action = 'removed';
|
|
2467
|
+
console.log(chalk.green(
|
|
2468
|
+
` - Removed legacy tool skills directory ${entry.obsolete} ` +
|
|
2469
|
+
`(moved to ${entry.replacedBy} in v${entry.sinceVersion})`
|
|
2470
|
+
));
|
|
2471
|
+
} else {
|
|
2472
|
+
// Something here is not ours. Remove only what the manifest records
|
|
2473
|
+
// AND what the directory listing actually contains, and keep the
|
|
2474
|
+
// directory, naming what caused the preservation.
|
|
2475
|
+
//
|
|
2476
|
+
// `entries.includes(id)` is not an optimisation — it is the safety
|
|
2477
|
+
// property. Joining a raw manifest key onto `obsoletePath` and
|
|
2478
|
+
// removing the result trusts file content as a filesystem path, and
|
|
2479
|
+
// two concrete destruction paths follow from that:
|
|
2480
|
+
//
|
|
2481
|
+
// 1. CASE. `residue` is computed with case-SENSITIVE set
|
|
2482
|
+
// membership, but `fs.remove` resolves case-INSENSITIVELY on
|
|
2483
|
+
// NTFS and APFS. An on-disk `Code-Review` against a manifest
|
|
2484
|
+
// key `code-review` is therefore reported as residue ("items
|
|
2485
|
+
// ma-agents did not install"), the directory is declared
|
|
2486
|
+
// preserved — and the user's directory is deleted in the same
|
|
2487
|
+
// breath. `entries.includes` is case-sensitive, so the two
|
|
2488
|
+
// halves of the decision can no longer disagree.
|
|
2489
|
+
//
|
|
2490
|
+
// 2. CONTAINMENT. A manifest key is not validated to be a
|
|
2491
|
+
// basename. A `skills` map holding `../../../victim` escapes
|
|
2492
|
+
// the obsolete directory entirely and deletes an unrelated
|
|
2493
|
+
// path under the project root — while still taking THIS
|
|
2494
|
+
// branch, because a key matching nothing in `readdir` counts
|
|
2495
|
+
// as neither owned-present nor residue. A name that came out
|
|
2496
|
+
// of `readdir(obsoletePath)` can only ever be a real, direct
|
|
2497
|
+
// child of that directory, so no traversal survives the
|
|
2498
|
+
// filter. This function's stated premise is that provenance is
|
|
2499
|
+
// unknown and must fail safe; "the manifest is normally
|
|
2500
|
+
// ma-agents-written" is not a defence inside it.
|
|
2501
|
+
//
|
|
2502
|
+
// A recorded id with no directory on disk is now an explicit no-op
|
|
2503
|
+
// — it is filtered out rather than being an incidentally harmless
|
|
2504
|
+
// `fs.remove` of a non-existent path — which keeps this idempotent.
|
|
2505
|
+
for (const skillId of owned.filter(id => entries.includes(id))) {
|
|
2506
|
+
await fs.remove(path.join(obsoletePath, skillId));
|
|
2507
|
+
}
|
|
2508
|
+
action = 'preserved-partial';
|
|
2509
|
+
console.log(chalk.yellow(
|
|
2510
|
+
` ! Legacy tool skills directory ${entry.obsolete} was preserved ` +
|
|
2511
|
+
`because it contains ${residue.length} item(s) ma-agents did not install: ` +
|
|
2512
|
+
`${residue.join(', ')}. ` +
|
|
2513
|
+
`Remove it manually once you have confirmed there is nothing to keep: ` +
|
|
2514
|
+
`rm -rf ${entry.obsolete}`
|
|
2515
|
+
));
|
|
2516
|
+
}
|
|
2401
2517
|
} else {
|
|
2402
2518
|
// Replacement is absent or empty — do not silently destroy user content.
|
|
2403
2519
|
action = 'preserved';
|
|
@@ -2695,6 +2811,12 @@ module.exports = {
|
|
|
2695
2811
|
getStatus,
|
|
2696
2812
|
readManifest,
|
|
2697
2813
|
ensureManifest,
|
|
2814
|
+
// Story 31.16 — exported so the obsolete-tool-dir tests can build their
|
|
2815
|
+
// fixtures through the REAL manifest writer instead of hand-writing the
|
|
2816
|
+
// `.ma-agents.json` shape. Behaviour unchanged; this is an export, not a new
|
|
2817
|
+
// code path. `readManifest` and `ensureManifest` were already exported for the
|
|
2818
|
+
// same reason.
|
|
2819
|
+
writeManifest,
|
|
2698
2820
|
getInstalledSkillInfo,
|
|
2699
2821
|
getManifestAgents,
|
|
2700
2822
|
compareSemver,
|
package/package.json
CHANGED