fix(config): correct global layer priority — overrides installer defaults
The global user layer was lowest priority, meaning installer-generated defaults in _bmad/config.user.toml always shadowed it. Reorder so global user preferences override installer defaults but are still overridden by hand-authored project customizations. New order for resolve_config.py: base_team → base_user → global_user → custom_team → custom_user New order for resolve_customization.py: defaults → global_user → team → user
This commit is contained in:
parent
9f6e83c255
commit
dfdc18df2f
|
|
@ -3,9 +3,9 @@
|
||||||
Resolve BMad's central config using five-layer TOML merge.
|
Resolve BMad's central config using five-layer TOML merge.
|
||||||
|
|
||||||
Reads from five layers (highest priority last):
|
Reads from five layers (highest priority last):
|
||||||
1. ~/.bmad/config/config.user.toml (global user defaults)
|
1. {project-root}/_bmad/config.toml (installer-owned team)
|
||||||
2. {project-root}/_bmad/config.toml (installer-owned team)
|
2. {project-root}/_bmad/config.user.toml (installer-owned user)
|
||||||
3. {project-root}/_bmad/config.user.toml (installer-owned user)
|
3. ~/.bmad/config/config.user.toml (global user preferences)
|
||||||
4. {project-root}/_bmad/custom/config.toml (human-authored team, committed)
|
4. {project-root}/_bmad/custom/config.toml (human-authored team, committed)
|
||||||
5. {project-root}/_bmad/custom/config.user.toml (human-authored user, gitignored)
|
5. {project-root}/_bmad/custom/config.user.toml (human-authored user, gitignored)
|
||||||
|
|
||||||
|
|
@ -153,14 +153,14 @@ def main():
|
||||||
project_root = Path(args.project_root).resolve()
|
project_root = Path(args.project_root).resolve()
|
||||||
bmad_dir = project_root / "_bmad"
|
bmad_dir = project_root / "_bmad"
|
||||||
|
|
||||||
global_user = load_toml(GLOBAL_DIR / "config.user.toml")
|
|
||||||
base_team = load_toml(bmad_dir / "config.toml", required=True)
|
base_team = load_toml(bmad_dir / "config.toml", required=True)
|
||||||
base_user = load_toml(bmad_dir / "config.user.toml")
|
base_user = load_toml(bmad_dir / "config.user.toml")
|
||||||
|
global_user = load_toml(GLOBAL_DIR / "config.user.toml")
|
||||||
custom_team = load_toml(bmad_dir / "custom" / "config.toml")
|
custom_team = load_toml(bmad_dir / "custom" / "config.toml")
|
||||||
custom_user = load_toml(bmad_dir / "custom" / "config.user.toml")
|
custom_user = load_toml(bmad_dir / "custom" / "config.user.toml")
|
||||||
|
|
||||||
merged = deep_merge(global_user, base_team)
|
merged = deep_merge(base_team, base_user)
|
||||||
merged = deep_merge(merged, base_user)
|
merged = deep_merge(merged, global_user)
|
||||||
merged = deep_merge(merged, custom_team)
|
merged = deep_merge(merged, custom_team)
|
||||||
merged = deep_merge(merged, custom_user)
|
merged = deep_merge(merged, custom_user)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ Resolve customization for a BMad skill using four-layer TOML merge.
|
||||||
Reads customization from four layers (highest priority first):
|
Reads customization from four layers (highest priority first):
|
||||||
1. {project-root}/_bmad/custom/{name}.user.toml (personal, gitignored)
|
1. {project-root}/_bmad/custom/{name}.user.toml (personal, gitignored)
|
||||||
2. {project-root}/_bmad/custom/{name}.toml (team/org, committed)
|
2. {project-root}/_bmad/custom/{name}.toml (team/org, committed)
|
||||||
3. {skill-root}/customize.toml (skill defaults)
|
3. ~/.bmad/config/{name}.user.toml (global user preferences)
|
||||||
4. ~/.bmad/config/{name}.user.toml (global user defaults)
|
4. {skill-root}/customize.toml (skill defaults)
|
||||||
|
|
||||||
Skill name is derived from the basename of the skill directory.
|
Skill name is derived from the basename of the skill directory.
|
||||||
|
|
||||||
|
|
@ -215,7 +215,7 @@ def main():
|
||||||
|
|
||||||
global_user = load_toml(GLOBAL_DIR / f"{skill_name}.user.toml")
|
global_user = load_toml(GLOBAL_DIR / f"{skill_name}.user.toml")
|
||||||
|
|
||||||
merged = deep_merge(global_user, defaults)
|
merged = deep_merge(defaults, global_user)
|
||||||
merged = deep_merge(merged, team)
|
merged = deep_merge(merged, team)
|
||||||
merged = deep_merge(merged, user)
|
merged = deep_merge(merged, user)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ async function withTempDir(fn) {
|
||||||
async function testResolveConfig() {
|
async function testResolveConfig() {
|
||||||
console.log('\n--- resolve_config.py ---\n');
|
console.log('\n--- resolve_config.py ---\n');
|
||||||
|
|
||||||
// Test 1: global user layer is loaded
|
// Test 1: global user overrides installer defaults
|
||||||
await withTempDir(async (tmpDir) => {
|
await withTempDir(async (tmpDir) => {
|
||||||
const globalDir = path.join(tmpDir, '.bmad', 'config');
|
const globalDir = path.join(tmpDir, '.bmad', 'config');
|
||||||
await fs.mkdir(globalDir, { recursive: true });
|
await fs.mkdir(globalDir, { recursive: true });
|
||||||
|
|
@ -79,7 +79,10 @@ async function testResolveConfig() {
|
||||||
await fs.mkdir(path.join(bmadDir, 'custom'), { recursive: true });
|
await fs.mkdir(path.join(bmadDir, 'custom'), { recursive: true });
|
||||||
await writeToml(path.join(bmadDir, 'config.toml'), {
|
await writeToml(path.join(bmadDir, 'config.toml'), {
|
||||||
project_name: 'TestProject',
|
project_name: 'TestProject',
|
||||||
user_name: 'ProjectAlice',
|
user_name: 'InstallerDefault',
|
||||||
|
});
|
||||||
|
await writeToml(path.join(bmadDir, 'config.user.toml'), {
|
||||||
|
user_name: 'InstallerUserDefault',
|
||||||
});
|
});
|
||||||
|
|
||||||
const origHome = process.env.HOME;
|
const origHome = process.env.HOME;
|
||||||
|
|
@ -88,18 +91,18 @@ async function testResolveConfig() {
|
||||||
const result = JSON.parse(execSync(`python3 ${SCRIPTS_DIR}/resolve_config.py --project-root ${projectDir}`, { encoding: 'utf-8' }));
|
const result = JSON.parse(execSync(`python3 ${SCRIPTS_DIR}/resolve_config.py --project-root ${projectDir}`, { encoding: 'utf-8' }));
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
result.user_name === 'ProjectAlice',
|
result.user_name === 'GlobalAlice',
|
||||||
'project config.user overrides global user_name',
|
'global user overrides installer defaults',
|
||||||
`Expected "ProjectAlice", got "${result.user_name}"`,
|
`Expected "GlobalAlice", got "${result.user_name}"`,
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
result.communication_language === 'en',
|
result.communication_language === 'en',
|
||||||
'global communication_language preserved when project has no override',
|
'global communication_language preserved when installer has no override',
|
||||||
`Expected "en", got "${result.communication_language}"`,
|
`Expected "en", got "${result.communication_language}"`,
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
result.project_name === 'TestProject',
|
result.project_name === 'TestProject',
|
||||||
'project config values preserved',
|
'installer team config values preserved',
|
||||||
`Expected "TestProject", got "${result.project_name}"`,
|
`Expected "TestProject", got "${result.project_name}"`,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -131,19 +134,19 @@ async function testResolveConfig() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test 3: full priority chain — global < base_team < base_user < custom_team < custom_user
|
// Test 3: full priority chain — base_team < base_user < global < custom_team < custom_user
|
||||||
await withTempDir(async (tmpDir) => {
|
await withTempDir(async (tmpDir) => {
|
||||||
const globalDir = path.join(tmpDir, '.bmad', 'config');
|
const globalDir = path.join(tmpDir, '.bmad', 'config');
|
||||||
await fs.mkdir(globalDir, { recursive: true });
|
await fs.mkdir(globalDir, { recursive: true });
|
||||||
await writeToml(path.join(globalDir, 'config.user.toml'), {
|
await writeToml(path.join(globalDir, 'config.user.toml'), {
|
||||||
user_name: 'L0-Global',
|
user_name: 'L2-Global',
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectDir = path.join(tmpDir, 'project');
|
const projectDir = path.join(tmpDir, 'project');
|
||||||
const bmadDir = path.join(projectDir, '_bmad');
|
const bmadDir = path.join(projectDir, '_bmad');
|
||||||
await fs.mkdir(path.join(bmadDir, 'custom'), { recursive: true });
|
await fs.mkdir(path.join(bmadDir, 'custom'), { recursive: true });
|
||||||
await writeToml(path.join(bmadDir, 'config.toml'), { user_name: 'L1-BaseTeam' });
|
await writeToml(path.join(bmadDir, 'config.toml'), { user_name: 'L0-BaseTeam' });
|
||||||
await writeToml(path.join(bmadDir, 'config.user.toml'), { user_name: 'L2-BaseUser' });
|
await writeToml(path.join(bmadDir, 'config.user.toml'), { user_name: 'L1-BaseUser' });
|
||||||
await writeToml(path.join(bmadDir, 'custom', 'config.toml'), { user_name: 'L3-CustomTeam' });
|
await writeToml(path.join(bmadDir, 'custom', 'config.toml'), { user_name: 'L3-CustomTeam' });
|
||||||
await writeToml(path.join(bmadDir, 'custom', 'config.user.toml'), { user_name: 'L4-CustomUser' });
|
await writeToml(path.join(bmadDir, 'custom', 'config.user.toml'), { user_name: 'L4-CustomUser' });
|
||||||
|
|
||||||
|
|
@ -211,7 +214,7 @@ async function testResolveConfig() {
|
||||||
async function testResolveCustomization() {
|
async function testResolveCustomization() {
|
||||||
console.log('\n--- resolve_customization.py ---\n');
|
console.log('\n--- resolve_customization.py ---\n');
|
||||||
|
|
||||||
// Test 1: global skill user layer is loaded
|
// Test 1: global skill user overrides skill defaults
|
||||||
await withTempDir(async (tmpDir) => {
|
await withTempDir(async (tmpDir) => {
|
||||||
const globalDir = path.join(tmpDir, '.bmad', 'config');
|
const globalDir = path.join(tmpDir, '.bmad', 'config');
|
||||||
await fs.mkdir(globalDir, { recursive: true });
|
await fs.mkdir(globalDir, { recursive: true });
|
||||||
|
|
@ -232,9 +235,9 @@ async function testResolveCustomization() {
|
||||||
const result = JSON.parse(execSync(`python3 ${SCRIPTS_DIR}/resolve_customization.py --skill ${skillDir}`, { encoding: 'utf-8' }));
|
const result = JSON.parse(execSync(`python3 ${SCRIPTS_DIR}/resolve_customization.py --skill ${skillDir}`, { encoding: 'utf-8' }));
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
result.agent === 'default-agent-prompt',
|
result.agent === 'global-agent-prompt',
|
||||||
'skill defaults override global user layer',
|
'global user overrides skill defaults',
|
||||||
`Expected "default-agent-prompt", got "${result.agent}"`,
|
`Expected "global-agent-prompt", got "${result.agent}"`,
|
||||||
);
|
);
|
||||||
assert(result.version === '1.0.0', 'skill default values preserved', `Expected "1.0.0", got "${result.version}"`);
|
assert(result.version === '1.0.0', 'skill default values preserved', `Expected "1.0.0", got "${result.version}"`);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -291,17 +294,17 @@ async function testResolveCustomization() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test 4: full priority chain — global < defaults < team < user
|
// Test 4: full priority chain — defaults < global < team < user
|
||||||
await withTempDir(async (tmpDir) => {
|
await withTempDir(async (tmpDir) => {
|
||||||
const globalDir = path.join(tmpDir, '.bmad', 'config');
|
const globalDir = path.join(tmpDir, '.bmad', 'config');
|
||||||
await fs.mkdir(globalDir, { recursive: true });
|
await fs.mkdir(globalDir, { recursive: true });
|
||||||
await writeToml(path.join(globalDir, 'test-skill.user.toml'), {
|
await writeToml(path.join(globalDir, 'test-skill.user.toml'), {
|
||||||
agent: 'L0-Global',
|
agent: 'L1-Global',
|
||||||
});
|
});
|
||||||
|
|
||||||
const skillDir = path.join(tmpDir, 'project', '_bmad', 'skills', 'test-skill');
|
const skillDir = path.join(tmpDir, 'project', '_bmad', 'skills', 'test-skill');
|
||||||
await fs.mkdir(skillDir, { recursive: true });
|
await fs.mkdir(skillDir, { recursive: true });
|
||||||
await writeToml(path.join(skillDir, 'customize.toml'), { agent: 'L1-Defaults' });
|
await writeToml(path.join(skillDir, 'customize.toml'), { agent: 'L0-Defaults' });
|
||||||
|
|
||||||
const customDir = path.join(tmpDir, 'project', '_bmad', 'custom');
|
const customDir = path.join(tmpDir, 'project', '_bmad', 'custom');
|
||||||
await fs.mkdir(customDir, { recursive: true });
|
await fs.mkdir(customDir, { recursive: true });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue