ma-agents 3.17.0-beta.2 → 3.17.0-beta.3
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.
|
@@ -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.17.0-beta.
|
|
40
|
+
"version": "3.17.0-beta.3",
|
|
41
41
|
"author": {
|
|
42
42
|
"name": "Alon Mayaffit"
|
|
43
43
|
},
|
|
@@ -478,6 +478,64 @@ async function scanDirectModeSkillDirs(rootDir) {
|
|
|
478
478
|
return scanSkillDirs(rootDir);
|
|
479
479
|
}
|
|
480
480
|
|
|
481
|
+
/**
|
|
482
|
+
* Recursively collect every directory that contains a `SKILL.md`, returned as
|
|
483
|
+
* POSIX paths RELATIVE to `rootDirAbs`. Unlike {@link scanSkillDirs} (DIRECT
|
|
484
|
+
* children only), this walks the whole subtree: real BMAD modules nest skills
|
|
485
|
+
* under `agents/`, `workflows/<phase>/…`, etc. A dir that IS a skill (has
|
|
486
|
+
* SKILL.md) is recorded and not descended into (skills don't nest in skills);
|
|
487
|
+
* `node_modules`/`.git` are skipped.
|
|
488
|
+
*/
|
|
489
|
+
async function findSkillDirsRecursive(baseDirAbs, rootDirAbs) {
|
|
490
|
+
const found = [];
|
|
491
|
+
async function walk(dirAbs) {
|
|
492
|
+
if (await fs.pathExists(path.join(dirAbs, 'SKILL.md'))) {
|
|
493
|
+
found.push(toPosix(path.relative(rootDirAbs, dirAbs)));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
let entries;
|
|
497
|
+
try {
|
|
498
|
+
entries = await fs.readdir(dirAbs, { withFileTypes: true });
|
|
499
|
+
} catch {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
for (const entry of entries) {
|
|
503
|
+
if (!entry.isDirectory()) continue;
|
|
504
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
505
|
+
await walk(path.join(dirAbs, entry.name));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
await walk(baseDirAbs);
|
|
509
|
+
return found.sort();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Determine which directory to scan for a registry entry's skills, given the
|
|
514
|
+
* cloned repo root and the entry's `module_definition` (repo-relative path to
|
|
515
|
+
* its module.yaml). Skills are NOT always beside module.yaml:
|
|
516
|
+
* - module.yaml at the skills' parent (`skills/module.yaml`,
|
|
517
|
+
* `src/core-skills/module.yaml`, `src/module.yaml`) → scan THAT dir.
|
|
518
|
+
* - module.yaml inside a `<name>-setup/assets/module.yaml` (bmad-method's
|
|
519
|
+
* PluginResolver "setup skill" layout, e.g. suno-band-manager) → the
|
|
520
|
+
* module's skills are SIBLINGS of the -setup skill, so scan the setup
|
|
521
|
+
* skill's PARENT.
|
|
522
|
+
* Returns a POSIX path relative to the repo root ('.' = repo root). No
|
|
523
|
+
* module_definition → the repo root (direct-style scan).
|
|
524
|
+
*/
|
|
525
|
+
async function resolveRegistrySkillsRoot(entryRootDir, moduleDefPosix) {
|
|
526
|
+
if (!moduleDefPosix) return '.';
|
|
527
|
+
const mdDirPosix = path.posix.dirname(moduleDefPosix);
|
|
528
|
+
if (path.posix.basename(mdDirPosix) === 'assets') {
|
|
529
|
+
const setupSkillPosix = path.posix.dirname(mdDirPosix);
|
|
530
|
+
const setupSkillAbs = path.join(entryRootDir, ...setupSkillPosix.split('/'));
|
|
531
|
+
if (setupSkillPosix && (await fs.pathExists(path.join(setupSkillAbs, 'SKILL.md')))) {
|
|
532
|
+
const parentPosix = path.posix.dirname(setupSkillPosix);
|
|
533
|
+
return parentPosix === '' || parentPosix === '.' ? '.' : parentPosix;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return mdDirPosix === '' || mdDirPosix === '.' ? '.' : mdDirPosix;
|
|
537
|
+
}
|
|
538
|
+
|
|
481
539
|
/**
|
|
482
540
|
* Resolve every selected plugin's installable skill/module layout via
|
|
483
541
|
* bmad-method's own `CustomModuleManager#resolvePlugin` (the pinned-internal
|
|
@@ -784,10 +842,10 @@ async function resolveRegistryEntry(plugin, resolveSourceFn, resolvePluginFn) {
|
|
|
784
842
|
// repo-relative path to the module's module.yaml; its directory is the
|
|
785
843
|
// module root. Absent → fall back to the repo root (PluginResolver over the
|
|
786
844
|
// whole clone), matching the AC3 fallback.
|
|
787
|
-
let
|
|
845
|
+
let moduleDefPosix = null;
|
|
788
846
|
if (plugin.moduleDefinition) {
|
|
789
|
-
|
|
790
|
-
const moduleYamlAbs = path.join(entryRootDir, ...
|
|
847
|
+
moduleDefPosix = toPosix(plugin.moduleDefinition).replace(/^\.\//, '');
|
|
848
|
+
const moduleYamlAbs = path.join(entryRootDir, ...moduleDefPosix.split('/'));
|
|
791
849
|
if (!(await fs.pathExists(moduleYamlAbs))) {
|
|
792
850
|
throw new CustomMarketplaceError(
|
|
793
851
|
`Registry entry '${plugin.code}': module_definition "${plugin.moduleDefinition}" ` +
|
|
@@ -795,27 +853,33 @@ async function resolveRegistryEntry(plugin, resolveSourceFn, resolvePluginFn) {
|
|
|
795
853
|
`repository layout changed; aborting before staging (FR238).`
|
|
796
854
|
);
|
|
797
855
|
}
|
|
798
|
-
relModuleDirPosix = path.posix.dirname(defPosix);
|
|
799
856
|
}
|
|
800
857
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
858
|
+
// Skills are NOT necessarily beside module.yaml — resolve the module's real
|
|
859
|
+
// skills root (handles the setup-skill/assets layout and modules whose
|
|
860
|
+
// module.yaml sits at the skills' parent) and scan it RECURSIVELY. A shallow
|
|
861
|
+
// scan of module.yaml's own dir silently missed nested skills (gds/bmm) and
|
|
862
|
+
// the setup-skill layout entirely (suno), dropping those modules.
|
|
863
|
+
const skillsRootPosix = await resolveRegistrySkillsRoot(entryRootDir, moduleDefPosix);
|
|
864
|
+
const skillsRootAbs =
|
|
865
|
+
skillsRootPosix === '.' ? entryRootDir : path.join(entryRootDir, ...skillsRootPosix.split('/'));
|
|
866
|
+
const skillRelPaths = await findSkillDirsRecursive(skillsRootAbs, entryRootDir);
|
|
805
867
|
diag('custom-source: registry entry resolved', {
|
|
806
868
|
code: plugin.code,
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
869
|
+
moduleDef: moduleDefPosix,
|
|
870
|
+
skillsRoot: skillsRootPosix,
|
|
871
|
+
skillDirsFound: skillRelPaths.length,
|
|
872
|
+
skills: skillRelPaths,
|
|
810
873
|
});
|
|
811
874
|
|
|
812
875
|
const rawPlugin = {
|
|
813
876
|
name: plugin.code,
|
|
814
877
|
description: plugin.description || '',
|
|
815
878
|
version: plugin.version || null,
|
|
816
|
-
source:
|
|
817
|
-
//
|
|
818
|
-
// returns [] and the
|
|
879
|
+
source: skillsRootPosix,
|
|
880
|
+
// Skill dirs relative to the repo root (what PluginResolver.resolve joins
|
|
881
|
+
// against repoPath). Empty → resolvePlugin returns [] and the selected
|
|
882
|
+
// entry surfaces in the per-entry guard below (never a silent drop).
|
|
819
883
|
skills: skillRelPaths,
|
|
820
884
|
};
|
|
821
885
|
|
|
@@ -884,6 +948,26 @@ async function stageRegistrySubset({ source, plugins, selectedCodes, projectRoot
|
|
|
884
948
|
entries.push(await resolveRegistryEntry(plugin, resolveSourceFn, resolvePluginFn));
|
|
885
949
|
}
|
|
886
950
|
|
|
951
|
+
// Per-entry LOUD guard: a SELECTED entry that resolves to zero modules must
|
|
952
|
+
// NOT be silently dropped while others install (which read as an overall
|
|
953
|
+
// "success"). Fail before staging, naming exactly which entries produced
|
|
954
|
+
// nothing — the user picked them explicitly, so a partial install is a
|
|
955
|
+
// fail-clean violation (FR238), not an acceptable default.
|
|
956
|
+
const emptyEntries = entries.filter(({ resolved }) => !resolved || resolved.length === 0);
|
|
957
|
+
if (emptyEntries.length > 0) {
|
|
958
|
+
const names = emptyEntries.map(({ plugin }) => plugin.code).join(', ');
|
|
959
|
+
diag('custom-source: registry entries resolved to zero modules', {
|
|
960
|
+
entries: emptyEntries.map(({ plugin }) => ({ code: plugin.code, repository: plugin.repository, moduleDefinition: plugin.moduleDefinition || null })),
|
|
961
|
+
});
|
|
962
|
+
throw new CustomMarketplaceError(
|
|
963
|
+
`Selected registry ${emptyEntries.length === 1 ? 'entry' : 'entries'} ${names} resolved to ` +
|
|
964
|
+
`zero installable modules — no SKILL.md directories were found for ${emptyEntries.length === 1 ? 'it' : 'them'} ` +
|
|
965
|
+
`(a stale registry pointer, a changed repo layout, or a module with no skills). ` +
|
|
966
|
+
`Aborting before staging so the selection is not silently partially installed (FR238). ` +
|
|
967
|
+
`Re-run and deselect ${emptyEntries.length === 1 ? 'it' : 'those'}, or report the registry entry as broken.`
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
|
|
887
971
|
const allModules = assertResolutionNonEmptyAndUniqueLeaves(
|
|
888
972
|
entries.map(({ plugin, rawPlugin, resolved }) => ({ plugin, rawPlugin, resolved })),
|
|
889
973
|
{ source, mode: 'registry' }
|
|
@@ -981,6 +1065,8 @@ module.exports = {
|
|
|
981
1065
|
CUSTOM_MARKETPLACE_STAGE_DIR_NAME,
|
|
982
1066
|
scanDirectModeSkillDirs,
|
|
983
1067
|
scanSkillDirs,
|
|
1068
|
+
findSkillDirsRecursive,
|
|
1069
|
+
resolveRegistrySkillsRoot,
|
|
984
1070
|
resolveSelectedPlugins,
|
|
985
1071
|
assertResolutionNonEmptyAndUniqueLeaves,
|
|
986
1072
|
stageCustomMarketplaceSubset,
|
package/package.json
CHANGED