ma-agents 3.13.0 → 3.13.1

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
@@ -33,8 +33,8 @@ const chalk = require('chalk');
33
33
  const path = require('path');
34
34
  const fs = require('fs');
35
35
  const { execFileSync } = require('child_process');
36
- const { installSkill, uninstallSkill, getStatus, listSkills, listAgents, updateProjectContextRepoLayout, migrateRetiredSkills } = require('../lib/installer');
37
- const { getProfile, setProfile, resolveProfile } = require('../lib/profile');
36
+ const { installSkill, uninstallSkill, getStatus, listSkills, listAgents, updateProjectContextRepoLayout, migrateRetiredSkills, reconcileAgentsSharedSkills } = require('../lib/installer');
37
+ const { getProfile, setProfile, resolveProfile, getBmadModules, setBmadModules } = require('../lib/profile');
38
38
  const { reconfigure: runReconfigure, ReconfigureYesRejectedError, ManifestNotFoundError, RoomodesSlugDivergenceError } = require('../lib/reconfigure');
39
39
  const { uninstallProfileArtifacts } = require('../lib/uninstall');
40
40
  const bmad = require('../lib/bmad');
@@ -495,6 +495,19 @@ function writeConfigField(content, fieldName, value) {
495
495
  return content.trimEnd() + '\n' + newLine + '\n';
496
496
  }
497
497
 
