fix(installer): strip core-key pollution from [modules.*]; soften config headers

- writeCentralConfig now always strips core-module keys from every
  [modules.<code>] bucket, even when the module's schema is not
  available in src/ (external / marketplace modules like cis, bmb).
  Core values belong in [core] only; workflows read them directly.
- When the module's own schema IS available (built-in modules),
  also drop any key it does not declare as a prompt — same
  spread-pollution filter as before, now layered on top.
- Section-aware headers on both _bmad/config.toml and
  _bmad/config.user.toml: [core] / [modules.*] values are
  editable (installer reads them as defaults on next install);
  [agents.*] is regenerated from module.yaml and will be wiped —
  overrides for agents go in _bmad/custom/config*.toml instead.
This commit is contained in:
Brian Madison 2026-04-19 19:38:52 -05:00
parent 6b91aa249d
commit ffcdaf7529
1 changed files with 41 additions and 10 deletions

View File

@ -423,11 +423,26 @@ class ManifestGenerator {
}
}
const partition = (moduleName, cfg) => {
// Core keys are always known (core module.yaml is built-in). These are
// the only keys allowed in [core]; they must be stripped from every
// non-core module bucket because legacy _bmad/{mod}/config.yaml files
// spread core values into each module. Core belongs in [core] only —
// workflows that need user_name/language/etc. read [core] directly.
const coreKeys = new Set(Object.keys(scopeByModuleKey.core || {}));
// Partition a module's answered config into team vs user buckets.
// For non-core modules: strip core keys always; when we know the module's
// own schema, also drop keys it doesn't declare. Unknown-schema modules
// (external / marketplace) fall through with their remaining answers as
// team so they don't vanish from the config.
const partition = (moduleName, cfg, onlyDeclaredKeys = false) => {
const team = {};
const user = {};
const scopes = scopeByModuleKey[moduleName] || {};
const isCore = moduleName === 'core';
for (const [key, value] of Object.entries(cfg || {})) {
if (!isCore && coreKeys.has(key)) continue;
if (onlyDeclaredKeys && !(key in scopes)) continue;
if (scopes[key] === 'user') {
user[key] = value;
} else {
@ -439,22 +454,33 @@ class ManifestGenerator {
const teamHeader = [
'# ─────────────────────────────────────────────────────────────────',
'# DO NOT EDIT — regenerated on every install.',
'# Installer-managed. Regenerated on every install.',
'#',
'# To override any value, add it to one of:',
'# _bmad/custom/config.toml (team, committed to version control)',
'# _bmad/custom/config.user.toml (personal, gitignored)',
'# [core] and [modules.<code>] values: you CAN edit these directly.',
'# The installer reads current values as defaults on next install,',
'# so your edits persist.',
'#',
'# [agents.<code>] values: regenerated from each module.yaml on every',
'# install. DO NOT edit here — your changes will be wiped. To override',
'# an agent descriptor or add custom agents, use:',
'# _bmad/custom/config.toml (team, committed)',
'# _bmad/custom/config.user.toml (personal, gitignored)',
'# Those files are never touched by the installer.',
'# ─────────────────────────────────────────────────────────────────',
'',
];
const userHeader = [
'# ─────────────────────────────────────────────────────────────────',
'# DO NOT EDIT — regenerated on every install.',
'# This file holds install answers scoped to YOU personally.',
'# Installer-managed. Regenerated on every install.',
'# Holds install answers scoped to YOU personally.',
'#',
'# To override any value, add it to:',
'# _bmad/custom/config.user.toml (personal, gitignored)',
'# You CAN edit values here directly. The installer reads current',
'# values as defaults on next install, so your edits persist.',
'#',
'# For custom agents or sections the installer does not know about,',
'# use _bmad/custom/config.user.toml — it is never touched by the',
'# installer.',
'# ─────────────────────────────────────────────────────────────────',
'',
];
@ -485,7 +511,12 @@ class ManifestGenerator {
if (moduleName === 'core') continue;
const cfg = moduleConfigs[moduleName];
if (!cfg || Object.keys(cfg).length === 0) continue;
const { team: modTeam, user: modUser } = partition(moduleName, cfg);
// Only filter out spread-from-core pollution when we actually know
// this module's prompt schema. For external/marketplace modules whose
// module.yaml isn't in the src tree, fall through as all-team so we
// don't drop their real answers.
const haveSchema = Object.keys(scopeByModuleKey[moduleName] || {}).length > 0;
const { team: modTeam, user: modUser } = partition(moduleName, cfg, haveSchema);
if (Object.keys(modTeam).length > 0) {
teamLines.push(`[modules.${moduleName}]`);
for (const [key, value] of Object.entries(modTeam)) {