ma-agents 3.17.0-beta.2 → 3.17.0-beta.4
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.4",
|
|
41
41
|
"author": {
|
|
42
42
|
"name": "Alon Mayaffit"
|
|
43
43
|
},
|
package/lib/bmad.js
CHANGED
|
@@ -1719,12 +1719,19 @@ async function installCustomMarketplaceSubset({ projectRoot, modules, tools, sel
|
|
|
1719
1719
|
let stagePath;
|
|
1720
1720
|
let customSourceArg;
|
|
1721
1721
|
let resolvedModules;
|
|
1722
|
+
// Registry mode is best-effort across entries: `installedCodes` is the subset
|
|
1723
|
+
// that actually resolved, `skipped` names the ones that did not (with reasons).
|
|
1724
|
+
// discovery/direct are atomic → installedCodes = the selection, no skips.
|
|
1725
|
+
let installedCodes = selection.selectedCodes;
|
|
1726
|
+
let skipped = [];
|
|
1722
1727
|
try {
|
|
1723
1728
|
if (selection.mode === 'registry') {
|
|
1724
1729
|
// Registry-index mode (this story): two-level clone+resolve+stage of
|
|
1725
1730
|
// each selected entry's own repository. Shares the same stage dir +
|
|
1726
1731
|
// install path as discovery/direct; only the staging step differs.
|
|
1727
|
-
|
|
1732
|
+
// Best-effort: a stale/broken entry is skipped (loudly), not fatal —
|
|
1733
|
+
// it does not block the entries that do resolve.
|
|
1734
|
+
({ stagePath, customSourceArg, resolvedModules, installedCodes, skipped } = await stageRegistryFn({
|
|
1728
1735
|
source: selection.source,
|
|
1729
1736
|
plugins: selection.plugins,
|
|
1730
1737
|
selectedCodes: selection.selectedCodes,
|
|
@@ -1747,6 +1754,11 @@ async function installCustomMarketplaceSubset({ projectRoot, modules, tools, sel
|
|
|
1747
1754
|
return false;
|
|
1748
1755
|
}
|
|
1749
1756
|
|
|
1757
|
+
// Defensive: a stager that predates the best-effort contract (or a partial
|
|
1758
|
+
// mock) may omit these — fall back to treating the whole selection as installed.
|
|
1759
|
+
if (!Array.isArray(installedCodes)) installedCodes = selection.selectedCodes;
|
|
1760
|
+
if (!Array.isArray(skipped)) skipped = [];
|
|
1761
|
+
|
|
1750
1762
|
const command = buildBmadArgs({
|
|
1751
1763
|
projectRoot,
|
|
1752
1764
|
modules,
|
|
@@ -1755,7 +1767,7 @@ async function installCustomMarketplaceSubset({ projectRoot, modules, tools, sel
|
|
|
1755
1767
|
customSource: customSourceArg,
|
|
1756
1768
|
});
|
|
1757
1769
|
|
|
1758
|
-
console.log(chalk.cyan(` Installing custom marketplace subset (${
|
|
1770
|
+
console.log(chalk.cyan(` Installing custom marketplace subset (${(installedCodes || []).join(', ')})...`));
|
|
1759
1771
|
console.log(chalk.gray(` Running: ${command}`));
|
|
1760
1772
|
|
|
1761
1773
|
try {
|
|
@@ -1768,6 +1780,27 @@ async function installCustomMarketplaceSubset({ projectRoot, modules, tools, sel
|
|
|
1768
1780
|
// for diagnostics on any post-invoke failure" policy (AC 8, 30.6b).
|
|
1769
1781
|
cleanupStageFn(stagePath);
|
|
1770
1782
|
|
|
1783
|
+
// LOUD best-effort report: any selected registry entry that could not be
|
|
1784
|
+
// installed (unreachable repo, stale approved_tag, no skills) is named
|
|
1785
|
+
// here with its reason — never silently omitted (FR238).
|
|
1786
|
+
if (skipped && skipped.length > 0) {
|
|
1787
|
+
console.log(
|
|
1788
|
+
chalk.yellow(
|
|
1789
|
+
`\n ${skipped.length} selected extension${skipped.length === 1 ? ' was' : 's were'} NOT installed ` +
|
|
1790
|
+
`(the rest installed fine):`
|
|
1791
|
+
)
|
|
1792
|
+
);
|
|
1793
|
+
for (const s of skipped) {
|
|
1794
|
+
console.log(chalk.yellow(` - ${s.code}: ${s.reason}`));
|
|
1795
|
+
}
|
|
1796
|
+
console.log(
|
|
1797
|
+
chalk.gray(
|
|
1798
|
+
' These are typically stale/broken registry entries (e.g. an approved_tag that no longer ' +
|
|
1799
|
+
'exists upstream). Report them to the marketplace maintainer, or re-run without them.'
|
|
1800
|
+
)
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1771
1804
|
// Story 30.7 (FR242) — persist source + selection (+ SHA pin) now that
|
|
1772
1805
|
// the subset is actually installed. `resolvedModules[].cloneSha` is
|
|
1773
1806
|
// only stamped for Git-URL sources (see custom-module-manager.js
|
|
@@ -1776,19 +1809,22 @@ async function installCustomMarketplaceSubset({ projectRoot, modules, tools, sel
|
|
|
1776
1809
|
// failure is logged as a warning only — it must never turn an
|
|
1777
1810
|
// otherwise-successful install into a reported failure.
|
|
1778
1811
|
try {
|
|
1812
|
+
// Persist only what ACTUALLY installed (`installedCodes`), not the
|
|
1813
|
+
// raw selection — otherwise a skipped/broken entry would be recorded
|
|
1814
|
+
// as installed and re-attempted (and re-reported) on every update.
|
|
1779
1815
|
const persistEntry = {
|
|
1780
1816
|
source: selection.source,
|
|
1781
1817
|
sourceUrl: selection.sourceUrl ?? null,
|
|
1782
1818
|
mode: selection.mode,
|
|
1783
|
-
selectedCodes:
|
|
1819
|
+
selectedCodes: installedCodes,
|
|
1784
1820
|
};
|
|
1785
1821
|
if (selection.mode === 'registry') {
|
|
1786
|
-
// AC5 — record each
|
|
1822
|
+
// AC5 — record each INSTALLED entry's registry-provided
|
|
1787
1823
|
// `approved_sha` (the immutable pin the registry declares), plus
|
|
1788
1824
|
// its `approved_tag`. `sha` (the top-level field) mirrors the
|
|
1789
1825
|
// first available approved_sha for back-compat with readers that
|
|
1790
1826
|
// only look at `sha`.
|
|
1791
|
-
const selectedSet = new Set(
|
|
1827
|
+
const selectedSet = new Set(installedCodes);
|
|
1792
1828
|
const registryEntries = (selection.plugins || [])
|
|
1793
1829
|
.filter((p) => selectedSet.has(p.code))
|
|
1794
1830
|
.map((p) => ({ code: p.code, approvedSha: p.approvedSha ?? null, approvedTag: p.approvedTag ?? null }));
|
|
@@ -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
|
|
|
@@ -877,11 +941,45 @@ async function stageRegistrySubset({ source, plugins, selectedCodes, projectRoot
|
|
|
877
941
|
seenCodes.add(p.code);
|
|
878
942
|
}
|
|
879
943
|
|
|
880
|
-
// Resolve
|
|
881
|
-
// stage dir
|
|
944
|
+
// Resolve each selected entry INDEPENDENTLY (each from its own clone) BEFORE
|
|
945
|
+
// touching the stage dir. A community registry legitimately contains
|
|
946
|
+
// individually-broken/stale entries — an unreachable repo, a nonexistent
|
|
947
|
+
// approved_tag (e.g. the marketplace pinning wds@v0.1.0 when that tag does
|
|
948
|
+
// not exist), or a module with no skills. One bad entry must NOT nuke the
|
|
949
|
+
// whole multi-select. Best-effort: keep the resolvable entries, collect the
|
|
950
|
+
// skipped ones (with reasons), install the good ones, and let the caller
|
|
951
|
+
// report the skips LOUDLY. FR238 = no SILENT partial install — NOT
|
|
952
|
+
// all-or-nothing.
|
|
882
953
|
const entries = [];
|
|
954
|
+
const skipped = [];
|
|
883
955
|
for (const plugin of selected) {
|
|
884
|
-
|
|
956
|
+
let entry;
|
|
957
|
+
try {
|
|
958
|
+
entry = await resolveRegistryEntry(plugin, resolveSourceFn, resolvePluginFn);
|
|
959
|
+
} catch (err) {
|
|
960
|
+
diag('custom-source: registry entry skipped (resolve failed)', { code: plugin.code, error: err.message });
|
|
961
|
+
skipped.push({ code: plugin.code, reason: err.message });
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (!entry.resolved || entry.resolved.length === 0) {
|
|
965
|
+
const reason =
|
|
966
|
+
'resolved to zero installable modules — no SKILL.md directories found ' +
|
|
967
|
+
'(stale registry pointer, changed repo layout, or a module with no skills)';
|
|
968
|
+
diag('custom-source: registry entry skipped (zero modules)', { code: plugin.code, repository: plugin.repository });
|
|
969
|
+
skipped.push({ code: plugin.code, reason });
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
entries.push(entry);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Hard-fail ONLY when nothing at all resolved — otherwise proceed with the
|
|
976
|
+
// resolvable subset and surface `skipped` to the caller for a loud report.
|
|
977
|
+
if (entries.length === 0) {
|
|
978
|
+
const detail = skipped.map((s) => `${s.code} (${s.reason})`).join('; ');
|
|
979
|
+
throw new CustomMarketplaceError(
|
|
980
|
+
`None of the ${selected.length} selected registry ${selected.length === 1 ? 'entry' : 'entries'} could be installed — ${detail}. ` +
|
|
981
|
+
`Nothing was staged (FR238).`
|
|
982
|
+
);
|
|
885
983
|
}
|
|
886
984
|
|
|
887
985
|
const allModules = assertResolutionNonEmptyAndUniqueLeaves(
|
|
@@ -937,7 +1035,13 @@ async function stageRegistrySubset({ source, plugins, selectedCodes, projectRoot
|
|
|
937
1035
|
);
|
|
938
1036
|
|
|
939
1037
|
const customSourceArg = `./${CUSTOM_MARKETPLACE_STAGE_DIR_NAME}`;
|
|
940
|
-
return {
|
|
1038
|
+
return {
|
|
1039
|
+
stagePath,
|
|
1040
|
+
customSourceArg,
|
|
1041
|
+
resolvedModules: allModules,
|
|
1042
|
+
installedCodes: entries.map((e) => e.plugin.code),
|
|
1043
|
+
skipped,
|
|
1044
|
+
};
|
|
941
1045
|
}
|
|
942
1046
|
|
|
943
1047
|
// ─── Story 30.7 — persist + re-enumerate/reconcile on update ───────────────
|
|
@@ -981,6 +1085,8 @@ module.exports = {
|
|
|
981
1085
|
CUSTOM_MARKETPLACE_STAGE_DIR_NAME,
|
|
982
1086
|
scanDirectModeSkillDirs,
|
|
983
1087
|
scanSkillDirs,
|
|
1088
|
+
findSkillDirsRecursive,
|
|
1089
|
+
resolveRegistrySkillsRoot,
|
|
984
1090
|
resolveSelectedPlugins,
|
|
985
1091
|
assertResolutionNonEmptyAndUniqueLeaves,
|
|
986
1092
|
stageCustomMarketplaceSubset,
|
package/package.json
CHANGED