498
+ // Bug 27.15 — unify the path convention. config.toml (written by upstream
499
+ // manifest-generator) declares artifact paths with a `{project-root}/` prefix;
500
+ // previously writeRepoLayoutConfig emitted a THIRD variant: bare relative
501
+ // `_bmad-output/...`. Emit the same `{project-root}/`-prefixed form here so
502
+ // ma-agents stops adding a divergent third convention. config.toml is the
503
+ // install-owned source of truth; the per-module config.yaml is the
504
+ // upstream-stripped read copy. Fully aligning the upstream stripper
505
+ // (official-modules.js) is a separate upstream follow-up; vendored node_modules
506
+ // is not edited here.
507
+ function canonicalArtifactPath(suffix) {
508
+ return `{project-root}/_bmad-output/${suffix}`;
509
+ }
510
+
498
511
  function writeRepoLayoutConfig(layout) {
499
512
  const configPath = path.join(process.cwd(), '_bmad', 'bmm', 'config.yaml');
500
513
  try {
@@ -522,8 +535,8 @@ function writeRepoLayoutConfig(layout) {
522
535
  content = writeConfigField(content, 'sprint_backend', 'jira');
523
536
  content = writeConfigField(content, 'jira_url', layout.sprintManagement.jiraUrl);
524
537
  content = writeConfigField(content, 'jira_project_key', layout.sprintManagement.jiraProjectKey);
525
- const planningArtifacts = kbPath === '.' ? '_bmad-output/planning-artifacts' : kbPath;
526
- const implArtifacts = '_bmad-output/implementation-artifacts';
538
+ const planningArtifacts = kbPath === '.' ? canonicalArtifactPath('planning-artifacts') : kbPath;
539
+ const implArtifacts = canonicalArtifactPath('implementation-artifacts');
527
540
  content = writeConfigField(content, 'planning_artifacts', planningArtifacts);
528
541
  content = writeConfigField(content, 'implementation_artifacts', implArtifacts);
529
542
  fs.writeFileSync(configPath, content, 'utf-8');
@@ -534,8 +547,8 @@ function writeRepoLayoutConfig(layout) {
534
547
  const spPath = spPortable.portable;
535
548
  content = writeConfigField(content, 'sprint_backend', 'file-system');
536
549
  content = writeConfigField(content, 'sprint_management_path', spPath);
537
- const planningArtifacts = kbPath === '.' ? '_bmad-output/planning-artifacts' : kbPath;
538
- const implArtifacts = spPath === '.' ? '_bmad-output/implementation-artifacts' : spPath;
550
+ const planningArtifacts = kbPath === '.' ? canonicalArtifactPath('planning-artifacts') : kbPath;
551
+ const implArtifacts = spPath === '.' ? canonicalArtifactPath('implementation-artifacts') : spPath;
539
552
  content = writeConfigField(content, 'planning_artifacts', planningArtifacts);
540
553
  content = writeConfigField(content, 'implementation_artifacts', implArtifacts);
541
554
  fs.writeFileSync(configPath, content, 'utf-8');
@@ -831,14 +844,26 @@ async function collectRepoLayout(flags, existingLayout = null) {
831
844
  * break without them. Retired modules (currently only `wds`) are filtered
832
845
  * out unconditionally.
833
846
  */
834
- async function selectBmadModules({ bmadModulesFlag, bmadModulesPrompt }) {
847
+ async function selectBmadModules({ bmadModulesFlag, bmadModulesPrompt, projectRoot } = {}) {
835
848
  const installable = bmad.getInstallableBmadModules().filter(m => !m.retired);
849
+ const root = projectRoot || process.cwd();
850
+
851
+ // Bug 27.13 — persist the resolved selection so a deselection sticks across
852
+ // future installs/reconfigures that do not re-pass --bmad-modules.
853
+ const persist = (modules) => {
854
+ try {
855
+ setBmadModules(root, modules);
856
+ } catch (err) {
857
+ console.warn(chalk.yellow(` Warning: could not persist BMAD module selection: ${err.message}`));
858
+ }
859
+ return modules;
860
+ };
836
861
 
837
862
  // Branch 1: explicit CSV — validate + use as-is.
838
863
  if (bmadModulesFlag) {
839
864
  const requested = bmadModulesFlag.split(',').map(s => s.trim()).filter(Boolean);
840
865
  try {
841
- return bmad.resolveBmadModules({ requested, available: installable });
866
+ return persist(bmad.resolveBmadModules({ requested, available: installable }));
842
867
  } catch (err) {
843
868
  console.error(chalk.red(`Error: ${err.message}`));
844
869
  process.exit(1);
@@ -872,11 +897,25 @@ async function selectBmadModules({ bmadModulesFlag, bmadModulesPrompt }) {
872
897
 
873
898
  // Belt-and-suspenders: force bmm back in if the user managed to uncheck
874
899
  // it despite the (required) tag.
875
- return bmad.resolveBmadModules({ requested: chosenModules, available: installable });
900
+ return persist(bmad.resolveBmadModules({ requested: chosenModules, available: installable }));
901
+ }
902
+
903
+ // Branch 3: no flag → use the persisted selection from a prior run if present
904
+ // (bug 27.13 — a previous deselection must survive); otherwise fall back to
905
+ // the original "install every available module" default. The persisted set is
906
+ // intersected with the currently-installable set so a since-retired module
907
+ // does not resurface, and bmm is force-included via resolveBmadModules.
908
+ const persisted = getBmadModules(root);
909
+ if (persisted && persisted.length > 0) {
910
+ const availableIds = new Set(installable.map(m => m.id));
911
+ const stillInstallable = persisted.filter(id => availableIds.has(id));
912
+ try {
913
+ return persist(bmad.resolveBmadModules({ requested: stillInstallable, available: installable }));
914
+ } catch {
915
+ // If the persisted set somehow fails validation, fall through to default.
916
+ }
876
917
  }
877
-
878
- // Branch 3: no flag → install everything available.
879
- return installable.map(m => m.id);
918
+ return persist(installable.map(m => m.id));
880
919
  }
881
920
 
882
921
  // --- Install wizard ---
@@ -1167,6 +1206,7 @@ async function installWizard(preselectedSkill, preselectedAgents, customPath, fo
1167
1206
  const bmadModules = await selectBmadModules({
1168
1207
  bmadModulesFlag,
1169
1208
  bmadModulesPrompt,
1209
+ projectRoot: process.cwd(),
1170
1210
  });
1171
1211
 
1172
1212
  if (!bmadInstalled) {
@@ -1314,6 +1354,20 @@ async function installWizard(preselectedSkill, preselectedAgents, customPath, fo
1314
1354
  }
1315
1355
  }
1316
1356
 
1357
+ // Bug 27.16 — if BMAD populated the shared `.agents/skills` dir but no
1358
+ // .agents-targeting agent (copilot/kilocode/roo-code) was selected, the
1359
+ // standalone catalog + MANIFEST.yaml were never written there. Backfill them
1360
+ // so the dir is never BMAD-only without the MANIFEST.yaml the instruction
1361
+ // block points agents at. No-op when an .agents agent was selected or no
1362
+ // shared dir exists.
1363
+ if (installScope === 'project') {
1364
+ try {
1365
+ await reconcileAgentsSharedSkills(process.cwd(), selectedAgentIds, installScope);
1366
+ } catch (err) {
1367
+ console.log(chalk.yellow(` .agents/skills reconciliation skipped: ${err.message}`));
1368
+ }
1369
+ }
1370
+
1317
1371
  // Step 6: Update project-context.md with repo layout section (after skills installed project-context.md)
1318
1372
  if (installScope === 'project') {
1319
1373
  const outputPath = path.join(process.cwd(), '_bmad-output', 'project-context.md');
@@ -1683,4 +1737,4 @@ if (require.main === module) {
1683
1737
  });
1684
1738
  }
1685
1739
 
1686
- module.exports = { parseFlags, collectRepoLayout, readExistingLayout, writeRepoLayoutConfig, writeProjectLayoutYaml, writeConfigField, normalizePath, toPortablePath, resolveStoredPath, ciCloneIfNeeded, showCurrentLayout, handleConfigLayout, yamlEscapeValue };
1740
+ module.exports = { parseFlags, selectBmadModules, collectRepoLayout, readExistingLayout, writeRepoLayoutConfig, writeProjectLayoutYaml, writeConfigField, normalizePath, toPortablePath, resolveStoredPath, ciCloneIfNeeded, showCurrentLayout, handleConfigLayout, yamlEscapeValue };
@@ -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.13.0",
40
+ "version": "3.13.1",
41
41
  "author": {
42
42
  "name": "Alon Mayaffit"
43
43
  },
package/lib/bmad.js CHANGED
@@ -278,17 +278,34 @@ function ensurePluginStageGitignoredForProject(projectRoot) {
278
278
  console.warn(chalk.yellow(` Warning: could not load installer for gitignore policy: ${err.message}`));
279
279
  return;
280
280
  }
281
- if (typeof installer.ensurePluginStageIgnored !== 'function') return;
282
- try {
283
- installer.ensurePluginStageIgnored(projectRoot);
284
- } catch (err) {
285
- // EACCES on read-only `.gitignore`, EROFS on read-only filesystem, etc.
286
- // Logged so the operator can fix it manually; never fatal to the install.
287
- console.warn(
288
- chalk.yellow(
289
- ` Warning: could not update ${path.join(projectRoot, '.gitignore')} with plugin-stage entry: ${err.message}`
290
- )
291
- );
281
+ if (typeof installer.ensurePluginStageIgnored === 'function') {
282
+ try {
283
+ installer.ensurePluginStageIgnored(projectRoot);
284
+ } catch (err) {
285
+ // EACCES on read-only `.gitignore`, EROFS on read-only filesystem, etc.
286
+ // Logged so the operator can fix it manually; never fatal to the install.
287
+ console.warn(
288
+ chalk.yellow(
289
+ ` Warning: could not update ${path.join(projectRoot, '.gitignore')} with plugin-stage entry: ${err.message}`
290
+ )
291
+ );
292
+ }
293
+ }
294
+
295
+ // Bug 27.11 — also ensure the regenerated, installer-managed `_bmad/` tree is
296
+ // gitignored. Same project-root contract and same non-fatal failure policy as
297
+ // the plugin-stage entry above. `_bmad-output/` is intentionally left tracked
298
+ // (see ensureBmadOutputTracked) and is not matched by these patterns.
299
+ if (typeof installer.ensureBmadDirIgnored === 'function') {
300
+ try {
301
+ installer.ensureBmadDirIgnored(projectRoot);
302
+ } catch (err) {
303
+ console.warn(
304
+ chalk.yellow(
305
+ ` Warning: could not update ${path.join(projectRoot, '.gitignore')} with _bmad/ entry: ${err.message}`
306
+ )
307
+ );
308
+ }
292
309
  }
293
310
  }
294
311
 
@@ -847,6 +864,11 @@ async function installBmad(modules = ['bmm', 'bmb'], tools = [], projectRoot = p
847
864
  try {
848
865
  runCommand(command, { cwd: projectRoot, env: buildChildSpawnEnv() });
849
866
  bmadInvokeSucceeded = true;
867
+ // Bug 27.14 — repair any module artifact path that upstream's
868
+ // suffix-match output-folder resolution contaminated (e.g. gds paths
869
+ // pinned to bmb's "skills" instead of the chosen output folder). Runs
870
+ // after bmad-method writes the config and before downstream consumers.
871
+ normalizeModuleOutputFolders(projectRoot);
850
872
  // F1a — on-prem persona phase-prefix pass. Runs BEFORE
851
873
  // deployMethodology so a methodology-deploy failure doesn't prevent
852
874
  // the prefix from being applied (they are independent concerns).
@@ -953,6 +975,10 @@ async function runMigration(modules, tools, projectRoot, force, { userName, comm
953
975
  return false;
954
976
  }
955
977
 
978
+ // Bug 27.14 — repair contaminated module artifact paths on the migration
979
+ // path too (same upstream suffix-match resolution applies to updates).
980
+ normalizeModuleOutputFolders(projectRoot);
981
+
956
982
  // Step 3: Merge user customizations
957
983
  if (backedUpFiles.length > 0) {
958
984
  console.log(chalk.cyan(' Step 3/4: Merging user customizations...'));
@@ -1808,6 +1834,237 @@ function readCanonicalBmadConfig(projectRoot) {
1808
1834
  }
1809
1835
  }
1810
1836
 
1837
+ // ── Bug 27.14 — output-folder propagation normalization ──────────────────────
1838
+ //
1839
+ // Upstream bmad-method's resolveConfigValue (official-modules.js) resolves the
1840
+ // `{output_folder}` token via a `endsWith('_output_folder')` suffix match over
1841
+ // all collected answers. When bmb (answer key `bmb_..._output_folder` = "skills")
1842
+ // and gds are co-installed, gds's `{output_folder}` resolves alphabetically to
1843
+ // bmb's "skills" value, so gds artifact paths become `{project-root}/skills/...`
1844
+ // instead of `{project-root}/_bmad-output/...`.
1845
+ //
1846
+ // ma-agents owns no part of that resolution (it lives in vendored node_modules),
1847
+ // so this is a post-install WORK-AROUND: after bmad-method writes the config,
1848
+ // re-read `[core] output_folder` and rewrite any contaminated module artifact
1849
+ // path back onto the canonical output folder. We deliberately scope the rewrite
1850
+ // to the well-known core artifact keys so we never touch a module's INTENTIONAL
1851
+ // own-folder values (e.g. bmb's `bmad_builder_output_folder = ".../skills"` or
1852
+ // wds's `design_artifacts = ".../design-artifacts"`).
1853
+ //
1854
+ // Upstream follow-up (do NOT edit node_modules here): patch resolveConfigValue
1855
+ // to require an exact `output_folder` / `core_output_folder` key instead of the
1856
+ // `endsWith` suffix match. Tracked as an upstream follow-up.
1857
+
1858
+ // Artifact keys that must always live under `[core] output_folder`. These are
1859
+ // the canonical bmm/gds planning+implementation+architecture artifact keys plus
1860
+ // tea's test_artifacts. Module-specific own-folder keys (bmb_*_output_folder,
1861
+ // design_artifacts, project_knowledge, *_output sub-paths) are intentionally
1862
+ // excluded.
1863
+ const OUTPUT_FOLDER_ARTIFACT_KEYS = [
1864
+ 'planning_artifacts',
1865
+ 'architecture_artifacts',
1866
+ 'implementation_artifacts',
1867
+ 'test_artifacts',
1868
+ ];
1869
+
1870
+ /**
1871
+ * Given an artifact path value and the canonical output folder, return a
1872
+ * corrected value if the path's output-folder segment differs from the
1873
+ * canonical one, or `null` if no change is needed.
1874
+ *
1875
+ * Handles both conventions:
1876
+ * - `{project-root}/<seg>/<suffix...>` (config.toml style)
1877
+ * - `<seg>/<suffix...>` (config.yaml stripped style)
1878
+ *
1879
+ * The leading `<seg>` (the first path segment after an optional `{project-root}/`
1880
+ * prefix) is the resolved output folder; if it !== `outputFolder` it is rewritten.
1881
+ * Only single-segment output folders are handled (the bmad default), which covers
1882
+ * the observed `skills` vs `_bmad-output` contamination.
1883
+ */
1884
+ function correctArtifactPath(value, outputFolder) {
1885
+ if (typeof value !== 'string' || !value) return null;
1886
+ const ROOT = '{project-root}/';
1887
+ let prefix = '';
1888
+ let rest = value;
1889
+ if (rest.startsWith(ROOT)) {
1890
+ prefix = ROOT;
1891
+ rest = rest.slice(ROOT.length);
1892
+ }
1893
+ const slash = rest.indexOf('/');
1894
+ if (slash === -1) return null; // no segment/suffix split — leave as-is
1895
+ const seg = rest.slice(0, slash);
1896
+ const suffix = rest.slice(slash); // includes leading '/'
1897
+ if (seg === outputFolder) return null; // already canonical
1898
+ return `${prefix}${outputFolder}${suffix}`;
1899
+ }
1900
+
1901
+ /**
1902
+ * Rewrite contaminated artifact paths inside `_bmad/config.toml`. Returns the
1903
+ * number of lines changed. Uses line-based editing (no TOML dependency) so we
1904
+ * preserve comments, ordering, and formatting exactly.
1905
+ */
1906
+ function normalizeTomlOutputFolders(tomlPath, outputFolder) {
1907
+ if (!fs.existsSync(tomlPath)) return 0;
1908
+ let raw;
1909
+ try {
1910
+ raw = fs.readFileSync(tomlPath, 'utf-8');
1911
+ } catch {
1912
+ return 0;
1913
+ }
1914
+ const usesCrlf = /\r\n/.test(raw);
1915
+ const eol = usesCrlf ? '\r\n' : '\n';
1916
+ const lines = raw.split(/\r?\n/);
1917
+ let changed = 0;
1918
+ let inModuleSection = false;
1919
+
1920
+ for (let i = 0; i < lines.length; i++) {
1921
+ const line = lines[i];
1922
+ const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
1923
+ if (sectionMatch) {
1924
+ const section = sectionMatch[1].trim();
1925
+ inModuleSection = section.startsWith('modules.');
1926
+ continue;
1927
+ }
1928
+ if (!inModuleSection) continue;
1929
+
1930
+ // key = "value" (only quoted string values are artifact paths)
1931
+ const kvMatch = line.match(/^(\s*)([A-Za-z0-9_]+)(\s*=\s*)"([^"]*)"(\s*(?:#.*)?)$/);
1932
+ if (!kvMatch) continue;
1933
+ const [, indent, key, eq, val, trailer] = kvMatch;
1934
+ if (!OUTPUT_FOLDER_ARTIFACT_KEYS.includes(key)) continue;
1935
+ const corrected = correctArtifactPath(val, outputFolder);
1936
+ if (corrected === null) continue;
1937
+ lines[i] = `${indent}${key}${eq}"${corrected}"${trailer}`;
1938
+ changed++;
1939
+ }
1940
+
1941
+ if (changed > 0) {
1942
+ fs.writeFileSync(tomlPath, lines.join(eol), 'utf-8');
1943
+ }
1944
+ return changed;
1945
+ }
1946
+
1947
+ /**
1948
+ * Rewrite contaminated artifact paths inside a per-module `_bmad/<mod>/config.yaml`.
1949
+ * Returns the number of keys changed. Uses js-yaml so we keep schema fidelity.
1950
+ */
1951
+ function normalizeYamlOutputFolders(yamlPath, outputFolder) {
1952
+ if (!fs.existsSync(yamlPath)) return 0;
1953
+ let raw;
1954
+ try {
1955
+ raw = fs.readFileSync(yamlPath, 'utf-8');
1956
+ } catch {
1957
+ return 0;
1958
+ }
1959
+ let doc;
1960
+ try {
1961
+ doc = yaml.parse(raw);
1962
+ } catch {
1963
+ return 0;
1964
+ }
1965
+ if (!doc || typeof doc !== 'object') return 0;
1966
+
1967
+ let changed = 0;
1968
+ for (const key of OUTPUT_FOLDER_ARTIFACT_KEYS) {
1969
+ if (typeof doc[key] !== 'string') continue;
1970
+ const corrected = correctArtifactPath(doc[key], outputFolder);
1971
+ if (corrected === null) continue;
1972
+ doc[key] = corrected;
1973
+ changed++;
1974
+ }
1975
+
1976
+ if (changed > 0) {
1977
+ // Preserve the leading comment header (everything up to the first
1978
+ // non-comment, non-blank line) so the "Generated by BMAD installer"
1979
+ // banner survives the rewrite.
1980
+ const headerLines = [];
1981
+ for (const l of raw.split(/\r?\n/)) {
1982
+ if (l.trim() === '' || l.trimStart().startsWith('#')) {
1983
+ headerLines.push(l);
1984
+ } else {
1985
+ break;
1986
+ }
1987
+ }
1988
+ const header = headerLines.length > 0 ? headerLines.join('\n') + '\n' : '';
1989
+ fs.writeFileSync(yamlPath, header + yaml.stringify(doc), 'utf-8');
1990
+ }
1991
+ return changed;
1992
+ }
1993
+
1994
+ /**
1995
+ * Bug 27.14 — post-install pass that re-points any contaminated module artifact
1996
+ * path at the canonical `[core] output_folder`. Reads the output folder from
1997
+ * `_bmad/config.toml` (the install-owned source of truth), then normalizes both
1998
+ * `_bmad/config.toml` and every `_bmad/<mod>/config.yaml`.
1999
+ *
2000
+ * Idempotent and non-fatal: a second run finds nothing to change, and any I/O
2001
+ * error is logged and swallowed (config normalization must never abort an
2002
+ * otherwise-green install).
2003
+ *
2004
+ * @param {string} projectRoot - Absolute path to the project root.
2005
+ * @returns {number} Total number of artifact paths rewritten (for tests/logging).
2006
+ */
2007
+ function normalizeModuleOutputFolders(projectRoot) {
2008
+ if (typeof projectRoot !== 'string' || !projectRoot) return 0;
2009
+ const tomlPath = path.join(projectRoot, BMAD_DIR, 'config.toml');
2010
+ if (!fs.existsSync(tomlPath)) return 0;
2011
+
2012
+ let tomlRaw;
2013
+ try {
2014
+ tomlRaw = fs.readFileSync(tomlPath, 'utf-8');
2015
+ } catch {
2016
+ return 0;
2017
+ }
2018
+
2019
+ // Extract `[core] output_folder = "..."`. Only the value inside the [core]
2020
+ // section counts; module sections may carry their own output_folder-like keys.
2021
+ let outputFolder = null;
2022
+ let inCore = false;
2023
+ for (const line of tomlRaw.split(/\r?\n/)) {
2024
+ const section = line.match(/^\s*\[([^\]]+)\]\s*$/);
2025
+ if (section) {
2026
+ inCore = section[1].trim() === 'core';
2027
+ continue;
2028
+ }
2029
+ if (!inCore) continue;
2030
+ const kv = line.match(/^\s*output_folder\s*=\s*"([^"]*)"/);
2031
+ if (kv) {
2032
+ outputFolder = kv[1].trim();
2033
+ break;
2034
+ }
2035
+ }
2036
+ if (!outputFolder) return 0; // nothing canonical to normalize against
2037
+
2038
+ let total = 0;
2039
+ try {
2040
+ total += normalizeTomlOutputFolders(tomlPath, outputFolder);
2041
+
2042
+ // Walk each `_bmad/<mod>/config.yaml`.
2043
+ const bmadDir = path.join(projectRoot, BMAD_DIR);
2044
+ let entries = [];
2045
+ try {
2046
+ entries = fs.readdirSync(bmadDir, { withFileTypes: true });
2047
+ } catch {
2048
+ entries = [];
2049
+ }
2050
+ for (const entry of entries) {
2051
+ if (!entry.isDirectory()) continue;
2052
+ const cfg = path.join(bmadDir, entry.name, 'config.yaml');
2053
+ total += normalizeYamlOutputFolders(cfg, outputFolder);
2054
+ }
2055
+ } catch (err) {
2056
+ console.warn(chalk.yellow(` Warning: output-folder normalization skipped: ${err.message}`));
2057
+ return total;
2058
+ }
2059
+
2060
+ if (total > 0) {
2061
+ console.log(chalk.green(
2062
+ ` Normalized ${total} module artifact path(s) to output folder "${outputFolder}/" (bug 27.14 work-around)`
2063
+ ));
2064
+ }
2065
+ return total;
2066
+ }
2067
+
1811
2068
  /**
1812
2069
  * Migration shim: normalize the project layout so that canonical config lives
1813
2070
  * at `_bmad/bmm/config.yaml` (v6.3.0) instead of `_bmad/_config/manifest.yaml`
@@ -2840,6 +3097,8 @@ module.exports = {
2840
3097
  isGeneratedClineWrapper,
2841
3098
  // Story 22.7 — canonical config (_bmad/bmm/config.yaml) helpers
2842
3099
  readCanonicalBmadConfig,
3100
+ normalizeModuleOutputFolders,
3101
+ correctArtifactPath,
2843
3102
  ensureCanonicalConfigLocation,
2844
3103
  CANONICAL_CONFIG_REL,
2845
3104
  LEGACY_MANIFEST_REL,
package/lib/installer.js CHANGED
@@ -166,7 +166,12 @@ function composeInstructionBlock({ profile, projectRoot } = {}) {
166
166
  composed += '\n\n' + onprem.replace(/\s+$/, '');
167
167
  }
168
168
 
169
- return composed + '\n';
169
+ // Bug 27.10 — the source templates are authored with CRLF line endings, but the
170
+ // segment seams and terminator above are hand-written LF. That mix yields a block
171
+ // whose body lines are CRLF while the joins are LF. Normalize to pure LF once here
172
+ // so the composed block is EOL-consistent regardless of how templates were checked
173
+ // out on Windows, and so downstream marker-merge (which assumes LF) stays stable.
174
+ return composed.replace(/\r\n/g, '\n') + '\n';
170
175
  }
171
176
 
172
177
  /**
@@ -245,6 +250,17 @@ function resolveBmadOutputDirs(projectRoot) {
245
250
  const CLAUDE_CODE_HOOK_ID = 'ma-agents-verify-manifest';
246
251
  const CLAUDE_CODE_SETTINGS_FILE = '.claude/settings.json';
247
252
 
253
+ // Bug 27.8 — the SessionStart hook must point at a path that exists in a real
254
+ // (npx/node_modules) install, not at the package's own `lib/` which is never
255
+ // copied into the target. The dependency-free hook script is copied into the
256
+ // target's `.claude/hooks/` and the command resolves it via $CLAUDE_PROJECT_DIR
257
+ // (the target project root). The legacy `lib/hooks/...` command string is kept
258
+ // here only so existing installs get it removed/migrated on the next run.
259
+ const CLAUDE_CODE_HOOK_SOURCE = path.join(__dirname, 'hooks', 'claude-code', 'verify-manifest.js');
260
+ const CLAUDE_CODE_HOOK_TARGET_REL = '.claude/hooks/verify-manifest.js';
261
+ const CLAUDE_CODE_HOOK_COMMAND = `node "$CLAUDE_PROJECT_DIR/${CLAUDE_CODE_HOOK_TARGET_REL}"`;
262
+ const CLAUDE_CODE_HOOK_LEGACY_COMMAND = `node "$CLAUDE_PROJECT_DIR/lib/hooks/claude-code/verify-manifest.js"`;
263
+
248
264
  function getPackageVersion() {
249
265
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
250
266
  return pkg.version;
@@ -397,13 +413,91 @@ function ensurePluginStageIgnored(projectRoot) {
397
413
  );
398
414
  }
399
415
 
416
+ // Bug 27.11 — the installer writes the `_bmad/` module tree which is
417
+ // "Installer-managed. Regenerated on every install — treat as read-only."
418
+ // Leaving it tracked guarantees churn and merge conflicts on every reinstall.
419
+ // This sister-policy to PLUGIN_STAGE_PATTERNS ensures `_bmad/` is gitignored.
420
+ //
421
+ // IMPORTANT: this targets ONLY the regenerated `_bmad/` tree. The
422
+ // `_bmad-output/` directory holds tracked project knowledge and is
423
+ // deliberately kept OUT of .gitignore (see ensureBmadOutputTracked) — the
424
+ // patterns below never match it because git treats `_bmad/` (trailing-slash
425
+ // anchored) and `_bmad-output` as distinct entries.
426
+ const BMAD_DIR_NAME = '_bmad';
427
+ const BMAD_DIR_PATTERNS = [
428
+ BMAD_DIR_NAME, // _bmad
429
+ `${BMAD_DIR_NAME}/`, // _bmad/
430
+ `/${BMAD_DIR_NAME}`, // /_bmad
431
+ `/${BMAD_DIR_NAME}/`, // /_bmad/
432
+ ];
433
+ const BMAD_DIR_CANONICAL_ENTRY = `${BMAD_DIR_NAME}/`;
434
+
435
+ /**
436
+ * Ensure `_bmad/` is present in the target project's `.gitignore`. Mirrors the
437
+ * append-only, idempotent, EOL-preserving contract of `ensurePluginStageIgnored`.
438
+ *
439
+ * - If `.gitignore` is absent, create it with the single canonical entry.
440
+ * - If any active (non-comment) line already matches any BMAD_DIR_PATTERNS
441
+ * variant, do nothing.
442
+ * - Otherwise, append `BMAD_DIR_CANONICAL_ENTRY` preserving existing EOL style.
443
+ *
444
+ * Defensive: no-op on falsy/non-string projectRoot, matching the sister helper.
445
+ *
446
+ * @param {string} projectRoot - Absolute path to the target project root.
447
+ */
448
+ function ensureBmadDirIgnored(projectRoot) {
449
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
450
+ return;
451
+ }
452
+
453
+ const gitignorePath = path.join(projectRoot, '.gitignore');
454
+
455
+ let content = '';
456
+ let fileExists = true;
457
+ try {
458
+ content = fs.readFileSync(gitignorePath, 'utf-8');
459
+ } catch (err) {
460
+ if (err.code !== 'ENOENT') throw err;
461
+ fileExists = false;
462
+ }
463
+
464
+ if (fileExists) {
465
+ const alreadyPresent = content.split(/\r?\n/).some(rawLine => {
466
+ const line = rawLine.trim();
467
+ if (!line || line.startsWith('#')) return false; // skip blanks & comments
468
+ return BMAD_DIR_PATTERNS.includes(line);
469
+ });
470
+ if (alreadyPresent) return; // idempotent no-op
471
+ }
472
+
473
+ const usesCrlf = fileExists && /\r\n/.test(content);
474
+ const eol = usesCrlf ? '\r\n' : '\n';
475
+
476
+ let next = content;
477
+ if (next && !next.endsWith('\n') && !next.endsWith('\r\n')) {
478
+ next += eol; // ensure the append lands on its own line
479
+ }
480
+ next += BMAD_DIR_CANONICAL_ENTRY + eol;
481
+
482
+ fs.writeFileSync(gitignorePath, next, 'utf-8');
483
+ console.log(
484
+ chalk.green(
485
+ `${BMAD_DIR_CANONICAL_ENTRY} added to .gitignore (installer-managed BMAD tree — regenerated on every install)`
486
+ )
487
+ );
488
+ }
489
+
400
490
  function ensureManifest(installPath, agentId, scope) {
401
491
  let manifest = readManifest(installPath);
402
492
  if (!manifest) {
493
+ // Bug 27.9 — when bootstrapped before any agent is known (e.g. profile
494
+ // bootstrap via setProfile → ensureManifest(root, null, 'project')), agentId
495
+ // is null. Mirror the migrate-branch guard so we never write the malformed
496
+ // `agent: null` / `agents: [null]` shape; an empty agents array is valid.
403
497
  manifest = {
404
498
  manifestVersion: MANIFEST_VERSION,
405
- agent: agentId,
406
- agents: [agentId],
499
+ agent: agentId || null,
500
+ agents: agentId ? [agentId] : [],
407
501
  scope: scope,
408
502
  skills: {}
409
503
  };
@@ -1265,6 +1359,116 @@ async function performInstall(skillId, skill, agent, installPath) {
1265
1359
  return true;
1266
1360
  }
1267
1361
 
1362
+ // --- Bug 27.16 — .agents/skills pipeline-consistency reconciliation ----------
1363
+ //
1364
+ // Two pipelines write skills: ma-agents' own standalone loop (installSkill →
1365
+ // performInstall + generateSkillsManifest — the ONLY writer of MANIFEST.yaml),
1366
+ // and bmad-method's PluginResolver (via --custom-source) which routes
1367
+ // copilot/roo/kilo to the shared `.agents/skills/` dir. The standalone loop only
1368
+ // targets `.agents/skills` when a copilot/kilocode/roo-code agent is selected.
1369
+ //
1370
+ // When the user selects only claude-code/cline/opencode, BMAD still writes the
1371
+ // BMAD-family skills into `.agents/skills` (175 of them in the Workshop repro)
1372
+ // but the standalone catalog and MANIFEST.yaml are never written there — leaving
1373
+ // a dir that looks installed but is silently partial AND missing the
1374
+ // MANIFEST.yaml the stamped instruction block points agents at.
1375
+ //
1376
+ // This pass makes `.agents/skills` pipeline-consistent: if BMAD populated it but
1377
+ // no `.agents`-targeting agent was selected (so the standalone loop skipped it),
1378
+ // install the full standalone catalog there and generate MANIFEST.yaml. The set
1379
+ // of `.agents`-targeting agent ids is derived from agents whose project skills
1380
+ // dir resolves to `.agents/skills`.
1381
+
1382
+ const AGENTS_SHARED_SKILLS_DIRNAME = '.agents';
1383
+
1384
+ /**
1385
+ * Return the list of agent ids whose project-level skills dir is the shared
1386
+ * `.agents/skills/` directory (copilot, kilocode, roo-code). Derived from the
1387
+ * agent registry's `skillsDir` so a future registry change surfaces here.
1388
+ */
1389
+ function getAgentsSharedSkillAgentIds() {
1390
+ return listAgents()
1391
+ .filter(a => a.skillsDir === path.join(AGENTS_SHARED_SKILLS_DIRNAME, 'skills') ||
1392
+ a.skillsDir === `${AGENTS_SHARED_SKILLS_DIRNAME}/skills`)
1393
+ .map(a => a.id);
1394
+ }
1395
+
1396
+ /**
1397
+ * Bug 27.16 — reconcile a BMAD-populated `.agents/skills` directory that the
1398
+ * standalone loop never targeted. No-op unless ALL of:
1399
+ * - scope === 'project'
1400
+ * - `<projectRoot>/.agents/skills` exists on disk (BMAD created it)
1401
+ * - no `.agents`-targeting agent was among the selected agents
1402
+ * - the dir has no MANIFEST.yaml (the standalone-loop signature)
1403
+ *
1404
+ * When triggered, installs the full standalone catalog into `.agents/skills`
1405
+ * (using a representative `.agents`-targeting agent for template resolution),
1406
+ * records the skills into that dir's `.ma-agents.json`, and generates
1407
+ * MANIFEST.yaml — so the dir is never BMAD-only without a manifest.
1408
+ *
1409
+ * Idempotent: once MANIFEST.yaml exists the early-return fires.
1410
+ *
1411
+ * @param {string} projectRoot
1412
+ * @param {string[]} selectedAgentIds - agent ids the user selected this run
1413
+ * @param {string} scope - 'project' | 'global'
1414
+ * @returns {Promise<{reconciled: boolean, installed: number}>}
1415
+ */
1416
+ async function reconcileAgentsSharedSkills(projectRoot, selectedAgentIds = [], scope = 'project') {
1417
+ const result = { reconciled: false, installed: 0 };
1418
+ if (scope !== 'project') return result;
1419
+ if (typeof projectRoot !== 'string' || !projectRoot) return result;
1420
+
1421
+ const sharedDir = path.join(projectRoot, AGENTS_SHARED_SKILLS_DIRNAME, 'skills');
1422
+ if (!fs.existsSync(sharedDir)) return result; // BMAD never populated it
1423
+
1424
+ const sharedAgentIds = getAgentsSharedSkillAgentIds();
1425
+ const selectedSet = new Set(selectedAgentIds || []);
1426
+ const anySharedSelected = sharedAgentIds.some(id => selectedSet.has(id));
1427
+ if (anySharedSelected) return result; // standalone loop already targeted it
1428
+
1429
+ // If a MANIFEST.yaml already exists, the dir is not standalone-orphaned.
1430
+ if (fs.existsSync(path.join(sharedDir, 'MANIFEST.yaml'))) return result;
1431
+
1432
+ // Pick a representative `.agents`-targeting agent for template resolution.
1433
+ const repAgent = sharedAgentIds.map(id => getAgent(id)).find(Boolean);
1434
+ if (!repAgent) return result; // registry has no such agent — nothing to do
1435
+
1436
+ console.log(chalk.yellow(
1437
+ ` Note: ${sharedDir} was populated by BMAD but carries no MANIFEST.yaml ` +
1438
+ `(no .agents-targeting agent selected) — backfilling the standalone catalog (bug 27.16).`
1439
+ ));
1440
+
1441
+ const skills = listSkills();
1442
+ const manifest = ensureManifest(sharedDir, repAgent.id, scope);
1443
+ const now = new Date().toISOString();
1444
+
1445
+ for (const skill of skills) {
1446
+ try {
1447
+ const ok = await performInstall(skill.id, skill, repAgent, sharedDir);
1448
+ if (!ok) continue;
1449
+ const existing = manifest.skills[skill.id];
1450
+ manifest.skills[skill.id] = {
1451
+ version: skill.version,
1452
+ installedAt: existing ? existing.installedAt : now,
1453
+ updatedAt: now,
1454
+ installerVersion: getPackageVersion(),
1455
+ agentVersion: repAgent.version
1456
+ };
1457
+ result.installed++;
1458
+ } catch (err) {
1459
+ console.log(chalk.yellow(` x ${skill.id} skipped in .agents/skills backfill: ${err.message}`));
1460
+ }
1461
+ }
1462
+
1463
+ writeManifest(sharedDir, manifest);
1464
+ await generateSkillsManifest(sharedDir);
1465
+ result.reconciled = true;
1466
+ console.log(chalk.green(
1467
+ ` Backfilled ${result.installed} standalone skill(s) + MANIFEST.yaml into ${sharedDir}`
1468
+ ));
1469
+ return result;
1470
+ }
1471
+
1268
1472
  // --- Install with upgrade detection ---
1269
1473
 
1270
1474
  async function installSkill(skillId, agentIds, customPath = '', scope = 'project', options = {}) {
@@ -1882,7 +2086,12 @@ function getStatus(agentIds, customPath = '', scope = 'project') {
1882
2086
 
1883
2087
  for (const { path: checkPath, scope: checkScope } of pathsToCheck) {
1884
2088
  const manifest = readManifest(checkPath);
1885
- if (!manifest || !manifest.skills || Object.keys(manifest.skills).length === 0) {
2089
+ // Bug 27.12 never treat the PROFILE-ONLY project-root manifest as a
2090
+ // skill inventory. The per-agent skills manifests are authoritative; a
2091
+ // manifest carrying the `project-root` scope discriminator is a
2092
+ // profile/settings store and must be skipped here.
2093
+ if (!manifest || manifest.scope === 'project-root' ||
2094
+ !manifest.skills || Object.keys(manifest.skills).length === 0) {
1886
2095
  continue;
1887
2096
  }
1888
2097
 
@@ -1924,27 +2133,49 @@ async function deployClaudeCodeHook(projectRoot) {
1924
2133
  settings.hooks.SessionStart = [];
1925
2134
  }
1926
2135
 
1927
- // Check if our hook is already present
1928
- const hookCommand = `node "$CLAUDE_PROJECT_DIR/lib/hooks/claude-code/verify-manifest.js"`;
2136
+ // Bug 27.8 copy the self-contained hook into the target so the command
2137
+ // resolves in any install (npx/node_modules/dev-repo). The legacy command
2138
+ // string (which pointed at the never-copied package `lib/`) is migrated to
2139
+ // the new command on every run.
2140
+ const hookTargetPath = path.join(projectRoot, CLAUDE_CODE_HOOK_TARGET_REL);
2141
+ await fs.ensureDir(path.dirname(hookTargetPath));
2142
+ await fs.copy(CLAUDE_CODE_HOOK_SOURCE, hookTargetPath, { overwrite: true });
2143
+
2144
+ const hookCommand = CLAUDE_CODE_HOOK_COMMAND;
2145
+ let migrated = false;
2146
+ for (const group of settings.hooks.SessionStart) {
2147
+ if (!group.hooks) continue;
2148
+ for (const h of group.hooks) {
2149
+ if (h.command === CLAUDE_CODE_HOOK_LEGACY_COMMAND || h._id === CLAUDE_CODE_HOOK_ID) {
2150
+ h.command = hookCommand;
2151
+ h._id = CLAUDE_CODE_HOOK_ID;
2152
+ migrated = true;
2153
+ }
2154
+ }
2155
+ }
2156
+
1929
2157
  const alreadyInstalled = settings.hooks.SessionStart.some(group =>
1930
2158
  group.hooks && group.hooks.some(h => h.command === hookCommand)
1931
2159
  );
1932
2160
 
1933
- if (alreadyInstalled) {
1934
- return; // Already deployed
2161
+ if (!alreadyInstalled) {
2162
+ settings.hooks.SessionStart.push({
2163
+ matcher: 'startup',
2164
+ hooks: [
2165
+ {
2166
+ type: 'command',
2167
+ command: hookCommand,
2168
+ _id: CLAUDE_CODE_HOOK_ID
2169
+ }
2170
+ ]
2171
+ });
2172
+ } else if (!migrated) {
2173
+ // Hook entry already current and file copied — still ensure settings exist.
2174
+ await fs.ensureDir(path.dirname(settingsPath));
2175
+ await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
2176
+ return;
1935
2177
  }
1936
2178
 
1937
- settings.hooks.SessionStart.push({
1938
- matcher: 'startup',
1939
- hooks: [
1940
- {
1941
- type: 'command',
1942
- command: hookCommand,
1943
- _id: CLAUDE_CODE_HOOK_ID
1944
- }
1945
- ]
1946
- });
1947
-
1948
2179
  await fs.ensureDir(path.dirname(settingsPath));
1949
2180
  await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
1950
2181
  console.log(chalk.cyan(' + Deployed Claude Code verify-manifest hook'));
@@ -1956,6 +2187,15 @@ async function deployClaudeCodeHook(projectRoot) {
1956
2187
  async function removeClaudeCodeHook(projectRoot) {
1957
2188
  const settingsPath = path.join(projectRoot, CLAUDE_CODE_SETTINGS_FILE);
1958
2189
 
2190
+ // Bug 27.8 — remove the copied hook file first, before any settings.json
2191
+ // early-return, so a missing/corrupt settings.json never orphans the file.
2192
+ const hookTargetPath = path.join(projectRoot, CLAUDE_CODE_HOOK_TARGET_REL);
2193
+ try {
2194
+ await fs.remove(hookTargetPath);
2195
+ } catch {
2196
+ // best-effort cleanup — never fail uninstall over a missing file
2197
+ }
2198
+
1959
2199
  if (!fs.existsSync(settingsPath)) return;
1960
2200
 
1961
2201
  let settings;
@@ -1967,10 +2207,15 @@ async function removeClaudeCodeHook(projectRoot) {
1967
2207
 
1968
2208
  if (!settings.hooks || !settings.hooks.SessionStart) return;
1969
2209
 
1970
- const hookCommand = `node "$CLAUDE_PROJECT_DIR/lib/hooks/claude-code/verify-manifest.js"`;
2210
+ // Match both the current command and the legacy package-relative command so
2211
+ // older installs get cleaned up too.
1971
2212
  settings.hooks.SessionStart = settings.hooks.SessionStart.filter(group => {
1972
2213
  if (!group.hooks) return true;
1973
- group.hooks = group.hooks.filter(h => h.command !== hookCommand && h._id !== CLAUDE_CODE_HOOK_ID);
2214
+ group.hooks = group.hooks.filter(h =>
2215
+ h.command !== CLAUDE_CODE_HOOK_COMMAND &&
2216
+ h.command !== CLAUDE_CODE_HOOK_LEGACY_COMMAND &&
2217
+ h._id !== CLAUDE_CODE_HOOK_ID
2218
+ );
1974
2219
  return group.hooks.length > 0;
1975
2220
  });
1976
2221
 
@@ -2009,6 +2254,9 @@ module.exports = {
2009
2254
  removeClaudeCodeHook,
2010
2255
  ensureBmadOutputTracked,
2011
2256
  ensurePluginStageIgnored,
2257
+ ensureBmadDirIgnored,
2258
+ reconcileAgentsSharedSkills,
2259
+ getAgentsSharedSkillAgentIds,
2012
2260
  generateSkillsManifest,
2013
2261
  generateProjectContext,
2014
2262
  generateRepoLayoutSection,
package/lib/profile.js CHANGED
@@ -23,6 +23,29 @@ const path = require('path');
23
23
  const MANIFEST_FILE = '.ma-agents.json';
24
24
  const VALID_PROFILES = ['on-prem', 'standard'];
25
25
 
26
+ // Bug 27.12 — the project-root .ma-agents.json is a PROFILE/settings store, not
27
+ // a skill inventory. Per-agent skills dirs (e.g. .claude/skills/.ma-agents.json)
28
+ // own the installed-skill inventory and remain the authoritative source read by
29
+ // getStatus/uninstall/reconcile. The root bootstrap therefore emits ONLY the
30
+ // fields it owns (manifestVersion + a scope discriminator) and NOT a `skills`/
31
+ // `agents` shape it would never back-fill — so the root file can no longer
32
+ // masquerade as an empty skill inventory (`skills: {}`).
33
+ const ROOT_MANIFEST_VERSION = '1.2.0';
34
+ const ROOT_SCOPE = 'project-root';
35
+
36
+ /**
37
+ * Bootstrap a fresh PROFILE-ONLY root manifest object (not persisted here).
38
+ * Intentionally omits `skills` and `agents`: those belong to per-agent skills
39
+ * manifests. The `scope: 'project-root'` discriminator makes the file's role
40
+ * explicit for any reader.
41
+ */
42
+ function bootstrapRootManifest() {
43
+ return {
44
+ manifestVersion: ROOT_MANIFEST_VERSION,
45
+ scope: ROOT_SCOPE,
46
+ };
47
+ }
48
+
26
49
  /**
27
50
  * Reads the persisted profile from .ma-agents.json at the given project root.
28
51
  * Returns undefined if the file does not exist, is malformed, or the "profile"
@@ -73,10 +96,12 @@ function setProfile(projectRoot, value) {
73
96
  }
74
97
 
75
98
  if (!manifest || typeof manifest !== 'object') {
76
- // Bootstrap via the standard schema (mirrors lib/installer.js ensureManifest shape).
77
- // Require lazily to avoid potential circular-import pitfalls at module load.
78
- const { ensureManifest } = require('./installer');
79
- manifest = ensureManifest(projectRoot, null, 'project');
99
+ // Bug 27.12 bootstrap a PROFILE-ONLY root manifest. Previously this used
100
+ // ensureManifest(root, null, 'project'), which stamped an empty `skills: {}`
101
+ // / `agents: []` shape the root file never owns or back-fills, making it
102
+ // look like an (empty) skill inventory. The per-agent skills manifests are
103
+ // the authoritative inventory; the root file stores profile/settings only.
104
+ manifest = bootstrapRootManifest();
80
105
  }
81
106
 
82
107
  // Migrate 1.1.0 (or missing version) → 1.2.0, since the "profile" field is 1.2.0-only.
@@ -127,4 +152,70 @@ function clearProfile(projectRoot) {
127
152
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
128
153
  }
129
154
 
130
- module.exports = { getProfile, setProfile, resolveProfile, clearProfile };
155
+ /**
156
+ * Bug 27.13 — durable BMAD-module selection.
157
+ *
158
+ * Reads the persisted BMAD module set from .ma-agents.json (the `bmadModules`
159
+ * field). Returns undefined when the file is absent/malformed or the field is
160
+ * missing, so callers can distinguish "never persisted" (→ install default set)
161
+ * from an explicit empty selection. A persisted value is always returned as an
162
+ * array of strings.
163
+ */
164
+ function getBmadModules(projectRoot) {
165
+ const manifestPath = path.join(projectRoot, MANIFEST_FILE);
166
+ if (!fs.existsSync(manifestPath)) return undefined;
167
+ let manifest;
168
+ try {
169
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
170
+ } catch {
171
+ return undefined;
172
+ }
173
+ if (!manifest || typeof manifest !== 'object') return undefined;
174
+ if (!Object.prototype.hasOwnProperty.call(manifest, 'bmadModules')) return undefined;
175
+ const value = manifest.bmadModules;
176
+ if (!Array.isArray(value)) return undefined;
177
+ // Normalize to a clean, de-duplicated array of non-empty strings.
178
+ return [...new Set(value.filter(m => typeof m === 'string' && m.length > 0))];
179
+ }
180
+
181
+ /**
182
+ * Bug 27.13 — persists the selected BMAD module set into .ma-agents.json,
183
+ * preserving all other fields. Creates the file via the standard ensureManifest
184
+ * bootstrap path if absent (mirroring setProfile). The choice then survives
185
+ * subsequent installs/reconfigures that do not re-pass --bmad-modules.
186
+ *
187
+ * @param {string} projectRoot
188
+ * @param {string[]} modules - module ids to persist (e.g. ['bmm','tea','bmb'])
189
+ */
190
+ function setBmadModules(projectRoot, modules) {
191
+ if (!Array.isArray(modules)) {
192
+ throw new Error(`setBmadModules expects an array of module ids (got ${JSON.stringify(modules)})`);
193
+ }
194
+ const cleaned = [...new Set(modules.filter(m => typeof m === 'string' && m.length > 0))];
195
+
196
+ const manifestPath = path.join(projectRoot, MANIFEST_FILE);
197
+ let manifest;
198
+ if (fs.existsSync(manifestPath)) {
199
+ try {
200
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
201
+ } catch {
202
+ manifest = null;
203
+ }
204
+ }
205
+ if (!manifest || typeof manifest !== 'object') {
206
+ // Bug 27.12 — profile-only root bootstrap (see setProfile / bootstrapRootManifest).
207
+ manifest = bootstrapRootManifest();
208
+ }
209
+
210
+ manifest.bmadModules = cleaned;
211
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
212
+ }
213
+
214
+ module.exports = {
215
+ getProfile,
216
+ setProfile,
217
+ resolveProfile,
218
+ clearProfile,
219
+ getBmadModules,
220
+ setBmadModules,
221
+ };
@@ -1,25 +1,25 @@
1
- ## Git Workflow (ALL file-changing tasks)
2
-
3
- These rules apply to EVERY task that modifies a tracked file. There is no
4
- "small fix exemption" — a one-line edit follows the same workflow as a feature.
5
-
6
- - **Worktree first.** Before changing any file, create a fresh isolated worktree
7
- on a new feature branch: `git worktree add ../<project>-<branch> -b <branch>`.
8
- The scripted form is `skills/git-workflow-skill/scripts/start-feature.sh`. All
9
- file-writing happens inside that worktree — never in the main working tree.
10
- - **Never commit directly to the trunk.** Do NOT commit to `main`, `dev`,
11
- `master`, or whatever the trunk branch is. Commits land only on your feature
12
- branch inside the worktree.
13
- - **Conventional Commits.** Every commit message uses a Conventional Commits
14
- prefix. The allowed prefixes are: `feat`, `fix`, `chore`, `docs`, `refactor`,
15
- `test`, `ci`.
16
- - **PR is the only path into the trunk.** Integrate exclusively by opening a pull
17
- request with `gh pr create`. Never merge directly and never auto-merge a PR
18
- without explicit human approval.
19
- - **Self-review before opening the PR.** Run the `code-review` skill on your own
20
- changes and address its findings before you open the PR.
21
- - **Clean up after merge.** Once the PR is merged, remove the worktree with
22
- `git worktree remove` and delete the feature branch.
23
-
24
- Full procedure: see `skills/git-workflow-skill/SKILL.md`, and the helper scripts
25
- `scripts/start-feature.sh` / `scripts/finish-feature.sh`.
1
+ ## Git Workflow (ALL file-changing tasks)
2
+
3
+ These rules apply to EVERY task that modifies a tracked file. There is no
4
+ "small fix exemption" — a one-line edit follows the same workflow as a feature.
5
+
6
+ - **Worktree first.** Before changing any file, create a fresh isolated worktree
7
+ on a new feature branch: `git worktree add ../<project>-<branch> -b <branch>`.
8
+ The scripted form is `skills/git-workflow-skill/scripts/start-feature.sh`. All
9
+ file-writing happens inside that worktree — never in the main working tree.
10
+ - **Never commit directly to the trunk.** Do NOT commit to `main`, `dev`,
11
+ `master`, or whatever the trunk branch is. Commits land only on your feature
12
+ branch inside the worktree.
13
+ - **Conventional Commits.** Every commit message uses a Conventional Commits
14
+ prefix. The allowed prefixes are: `feat`, `fix`, `chore`, `docs`, `refactor`,
15
+ `test`, `ci`.
16
+ - **PR is the only path into the trunk.** Integrate exclusively by opening a pull
17
+ request with `gh pr create`. Never merge directly and never auto-merge a PR
18
+ without explicit human approval.
19
+ - **Self-review before opening the PR.** Run the `code-review` skill on your own
20
+ changes and address its findings before you open the PR.
21
+ - **Clean up after merge.** Once the PR is merged, remove the worktree with
22
+ `git worktree remove` and delete the feature branch.
23
+
24
+ Full procedure: see `skills/git-workflow-skill/SKILL.md`, and the helper scripts
25
+ `scripts/start-feature.sh` / `scripts/finish-feature.sh`.
@@ -1,86 +1,86 @@
1
- ## On-Prem / Local-LLM Guardrails
2
-
3
- These rules apply ONLY when this project is installed with `profile: on-prem`.
4
- They are appended to the universal block by the composer in `lib/installer.js`.
5
- Local LLMs (Nemotron, Qwen, DeepSeek, Llama-3, etc. served via vLLM, Ollama, or
6
- TGI) fail in patterns cloud LLMs rarely exhibit — the rules below pin those
7
- failure modes down explicitly. Keep these rules verbatim in every response
8
- context where tool use is possible.
9
-
10
- ### Reasoning mode: `/no_think` on planning-phase prompts
11
-
12
- Local reasoning-capable models (Nemotron-variants and similar) default to
13
- chain-of-thought reasoning that bloats planning-phase prompts with internal
14
- deliberation the operator does not need to read. Prepend the literal token
15
- `/no_think` as the first line of any planning-phase system prompt or user turn
16
- you compose. The token is consumed by the serving layer and suppresses the
17
- model's reasoning trace on that turn.
18
-
19
- - Planning-phase turns (PM, Architect, Tech Lead): begin the turn with
20
- `/no_think` on its own line. Reasoning-mode OFF.
21
- - Implementation-phase turns (Dev): omit `/no_think`. Reasoning-mode ON is
22
- desirable for stepwise code synthesis and debugging.
23
- - Review / QA turns: omit `/no_think` when the review benefits from explicit
24
- reasoning (root-cause analysis). Include `/no_think` for mechanical checks
25
- (lint, style, formatting).
26
-
27
- If the downstream serving layer does not recognize `/no_think`, it is a no-op
28
- text token — safe to include unconditionally on planning turns.
29
-
30
- ### No writes to `~/.claude/` or any user home directory
31
-
32
- Local LLMs frequently hallucinate paths under `~/.claude/`, `~/.cache/`,
33
- `~/Library/`, or `%APPDATA%` — imitating patterns learned from Claude Code and
34
- Cursor training data. These paths are OUTSIDE the project and cross-contaminate
35
- other projects on the same machine.
36
-
37
- - NEVER create, write, or modify files under `~/.claude/`, `~/.cache/`,
38
- `~/Library/`, `~/AppData/`, `%APPDATA%`, or any path that resolves outside
39
- the current project directory.
40
- - All project artifacts — code, configuration, logs, scratch notes, and agent
41
- state — MUST land under the current working directory (the project root) or
42
- an explicitly-named subdirectory thereof.
43
- - When a tool call appears to target a home-directory path, refuse the write
44
- and respond in text explaining the violation. Ask the user for an explicit
45
- in-project path before proceeding.
46
-
47
- ### No `str_replace_editor` or Claude Code-specific tools
48
-
49
- Local LLMs hallucinate Anthropic-proprietary tools — most commonly
50
- `str_replace_editor`, `text_editor_20241022`, and `computer_use_preview` — that
51
- do NOT exist outside the Anthropic API. Calling them against a local-LLM
52
- serving layer produces a tool-not-found error or, worse, a silent no-op.
53
-
54
- - Do NOT emit tool calls named `str_replace_editor`, `text_editor_*`,
55
- `computer_use_*`, or any other tool whose name includes `str_replace_editor`
56
- or matches Anthropic-specific tool schemas.
57
- - Use only tools enumerated in the active tool manifest (`MANIFEST.yaml`) or
58
- the IDE's native tool surface (Roo Code, Cline, OpenCode native tools).
59
- - When you want to edit a file, use the native file-write tool of the active
60
- agent — not `str_replace_editor`. If unsure what tool is available, list
61
- available tools or ask the user before emitting a tool call.
62
-
63
- ### Per-phase reasoning and sampling guidance
64
-
65
- Local LLMs require tighter sampling control than cloud LLMs. Use these defaults
66
- unless the serving layer overrides them.
67
-
68
- - **Planning phase (PM, Architect, Tech Lead):**
69
- - Reasoning: OFF (`/no_think` prepended).
70
- - Temperature: low (0.0 – 0.3). Planning artifacts should be deterministic
71
- and reproducible.
72
- - Top-p: 0.9 or unset. Top-k: unset.
73
- - Max tokens: generous (8k+) — planning documents are long.
74
- - **Implementation phase (Dev):**
75
- - Reasoning: ON (omit `/no_think`).
76
- - Temperature: moderate (0.3 – 0.6). Code synthesis benefits from controlled
77
- exploration but not creative rewriting.
78
- - Top-p: 0.95 or unset. Top-k: unset.
79
- - Max tokens: generous (8k+) — full-file rewrites are common.
80
- - **Review / QA phase:**
81
- - Reasoning: ON for root-cause analysis; OFF for mechanical checks.
82
- - Temperature: low (0.0 – 0.2). Reviews should be deterministic.
83
-
84
- If the serving layer applies its own sampler defaults, the per-phase guidance
85
- above is advisory — but the phase boundary and `/no_think` placement are
86
- load-bearing and MUST be honored on every turn.
1
+ ## On-Prem / Local-LLM Guardrails
2
+
3
+ These rules apply ONLY when this project is installed with `profile: on-prem`.
4
+ They are appended to the universal block by the composer in `lib/installer.js`.
5
+ Local LLMs (Nemotron, Qwen, DeepSeek, Llama-3, etc. served via vLLM, Ollama, or
6
+ TGI) fail in patterns cloud LLMs rarely exhibit — the rules below pin those
7
+ failure modes down explicitly. Keep these rules verbatim in every response
8
+ context where tool use is possible.
9
+
10
+ ### Reasoning mode: `/no_think` on planning-phase prompts
11
+
12
+ Local reasoning-capable models (Nemotron-variants and similar) default to
13
+ chain-of-thought reasoning that bloats planning-phase prompts with internal
14
+ deliberation the operator does not need to read. Prepend the literal token
15
+ `/no_think` as the first line of any planning-phase system prompt or user turn
16
+ you compose. The token is consumed by the serving layer and suppresses the
17
+ model's reasoning trace on that turn.
18
+
19
+ - Planning-phase turns (PM, Architect, Tech Lead): begin the turn with
20
+ `/no_think` on its own line. Reasoning-mode OFF.
21
+ - Implementation-phase turns (Dev): omit `/no_think`. Reasoning-mode ON is
22
+ desirable for stepwise code synthesis and debugging.
23
+ - Review / QA turns: omit `/no_think` when the review benefits from explicit
24
+ reasoning (root-cause analysis). Include `/no_think` for mechanical checks
25
+ (lint, style, formatting).
26
+
27
+ If the downstream serving layer does not recognize `/no_think`, it is a no-op
28
+ text token — safe to include unconditionally on planning turns.
29
+
30
+ ### No writes to `~/.claude/` or any user home directory
31
+
32
+ Local LLMs frequently hallucinate paths under `~/.claude/`, `~/.cache/`,
33
+ `~/Library/`, or `%APPDATA%` — imitating patterns learned from Claude Code and
34
+ Cursor training data. These paths are OUTSIDE the project and cross-contaminate
35
+ other projects on the same machine.
36
+
37
+ - NEVER create, write, or modify files under `~/.claude/`, `~/.cache/`,
38
+ `~/Library/`, `~/AppData/`, `%APPDATA%`, or any path that resolves outside
39
+ the current project directory.
40
+ - All project artifacts — code, configuration, logs, scratch notes, and agent
41
+ state — MUST land under the current working directory (the project root) or
42
+ an explicitly-named subdirectory thereof.
43
+ - When a tool call appears to target a home-directory path, refuse the write
44
+ and respond in text explaining the violation. Ask the user for an explicit
45
+ in-project path before proceeding.
46
+
47
+ ### No `str_replace_editor` or Claude Code-specific tools
48
+
49
+ Local LLMs hallucinate Anthropic-proprietary tools — most commonly
50
+ `str_replace_editor`, `text_editor_20241022`, and `computer_use_preview` — that
51
+ do NOT exist outside the Anthropic API. Calling them against a local-LLM
52
+ serving layer produces a tool-not-found error or, worse, a silent no-op.
53
+
54
+ - Do NOT emit tool calls named `str_replace_editor`, `text_editor_*`,
55
+ `computer_use_*`, or any other tool whose name includes `str_replace_editor`
56
+ or matches Anthropic-specific tool schemas.
57
+ - Use only tools enumerated in the active tool manifest (`MANIFEST.yaml`) or
58
+ the IDE's native tool surface (Roo Code, Cline, OpenCode native tools).
59
+ - When you want to edit a file, use the native file-write tool of the active
60
+ agent — not `str_replace_editor`. If unsure what tool is available, list
61
+ available tools or ask the user before emitting a tool call.
62
+
63
+ ### Per-phase reasoning and sampling guidance
64
+
65
+ Local LLMs require tighter sampling control than cloud LLMs. Use these defaults
66
+ unless the serving layer overrides them.
67
+
68
+ - **Planning phase (PM, Architect, Tech Lead):**
69
+ - Reasoning: OFF (`/no_think` prepended).
70
+ - Temperature: low (0.0 – 0.3). Planning artifacts should be deterministic
71
+ and reproducible.
72
+ - Top-p: 0.9 or unset. Top-k: unset.
73
+ - Max tokens: generous (8k+) — planning documents are long.
74
+ - **Implementation phase (Dev):**
75
+ - Reasoning: ON (omit `/no_think`).
76
+ - Temperature: moderate (0.3 – 0.6). Code synthesis benefits from controlled
77
+ exploration but not creative rewriting.
78
+ - Top-p: 0.95 or unset. Top-k: unset.
79
+ - Max tokens: generous (8k+) — full-file rewrites are common.
80
+ - **Review / QA phase:**
81
+ - Reasoning: ON for root-cause analysis; OFF for mechanical checks.
82
+ - Temperature: low (0.0 – 0.2). Reviews should be deterministic.
83
+
84
+ If the serving layer applies its own sampler defaults, the per-phase guidance
85
+ above is advisory — but the phase boundary and `/no_think` placement are
86
+ load-bearing and MUST be honored on every turn.
@@ -1,29 +1,29 @@
1
- # AI Agent Skills - Planning Instruction
2
-
3
- You have access to a library of skills in your skills directory. Before starting any task:
4
-
5
- 1. Read the skill manifest at {{MANIFEST_PATH}}
6
- 2. Based on the task description, select which skills are relevant
7
- 3. Read only the selected skill files
8
- 4. Then proceed with the task
9
-
10
- Always load skills marked with always_load: true.
11
- Do not load skills that are not relevant to the current task.
12
-
13
- ## Respond in TEXT vs. create FILES
14
-
15
- Choose your response medium deliberately. Defaulting to file creation when the user asked a question is a common failure mode — especially for coding agents running in web UIs.
16
-
17
- - **Create or modify FILES when the user's request contains file-action keywords:** `create`, `write`, `generate`, `build`, `implement` (and obvious synonyms such as `add`, `produce`, `refactor`, `fix`, `update <file>`). These signal a concrete artifact is expected.
18
- - **Respond in TEXT when the request contains text-response keywords:** `what do you think`, `how should we`, `discuss`, `opinion` (and obvious synonyms such as `explain`, `why`, `should I`, `compare`, `recommend`). These signal that a conversation is expected, not a deliverable.
19
- - **If unsure, respond in TEXT.** A text answer can always be followed by file creation on confirmation; an unwanted file cannot be cleanly undone.
20
- - **Never create `response.md`, `output.md`, or any similarly named scratch file as a reply.** A reply belongs in the chat transcript, not on disk.
21
- - **Confirm file paths before writing.** When you are about to create or modify a file whose path the user has not explicitly named, state the intended path in text and wait for confirmation, unless the path is unambiguous from the task context.
22
-
23
- ## BMAD phase discipline
24
-
25
- BMAD-METHOD organizes work into declared phases (analysis, planning, architecture, story-creation, implementation, review). Respect the currently declared phase.
26
-
27
- - **Do not skip ahead to implementation during planning.** If the project is in a planning phase — or the user has asked for requirements, architecture, or a story — produce planning artifacts, not code.
28
- - **Do not retroactively plan after you have already coded.** If implementation has already started, flag the gap instead of fabricating back-dated planning documents.
29
- - The declared phase is established by the active skill, the story status, or an explicit statement from the user. When none of these is available, ask before assuming.
1
+ # AI Agent Skills - Planning Instruction
2
+
3
+ You have access to a library of skills in your skills directory. Before starting any task:
4
+
5
+ 1. Read the skill manifest at {{MANIFEST_PATH}}
6
+ 2. Based on the task description, select which skills are relevant
7
+ 3. Read only the selected skill files
8
+ 4. Then proceed with the task
9
+
10
+ Always load skills marked with always_load: true.
11
+ Do not load skills that are not relevant to the current task.
12
+
13
+ ## Respond in TEXT vs. create FILES
14
+
15
+ Choose your response medium deliberately. Defaulting to file creation when the user asked a question is a common failure mode — especially for coding agents running in web UIs.
16
+
17
+ - **Create or modify FILES when the user's request contains file-action keywords:** `create`, `write`, `generate`, `build`, `implement` (and obvious synonyms such as `add`, `produce`, `refactor`, `fix`, `update <file>`). These signal a concrete artifact is expected.
18
+ - **Respond in TEXT when the request contains text-response keywords:** `what do you think`, `how should we`, `discuss`, `opinion` (and obvious synonyms such as `explain`, `why`, `should I`, `compare`, `recommend`). These signal that a conversation is expected, not a deliverable.
19
+ - **If unsure, respond in TEXT.** A text answer can always be followed by file creation on confirmation; an unwanted file cannot be cleanly undone.
20
+ - **Never create `response.md`, `output.md`, or any similarly named scratch file as a reply.** A reply belongs in the chat transcript, not on disk.
21
+ - **Confirm file paths before writing.** When you are about to create or modify a file whose path the user has not explicitly named, state the intended path in text and wait for confirmation, unless the path is unambiguous from the task context.
22
+
23
+ ## BMAD phase discipline
24
+
25
+ BMAD-METHOD organizes work into declared phases (analysis, planning, architecture, story-creation, implementation, review). Respect the currently declared phase.
26
+
27
+ - **Do not skip ahead to implementation during planning.** If the project is in a planning phase — or the user has asked for requirements, architecture, or a story — produce planning artifacts, not code.
28
+ - **Do not retroactively plan after you have already coded.** If implementation has already started, flag the gap instead of fabricating back-dated planning documents.
29
+ - The declared phase is established by the active skill, the story status, or an explicit statement from the user. When none of these is available, ask before assuming.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ma-agents",
3
- "version": "3.13.0",
3
+ "version": "3.13.1",
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": {
@@ -1,21 +1,21 @@
1
- {
2
- "name": "Git Workflow",
3
- "description": "MANDATORY worktree-based workflow for ALL file-changing activities. Enforces isolated feature branches, conventional commits, and PR-based merging.",
4
- "version": "2.1.0",
5
- "author": "AI Agent Skills",
6
- "tags": [
7
- "git",
8
- "worktrees",
9
- "workflow",
10
- "branching",
11
- "conventional-commits",
12
- "pull-requests",
13
- "multi-agent"
14
- ],
15
- "applies_when": [
16
- "committing changes",
17
- "creating branches or PRs",
18
- "any code writing or modification task"
19
- ],
20
- "always_load": true
21
- }
1
+ {
2
+ "name": "Git Workflow",
3
+ "description": "MANDATORY worktree-based workflow for ALL file-changing activities. Enforces isolated feature branches, conventional commits, and PR-based merging.",
4
+ "version": "2.1.0",
5
+ "author": "AI Agent Skills",
6
+ "tags": [
7
+ "git",
8
+ "worktrees",
9
+ "workflow",
10
+ "branching",
11
+ "conventional-commits",
12
+ "pull-requests",
13
+ "multi-agent"
14
+ ],
15
+ "applies_when": [
16
+ "committing changes",
17
+ "creating branches or PRs",
18
+ "any code writing or modification task"
19
+ ],
20
+ "always_load": true
21
+ }