ma-agents 3.17.0-beta.3 → 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.3",
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
- ({ stagePath, customSourceArg, resolvedModules } = await stageRegistryFn({
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 (${selection.selectedCodes.join(', ')})...`));
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: selection.selectedCodes,
1819
+ selectedCodes: installedCodes,
1784
1820
  };
1785
1821
  if (selection.mode === 'registry') {
1786
- // AC5 — record each selected entry's registry-provided
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(selection.selectedCodes);
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 }));
@@ -941,30 +941,44 @@ async function stageRegistrySubset({ source, plugins, selectedCodes, projectRoot
941
941
  seenCodes.add(p.code);
942
942
  }
943
943
 
944
- // Resolve every selected entry (each from its own clone) BEFORE touching the
945
- // stage dir, so an unreachable/stale entry aborts clean with nothing staged.
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.
946
953
  const entries = [];
954
+ const skipped = [];
947
955
  for (const plugin of selected) {
948
- entries.push(await resolveRegistryEntry(plugin, resolveSourceFn, resolvePluginFn));
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);
949
973
  }
950
974
 
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
- });
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('; ');
962
979
  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.`
980
+ `None of the ${selected.length} selected registry ${selected.length === 1 ? 'entry' : 'entries'} could be installed — ${detail}. ` +
981
+ `Nothing was staged (FR238).`
968
982
  );
969
983
  }
970
984
 
@@ -1021,7 +1035,13 @@ async function stageRegistrySubset({ source, plugins, selectedCodes, projectRoot
1021
1035
  );
1022
1036
 
1023
1037
  const customSourceArg = `./${CUSTOM_MARKETPLACE_STAGE_DIR_NAME}`;
1024
- return { stagePath, customSourceArg, resolvedModules: allModules };
1038
+ return {
1039
+ stagePath,
1040
+ customSourceArg,
1041
+ resolvedModules: allModules,
1042
+ installedCodes: entries.map((e) => e.plugin.code),
1043
+ skipped,
1044
+ };
1025
1045
  }
1026
1046
 
1027
1047
  // ─── Story 30.7 — persist + re-enumerate/reconcile on update ───────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ma-agents",
3
- "version": "3.17.0-beta.3",
3
+ "version": "3.17.0-beta.4",
4
4
  "description": "NPX tool to install skills for AI coding agents (Claude Code, Gemini, Copilot, Kilocode, Cline, Cursor, Roo Code)",
5
5
  "main": "index.js",
6
6
  "bin": {