ma-agents 3.17.0-beta.1 → 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.
package/bin/cli.js CHANGED
@@ -40,6 +40,7 @@ const { uninstallProfileArtifacts } = require('../lib/uninstall');
40
40
  const bmad = require('../lib/bmad');
41
41
  const { getBmadPlatformCode } = require('../lib/agents');
42
42
  const { enumerateCustomSource, NFR71_WARNING, CustomMarketplaceError, reconcileCustomMarketplaceSelection, registryTrustLabel } = require('../lib/custom-marketplace');
43
+ const { setLogSink, diag, diagError } = require('../lib/diag');
43
44
  const { handleCreateSkill, handleValidateSkill, handleSetMandatory, handleCustomizeAgent, handleCreateAgent } = require('../lib/skill-authoring');
44
45
 
45
46
  const PKG = require('../package.json');
@@ -55,9 +56,21 @@ const VERSION = PKG.version;
55
56
  */
56
57
  function setupLogging(logFilePath) {
57
58
  const logFd = fs.openSync(logFilePath, 'w');
58
- const header = `[${new Date().toISOString()}] ${NAME} v${VERSION} log started\n`;
59
+ // Rich header the environment facts needed to make sense of a report later:
60
+ // exact version, how it was invoked, runtime, platform, and where it ran.
61
+ const header =
62
+ `[${new Date().toISOString()}] ${NAME} v${VERSION} — log started\n` +
63
+ ` argv: ${process.argv.slice(1).join(' ')}\n` +
64
+ ` node: ${process.version}\n` +
65
+ ` platform: ${process.platform} ${process.arch}\n` +
66
+ ` cwd: ${process.cwd()}\n`;
59
67
  fs.writeSync(logFd, header);
60
68
 
69
+ // Route log-file-only diagnostics (lib/diag.js) to this fd. These breadcrumbs
70
+ // (custom-marketplace phases, swallowed error causes, etc.) land ONLY in the
71
+ // log, never on the user's console.
72
+ setLogSink((s) => { try { fs.writeSync(logFd, s); } catch { /* ignore */ } });
73
+
61
74
  const origStdoutWrite = process.stdout.write.bind(process.stdout);
62
75
  const origStderrWrite = process.stderr.write.bind(process.stderr);
63
76
 
@@ -81,6 +94,7 @@ function setupLogging(logFilePath) {
81
94
  process.stderr.write = origStderrWrite;
82
95
  const footer = `\n[${new Date().toISOString()}] log ended\n`;
83
96
  try { fs.writeSync(logFd, footer); } catch { /* ignore */ }
97
+ setLogSink(null);
84
98
  try { fs.closeSync(logFd); } catch { /* ignore */ }
85
99
  delete process.env.MA_AGENTS_LOG_ACTIVE;
86
100
  };
@@ -1025,13 +1039,16 @@ async function promptCustomMarketplace({ bmadCustomSourceFlag = '', yesFlag = fa
1025
1039
  }
1026
1040
 
1027
1041
  let enumerated;
1042
+ diag('custom-source: enumerate begin', { source });
1028
1043
  try {
1029
1044
  enumerated = await enumerateCustomSource(source);
1030
1045
  } catch (err) {
1031
1046
  // Fail clean (spike §3 contract) — a short, human-readable message only.
1032
1047
  // Never print err.stack here: an unreachable/unauthorized custom source
1033
1048
  // must not dump a raw upstream stack, and must never touch the base
1034
- // install (the rest of installWizard proceeds unaffected).
1049
+ // install (the rest of installWizard proceeds unaffected). The full cause
1050
+ // (and stack) goes to the LOG FILE only, for diagnosis.
1051
+ diagError('custom-source: enumerate failed', err);
1035
1052
  const message = err instanceof CustomMarketplaceError ? err.message : `Custom marketplace source error: ${err.message}`;
1036
1053
  console.log(chalk.red(`\n ${message}`));
1037
1054
  console.log(chalk.gray(' Skipping custom marketplace source; the rest of the install is unaffected.'));
@@ -1039,6 +1056,11 @@ async function promptCustomMarketplace({ bmadCustomSourceFlag = '', yesFlag = fa
1039
1056
  }
1040
1057
 
1041
1058
  const { plugins, mode, rootDir, sourceUrl } = enumerated;
1059
+ diag('custom-source: enumerated', {
1060
+ source, mode, rootDir, sourceUrl,
1061
+ pluginCount: (plugins || []).length,
1062
+ codes: (plugins || []).map((p) => p.code),
1063
+ });
1042
1064
  if (!plugins || plugins.length === 0) {
1043
1065
  console.log(chalk.yellow(` Custom marketplace source "${source}" declared no plugins — nothing to select.`));
1044
1066
  return { source, mode, selectedCodes: [], plugins: [], rootDir, sourceUrl };
@@ -1117,12 +1139,16 @@ async function promptCustomMarketplace({ bmadCustomSourceFlag = '', yesFlag = fa
1117
1139
  */
1118
1140
  async function reconcileCustomMarketplaceOnUpdate({ projectRoot }) {
1119
1141
  const persisted = getCustomMarketplaceSelection(projectRoot);
1142
+ diag('custom-source: reconcile on update', persisted
1143
+ ? { source: persisted.source, mode: persisted.mode, selectedCodes: persisted.selectedCodes }
1144
+ : { persisted: false });
1120
1145
  if (!persisted) return undefined;
1121
1146
 
1122
1147
  let enumerated;
1123
1148
  try {
1124
1149
  enumerated = await enumerateCustomSource(persisted.source);
1125
1150
  } catch (err) {
1151
+ diagError('custom-source: reconcile re-enumerate failed', err);
1126
1152
  const message = err instanceof CustomMarketplaceError ? err.message : `Custom marketplace source error: ${err.message}`;
1127
1153
  console.log(chalk.red(`\n Custom marketplace update FAILED: ${message}`));
1128
1154
  console.log(
@@ -1547,6 +1573,12 @@ async function installWizard(preselectedSkill, preselectedAgents, customPath, fo
1547
1573
  }
1548
1574
  }
1549
1575
  if (customMarketplaceSelection && customMarketplaceSelection.selectedCodes.length > 0) {
1576
+ diag('custom-source: install begin', {
1577
+ mode: customMarketplaceSelection.mode,
1578
+ source: customMarketplaceSelection.source,
1579
+ selectedCodes: customMarketplaceSelection.selectedCodes,
1580
+ baseBmadOk,
1581
+ });
1550
1582
  if (!baseBmadOk) {
1551
1583
  // AC 6 (30.6b) — never layer --custom-source onto a project with no
1552
1584
  // (or a broken) base BMAD install. The rest of the install is
@@ -1565,10 +1597,14 @@ async function installWizard(preselectedSkill, preselectedAgents, customPath, fo
1565
1597
  tools: bmadTools,
1566
1598
  selection: customMarketplaceSelection,
1567
1599
  });
1600
+ diag('custom-source: install result', { success: !!success });
1568
1601
  if (success) {
1569
1602
  console.log(chalk.green(' Custom marketplace subset installed successfully!'));
1570
1603
  }
1571
1604
  } catch (err) {
1605
+ // User sees the one-liner; the full cause/stack (which entry, the
1606
+ // git/resolve error, the FR238 zero-module detail) goes to the log.
1607
+ diagError('custom-source: install failed', err);
1572
1608
  console.log(chalk.red(`\n Custom marketplace install failed: ${err.message}`));
1573
1609
  }
1574
1610
  }
@@ -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.1",
40
+ "version": "3.17.0-beta.3",
41
41
  "author": {
42
42
  "name": "Alon Mayaffit"
43
43
  },
@@ -40,6 +40,7 @@
40
40
  const path = require('node:path');
41
41
  const fs = require('fs-extra');
42
42
  const yaml = require('js-yaml');
43
+ const { diag } = require('./diag');
43
44
 
44
45
  /** Deep, pinned-internal require path (NFR17). Single call site. */
45
46
  const CUSTOM_MODULE_MANAGER_PATH = 'bmad-method/tools/installer/modules/custom-module-manager.js';
@@ -477,6 +478,64 @@ async function scanDirectModeSkillDirs(rootDir) {
477
478
  return scanSkillDirs(rootDir);
478
479
  }
479
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
+
480
539
  /**
481
540
  * Resolve every selected plugin's installable skill/module layout via
482
541
  * bmad-method's own `CustomModuleManager#resolvePlugin` (the pinned-internal
@@ -554,6 +613,19 @@ async function resolveSelectedPlugins({ mode, rootDir, sourceUrl, plugins, selec
554
613
  function assertResolutionNonEmptyAndUniqueLeaves(resolvedByPlugin, { source = '', mode = null } = {}) {
555
614
  const allModules = resolvedByPlugin.flatMap((r) => r.resolved || []);
556
615
 
616
+ // Per-plugin resolution detail → log file only. When the guard below trips,
617
+ // this line pinpoints WHICH entry resolved to nothing (and how many
618
+ // skills each resolved module got) — the crux of a "zero modules" report.
619
+ diag('custom-source: resolution', {
620
+ source, mode,
621
+ perPlugin: resolvedByPlugin.map((r) => ({
622
+ plugin: (r.plugin && (r.plugin.code || r.plugin.name)) || (r.rawPlugin && r.rawPlugin.name) || '?',
623
+ modules: (r.resolved || []).length,
624
+ skills: (r.resolved || []).reduce((n, m) => n + ((m.skillPaths && m.skillPaths.length) || 0), 0),
625
+ })),
626
+ totalModules: allModules.length,
627
+ });
628
+
557
629
  if (allModules.length === 0) {
558
630
  // AC7 — a direct-mode source that resolves to nothing is the exact
559
631
  // failure a mis-pointed registry URL used to produce. Name the likely
@@ -741,6 +813,13 @@ async function resolveRegistryEntry(plugin, resolveSourceFn, resolvePluginFn) {
741
813
  // source string.
742
814
  const safePinRef = pinRef && /^[\w.\-+/]+$/.test(pinRef) && !pinRef.includes(':') ? pinRef : null;
743
815
  const cloneInput = safePinRef ? `${plugin.repository}@${safePinRef}` : plugin.repository;
816
+ diag('custom-source: registry entry clone', {
817
+ code: plugin.code,
818
+ repository: plugin.repository,
819
+ pinRef: safePinRef,
820
+ approvedSha: plugin.approvedSha || null,
821
+ moduleDefinition: plugin.moduleDefinition || '(none — repo root)',
822
+ });
744
823
 
745
824
  let resolvedSource;
746
825
  try {
@@ -763,10 +842,10 @@ async function resolveRegistryEntry(plugin, resolveSourceFn, resolvePluginFn) {
763
842
  // repo-relative path to the module's module.yaml; its directory is the
764
843
  // module root. Absent → fall back to the repo root (PluginResolver over the
765
844
  // whole clone), matching the AC3 fallback.
766
- let relModuleDirPosix = '.';
845
+ let moduleDefPosix = null;
767
846
  if (plugin.moduleDefinition) {
768
- const defPosix = toPosix(plugin.moduleDefinition).replace(/^\.\//, '');
769
- const moduleYamlAbs = path.join(entryRootDir, ...defPosix.split('/'));
847
+ moduleDefPosix = toPosix(plugin.moduleDefinition).replace(/^\.\//, '');
848
+ const moduleYamlAbs = path.join(entryRootDir, ...moduleDefPosix.split('/'));
770
849
  if (!(await fs.pathExists(moduleYamlAbs))) {
771
850
  throw new CustomMarketplaceError(
772
851
  `Registry entry '${plugin.code}': module_definition "${plugin.moduleDefinition}" ` +
@@ -774,21 +853,33 @@ async function resolveRegistryEntry(plugin, resolveSourceFn, resolvePluginFn) {
774
853
  `repository layout changed; aborting before staging (FR238).`
775
854
  );
776
855
  }
777
- relModuleDirPosix = path.posix.dirname(defPosix);
778
856
  }
779
857
 
780
- const moduleBaseAbs =
781
- relModuleDirPosix === '.' ? entryRootDir : path.join(entryRootDir, ...relModuleDirPosix.split('/'));
782
- const skillNames = await scanSkillDirs(moduleBaseAbs);
783
- const skillRelPaths = skillNames.map((n) => (relModuleDirPosix === '.' ? n : `${relModuleDirPosix}/${n}`));
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);
867
+ diag('custom-source: registry entry resolved', {
868
+ code: plugin.code,
869
+ moduleDef: moduleDefPosix,
870
+ skillsRoot: skillsRootPosix,
871
+ skillDirsFound: skillRelPaths.length,
872
+ skills: skillRelPaths,
873
+ });
784
874
 
785
875
  const rawPlugin = {
786
876
  name: plugin.code,
787
877
  description: plugin.description || '',
788
878
  version: plugin.version || null,
789
- source: relModuleDirPosix,
790
- // Empty when the module dir has no SKILL.md subdirs — resolvePlugin then
791
- // returns [] and the FR238 guard aborts cleanly (zero installable modules).
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).
792
883
  skills: skillRelPaths,
793
884
  };
794
885
 
@@ -857,6 +948,26 @@ async function stageRegistrySubset({ source, plugins, selectedCodes, projectRoot
857
948
  entries.push(await resolveRegistryEntry(plugin, resolveSourceFn, resolvePluginFn));
858
949
  }
859
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
+
860
971
  const allModules = assertResolutionNonEmptyAndUniqueLeaves(
861
972
  entries.map(({ plugin, rawPlugin, resolved }) => ({ plugin, rawPlugin, resolved })),
862
973
  { source, mode: 'registry' }
@@ -954,6 +1065,8 @@ module.exports = {
954
1065
  CUSTOM_MARKETPLACE_STAGE_DIR_NAME,
955
1066
  scanDirectModeSkillDirs,
956
1067
  scanSkillDirs,
1068
+ findSkillDirsRecursive,
1069
+ resolveRegistrySkillsRoot,
957
1070
  resolveSelectedPlugins,
958
1071
  assertResolutionNonEmptyAndUniqueLeaves,
959
1072
  stageCustomMarketplaceSubset,
package/lib/diag.js ADDED
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Log-file-only diagnostics.
5
+ *
6
+ * When `--log` is active, bin/cli.js#setupLogging installs a sink that writes
7
+ * to the log file descriptor. `diag()`/`diagError()` then emit structured
8
+ * breadcrumbs to the LOG FILE ONLY — never to the user's console (the wizard's
9
+ * user-facing output intentionally stays clean; e.g. a failed custom-marketplace
10
+ * source shows a one-line message, not a raw stack — NFR71). Without a sink
11
+ * (no `--log`), every call is a cheap no-op.
12
+ *
13
+ * This is how the swallowed root cause of a custom-marketplace failure (the
14
+ * git/clone/resolve error behind a clean CustomMarketplaceError, the per-entry
15
+ * module_definition that was missing, the FR238 zero-module trigger, etc.)
16
+ * reaches the log for post-hoc diagnosis without leaking to the terminal.
17
+ */
18
+
19
+ let sink = null;
20
+
21
+ /**
22
+ * @param {((text: string) => void)|null} fn - writer for log-file output, or null to disable.
23
+ */
24
+ function setLogSink(fn) {
25
+ sink = typeof fn === 'function' ? fn : null;
26
+ }
27
+
28
+ /** @returns {boolean} whether diagnostics are being captured. */
29
+ function isActive() {
30
+ return sink !== null;
31
+ }
32
+
33
+ function stamp() {
34
+ // new Date() is available in the CLI runtime (this is not a workflow script).
35
+ return new Date().toISOString();
36
+ }
37
+
38
+ function stringify(detail) {
39
+ if (typeof detail === 'string') return detail;
40
+ try {
41
+ return JSON.stringify(detail);
42
+ } catch {
43
+ return String(detail);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Emit one diagnostic breadcrumb to the log file (no-op without a sink).
49
+ * @param {string} label
50
+ * @param {*} [detail] - string or JSON-serializable value.
51
+ */
52
+ function diag(label, detail) {
53
+ if (!sink) return;
54
+ let line = `[diag ${stamp()}] ${label}`;
55
+ if (detail !== undefined) line += `: ${stringify(detail)}`;
56
+ try {
57
+ sink(line + '\n');
58
+ } catch {
59
+ /* never let diagnostics break the install */
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Emit an error — including its `stack` AND any wrapped `cause` (message +
65
+ * stack) — to the log file. This is the crucial bit for custom-marketplace
66
+ * failures, whose user-facing message deliberately hides the upstream cause.
67
+ * @param {string} label
68
+ * @param {Error|*} err
69
+ */
70
+ function diagError(label, err) {
71
+ if (!sink || !err) return;
72
+ const parts = [`[diag ${stamp()}] ${label}: ${err.message || String(err)}`];
73
+ if (err.stack) parts.push(` stack: ${err.stack}`);
74
+ if (err.cause) {
75
+ const c = err.cause;
76
+ parts.push(` cause: ${(c && c.message) || String(c)}`);
77
+ if (c && c.stack) parts.push(` cause.stack: ${c.stack}`);
78
+ }
79
+ try {
80
+ sink(parts.join('\n') + '\n');
81
+ } catch {
82
+ /* never let diagnostics break the install */
83
+ }
84
+ }
85
+
86
+ module.exports = { setLogSink, isActive, diag, diagError };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ma-agents",
3
- "version": "3.17.0-beta.1",
3
+ "version": "3.17.0-beta.3",
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": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/cli.js",
11
- "test": "node test/yes-flag.test.js && node test/skill-authoring.test.js && node test/skill-validation.test.js && node test/skill-mandatory.test.js && node test/skill-customize-agent.test.js && node test/create-agent.test.js && node test/generate-project-context.test.js && node test/build-bmad-args.test.js && node test/build-bmad-cache-reader.test.js && node test/bundle-filter-set-equality.test.js && node test/bmad-version-bump.test.js && node test/staging-6-10-0-regression.test.js && node test/uv-transition-resilience.test.js && node test/passthrough-reconciliation-6-9-0.test.js && node test/extension-module-restructure.test.js && node test/convert-agents-to-skills.test.js && node test/migration.test.js && node test/migration-validation.test.js && node test/story-15-5-workflow-skills.test.js && node test/repo-layout.test.js && node test/config-storage.test.js && node test/cross-repo-validation.test.js && node test/config-lost-on-update.test.js && node test/portable-paths.test.js && node test/cicd-remote-mode.test.js && node test/config-layout.test.js && node test/roo-code-agent.test.js && node test/roo-code-injection.test.js && node test/profile.test.js && node test/instruction-block.test.js && node test/instruction-injection.test.js && node test/agents-md.test.js && node test/instruction-block-git.test.js && node test/onprem-injection.test.js && node test/onprem-layer.test.js && node test/copilot-instructions-path.test.js && node test/roomodes.test.js && node test/clinerules.test.js && node test/cline-workflow-reconciliation.test.js && node test/reconfigure.test.js && node test/experimental-warning.test.js && node test/bmad-persona-phase-prefix.test.js && node test/f1a-on-prem-prefix-post-install.test.js && node test/retired-skills-migration.test.js && node test/rename-migration.test.js && node test/uninstall.test.js && node test/offline-recompile.test.js && node test/plugin-manifests.test.js && node test/demerzel-ml-removal-drift.test.js && node test/demerzel-deprecation-notice.test.js && node test/build-plugin.test.js && node test/config-path-migration.test.js && node test/gitignore-plugin-stage.test.js && node test/customizations-translation.test.js && node test/routing-parity.test.js && node test/bmad-extension.test.js && node test/integration-verification.test.js && node test/agent-scope.test.js && node test/obsolete-tool-dirs.test.js && node test/bmad-empty-tools-guard.test.js && node test/v68-pluginresolver-enumeration-passthrough.test.js && node test/agents.test.js && node test/installer.test.js && node test/bmad.test.js && node test/cli.test.js && node test/knowledge-atlas-smoke.test.js && node test/knowledge-atlas-manifest.test.js && node test/knowledge-atlas-render.test.js && node test/knowledge-atlas-graph.test.js && node test/knowledge-atlas-packaging.test.js && node test/knowledge-atlas-offline-safety.test.js && node test/knowledge-atlas-reconcile.test.js && node test/knowledge-atlas-determinism.test.js && node test/knowledge-atlas-degradation.test.js && node test/knowledge-atlas-realproject.test.js && node test/knowledge-atlas-link-integrity.test.js && node test/knowledge-atlas-config-scan.test.js && node test/knowledge-atlas-references.test.js && node test/agent-injection-strategy.test.js && node test/bmad-module-selection.test.js && node test/custom-marketplace-surface-drift.test.js && node test/custom-marketplace-wizard.test.js && node test/custom-marketplace-live-clone.test.js && node test/custom-marketplace-stage-install.test.js && node test/custom-marketplace-persist-reconcile.test.js && node test/demerzel-nondestructive-invariant.test.js && node test/fr243-custom-source-allowlist-nonleak.test.js && node test/custom-marketplace-registry.test.js",
11
+ "test": "node test/yes-flag.test.js && node test/skill-authoring.test.js && node test/skill-validation.test.js && node test/skill-mandatory.test.js && node test/skill-customize-agent.test.js && node test/create-agent.test.js && node test/generate-project-context.test.js && node test/build-bmad-args.test.js && node test/build-bmad-cache-reader.test.js && node test/bundle-filter-set-equality.test.js && node test/bmad-version-bump.test.js && node test/staging-6-10-0-regression.test.js && node test/uv-transition-resilience.test.js && node test/passthrough-reconciliation-6-9-0.test.js && node test/extension-module-restructure.test.js && node test/convert-agents-to-skills.test.js && node test/migration.test.js && node test/migration-validation.test.js && node test/story-15-5-workflow-skills.test.js && node test/repo-layout.test.js && node test/config-storage.test.js && node test/cross-repo-validation.test.js && node test/config-lost-on-update.test.js && node test/portable-paths.test.js && node test/cicd-remote-mode.test.js && node test/config-layout.test.js && node test/roo-code-agent.test.js && node test/roo-code-injection.test.js && node test/profile.test.js && node test/instruction-block.test.js && node test/instruction-injection.test.js && node test/agents-md.test.js && node test/instruction-block-git.test.js && node test/onprem-injection.test.js && node test/onprem-layer.test.js && node test/copilot-instructions-path.test.js && node test/roomodes.test.js && node test/clinerules.test.js && node test/cline-workflow-reconciliation.test.js && node test/reconfigure.test.js && node test/experimental-warning.test.js && node test/bmad-persona-phase-prefix.test.js && node test/f1a-on-prem-prefix-post-install.test.js && node test/retired-skills-migration.test.js && node test/rename-migration.test.js && node test/uninstall.test.js && node test/offline-recompile.test.js && node test/plugin-manifests.test.js && node test/demerzel-ml-removal-drift.test.js && node test/demerzel-deprecation-notice.test.js && node test/build-plugin.test.js && node test/config-path-migration.test.js && node test/gitignore-plugin-stage.test.js && node test/customizations-translation.test.js && node test/routing-parity.test.js && node test/bmad-extension.test.js && node test/integration-verification.test.js && node test/agent-scope.test.js && node test/obsolete-tool-dirs.test.js && node test/bmad-empty-tools-guard.test.js && node test/v68-pluginresolver-enumeration-passthrough.test.js && node test/agents.test.js && node test/installer.test.js && node test/bmad.test.js && node test/cli.test.js && node test/knowledge-atlas-smoke.test.js && node test/knowledge-atlas-manifest.test.js && node test/knowledge-atlas-render.test.js && node test/knowledge-atlas-graph.test.js && node test/knowledge-atlas-packaging.test.js && node test/knowledge-atlas-offline-safety.test.js && node test/knowledge-atlas-reconcile.test.js && node test/knowledge-atlas-determinism.test.js && node test/knowledge-atlas-degradation.test.js && node test/knowledge-atlas-realproject.test.js && node test/knowledge-atlas-link-integrity.test.js && node test/knowledge-atlas-config-scan.test.js && node test/knowledge-atlas-references.test.js && node test/agent-injection-strategy.test.js && node test/bmad-module-selection.test.js && node test/custom-marketplace-surface-drift.test.js && node test/custom-marketplace-wizard.test.js && node test/custom-marketplace-live-clone.test.js && node test/custom-marketplace-stage-install.test.js && node test/custom-marketplace-persist-reconcile.test.js && node test/demerzel-nondestructive-invariant.test.js && node test/fr243-custom-source-allowlist-nonleak.test.js && node test/custom-marketplace-registry.test.js && node test/diag.test.js",
12
12
  "build:bmad-cache": "node scripts/build-bmad-cache.js",
13
13
  "build:plugin": "node scripts/build-plugin.js",
14
14
  "build:atlas": "node scripts/build-atlas.js",