fix(#3531): merge routing_tier_defaults over manifest tier defaults (#3539)

* test(#3531): failing-first suite for routing_tier_defaults manifest merge

* fix(#3531): merge routing_tier_defaults over manifest tier defaults

* docs(#3531): document routing_tier_defaults merge-over-built-ins semantics

* fix(#3531): correct test helper scope, update folded #443 expectations, guard merge keys

* test(#3531): pin tiers in effort-sync and surface-axis fixtures post-merge

* chore(#3531): backfill changeset pr number

* fix(#3531): correct rebase resolution — keep both 3531 and 3533 test blocks intact

* test(#3531): pin inherit/effort fixtures to the layer that reaches tiered agents

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-15 08:19:35 -04:00
committed by GitHub
parent bc9a22868f
commit b7cca0363f
9 changed files with 328 additions and 56 deletions

View File

@@ -3774,7 +3774,10 @@ describe('#3533 effort sync: inherit means the key must not exist', () => {
// Fixture carries its own name so the survivor assertion below is
// satisfiable (AGENT_WITH_EFFORT names gsd-planner — wrong file).
fs.writeFileSync(path.join(agentsDir, 'gsd-executor.md'), AGENT_WITH_EFFORT.replace('name: gsd-planner', 'name: gsd-executor'));
writePlanningConfig(tmpDir, { default: 'inherit' });
// #3531+#3533 combined: pin every TIER to inherit — a bare effort.default
// no longer reaches a tiered agent now that the config block merges over
// the built-in tier ladder (the manifest standard tier would answer 'high').
writePlanningConfig(tmpDir, { routing_tier_defaults: { light: 'inherit', standard: 'inherit', heavy: 'inherit' } });
const { cmdEffortSync } = require('../gsd-core/bin/lib/commands.cjs');
const result = captureOutput(() =>
@@ -3897,7 +3900,10 @@ describe('feat-488: effort sync command', () => {
const agentsDir = makeAgentsDir(tmpDir);
const agentPath = path.join(agentsDir, 'gsd-executor.md');
fs.writeFileSync(agentPath, AGENT_WITHOUT_EFFORT);
writePlanningConfig(tmpDir, { default: 'max' });
// #3531: pin every tier so the injected value is tier-independent — an
// effort.default alone no longer answers for a tiered agent now that the
// config block merges over the built-in tier ladder.
writePlanningConfig(tmpDir, { routing_tier_defaults: { light: 'max', standard: 'max', heavy: 'max' }, default: 'max' });
const { cmdEffortSync } = require('../gsd-core/bin/lib/commands.cjs');
const result = captureOutput(() =>
@@ -3946,10 +3952,14 @@ describe('feat-488: effort sync command', () => {
fs.mkdirSync(planningDir, { recursive: true });
fs.writeFileSync(path.join(planningDir, 'config.json'), JSON.stringify({ model_profile: 'balanced' }));
// Home defaults set effort.default = low
// Home defaults set the heavy tier effort to low. (#3531: a bare home
// effort.default would no longer reach gsd-planner — the merged tier
// ladder answers for tiered agents — so the home fixture pins the tier,
// which is what this test's claim actually exercises: home-level effort
// applies when the project config has no effort section.)
const gsdDir = path.join(tmpHome, '.gsd');
fs.mkdirSync(gsdDir, { recursive: true });
fs.writeFileSync(path.join(gsdDir, 'defaults.json'), JSON.stringify({ effort: { default: 'low' } }));
fs.writeFileSync(path.join(gsdDir, 'defaults.json'), JSON.stringify({ effort: { routing_tier_defaults: { heavy: 'low' } } }));
// Isolate HOME (and USERPROFILE for Windows parity) so
// readGsdEffectiveEffortConfig reads our fixture, not the
@@ -3978,7 +3988,7 @@ describe('feat-488: effort sync command', () => {
}
}
// With home effort.default = 'low' and the agent currently at 'medium',
// With home heavy-tier effort 'low' and the agent currently at 'medium',
// cmdEffortSync must sync exactly 1 agent and set it to 'low'.
assert.equal(result.synced, 1, 'should sync 1 agent whose effort differs from home default');
assert.equal(result.changes[0].agent, 'gsd-planner');

View File

@@ -149,7 +149,9 @@ describe('#3534 resolve-execution reports resolved AND effective effort', () =>
t.after(() => cleanup(dir));
fs.writeFileSync(
path.join(dir, '.planning', 'config.json'),
JSON.stringify({ runtime: 'codex', effort: { default: 'medium' } }, null, 2),
// #3531+#3534 combined: pin the AGENT — a bare effort.default no longer
// reaches a tiered agent under the merged tier ladder.
JSON.stringify({ runtime: 'codex', effort: { agent_overrides: { 'gsd-executor': 'medium' } } }, null, 2),
);
const out = resolveExecution(dir);
assert.equal(out.effort, 'medium');
@@ -440,7 +442,10 @@ describe('#2481 — the escalation surface renders argv (CLI-level, not a workfl
fs.writeFileSync(
path.join(dir, '.planning', 'config.json'),
JSON.stringify({
effort: { default: 'low' },
// #3531: pin the heavy tier rather than effort.default — a bare default
// no longer answers for gsd-planner (heavy) now that the config block
// merges over the built-in tier ladder.
effort: { routing_tier_defaults: { heavy: 'low' } },
dynamic_routing: { enabled: true, escalate_on_failure: true, max_escalations: 3 },
}, null, 2),
);
@@ -578,7 +583,9 @@ describe('#2481 review workflow resolves effort per reviewer', () => {
fs.mkdirSync(path.join(projectDir, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(projectDir, '.planning', 'config.json'),
JSON.stringify({ effort: { default: 'xhigh' } }, null, 2),
// #3531: pin every tier so the expected value is agent-independent —
// the reviewer lane's tier decides, not effort.default.
JSON.stringify({ effort: { routing_tier_defaults: { light: 'xhigh', standard: 'xhigh', heavy: 'xhigh' } } }, null, 2),
);
const r = cp.spawnSync(

View File

@@ -4104,6 +4104,68 @@ describe('#3533 inherit: install writes NO effort key when the agent resolves to
});
});
// ─── describe 5c: #3531 (10c) — a partial effort block must not discard manifest tier defaults ──
//
// Regression fixture from issue #3160: adding four agent_overrides produced a
// 23-agent dry-run diff that DOWNGRADED xhigh-tier agents to high and UPGRADED
// low-tier agents to high — because an effort block without
// routing_tier_defaults disabled the built-in tier ladder (manifest tier
// defaults were consulted only when the whole effort block was absent).
describe('#3531 resolveInstallTimeEffort: agent_overrides-only effort block keeps manifest tier defaults', () => {
let tmpDir;
let claudeHome;
beforeEach(() => {
tmpDir = makeTmpDir('gsd-3531-tier-merge-');
const projectDir = path.join(tmpDir, 'project');
claudeHome = path.join(projectDir, '.claude');
fs.mkdirSync(claudeHome, { recursive: true });
fs.mkdirSync(path.join(projectDir, '.planning'), { recursive: true });
});
afterEach(() => {
cleanup(tmpDir);
});
function writeProjectConfig(config) {
const projectDir = path.dirname(claudeHome);
fs.writeFileSync(
path.join(projectDir, '.planning', 'config.json'),
JSON.stringify(config, null, 2)
);
}
function readInstalledEffort(agentFile) {
const fm = readFrontmatter(path.join(claudeHome, 'agents', agentFile));
const match = fm.match(/^effort:\s*(\S+)$/m);
assert.ok(match, `effort: must be present in ${agentFile} frontmatter\nActual:\n${fm}`);
return match[1];
}
test('agent_overrides-only block leaves untiered agents on their manifest tier defaults', () => {
// The exact #3160 shape: one agent overridden, no routing_tier_defaults.
writeProjectConfig({ effort: { agent_overrides: { 'gsd-code-reviewer': 'medium' } } });
runGlobalInstall('claude', claudeHome);
// Before #3531: every non-overridden agent collapsed to the manifest
// default 'high' — downgrading planner (xhigh) and upgrading mapper (low).
assert.strictEqual(readInstalledEffort('gsd-planner.md'), 'xhigh');
assert.strictEqual(readInstalledEffort('gsd-executor.md'), 'high');
assert.strictEqual(readInstalledEffort('gsd-codebase-mapper.md'), 'low');
// The one agent the user actually named still gets its override.
assert.strictEqual(readInstalledEffort('gsd-code-reviewer.md'), 'medium');
});
test('partial routing_tier_defaults fills gaps from the manifest at install', () => {
writeProjectConfig({ effort: { routing_tier_defaults: { heavy: 'medium' } } });
runGlobalInstall('claude', claudeHome);
assert.strictEqual(readInstalledEffort('gsd-planner.md'), 'medium');
assert.strictEqual(readInstalledEffort('gsd-executor.md'), 'high');
assert.strictEqual(readInstalledEffort('gsd-codebase-mapper.md'), 'low');
});
});
// ─── describe 5: Source stays clean ──────────────────────────────────────────
describe('#443 Source purity: agents/gsd-planner.md has no effort: key', () => {

View File

@@ -258,6 +258,11 @@ describe('assertValidGranularityOverride', () => {
// ─── resolveEffortInternal ────────────────────────────────────────────────────
// #3531 — the runtime resolver's install-time sibling. Driven directly as a
// pure function (effortCfg in, effort out) for the parity matrix below.
const installEffortResolver = require('../gsd-core/bin/lib/install-effort-resolver.cjs');
const { resolveInstallTimeEffort, readGsdEffectiveEffortConfig } = installEffortResolver;
describe('resolveEffortInternal', () => {
let tmpDir;
beforeEach(() => { tmpDir = makeTempProject(); });
@@ -318,8 +323,10 @@ describe('#3533 effort inherit: expressible at every layer, never a wire level',
writeConfig(tmpDir, { effort: { default: 'inherit' } });
assert.strictEqual(resolveEffortInternal(tmpDir, 'completely-unknown-agent-xyz'), 'inherit');
// A tiered agent under an inherit tier default also inherits (tier layer won).
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-planner'), 'inherit');
// #3531+#3533 combined: a bare effort.default no longer reaches a TIERED
// agent — the merged tier layer answers (manifest heavy = xhigh). To
// inherit at a tier, pin the tier; the tier-default row above covers that.
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-planner'), 'xhigh');
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-executor', { override: 'inherit' }), 'inherit');
});
@@ -347,6 +354,113 @@ describe('#3533 effort inherit: expressible at every layer, never a wire level',
});
});
// ─── #3531 (10c): routing_tier_defaults merges over manifest tier defaults ───
describe('#3531 routing_tier_defaults merge: manifest built-ins survive partial config', () => {
let tmpDir;
beforeEach(() => { tmpDir = makeTempProject(); });
afterEach(() => { cleanup(tmpDir); });
test('effort block without routing_tier_defaults keeps manifest tier defaults (runtime)', () => {
writeConfig(tmpDir, { effort: { agent_overrides: { 'gsd-executor': 'low' } } });
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-planner'), 'xhigh');
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-executor'), 'low');
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-codebase-mapper'), 'low');
});
test('partial routing_tier_defaults merges over manifest, gaps filled per-tier (runtime)', () => {
writeConfig(tmpDir, { effort: { routing_tier_defaults: { heavy: 'medium' } } });
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-planner'), 'medium');
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-executor'), 'high');
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-codebase-mapper'), 'low');
});
test('non-object routing_tier_defaults treated as absent (runtime)', () => {
writeConfig(tmpDir, { effort: { routing_tier_defaults: ['heavy'] } });
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-planner'), 'xhigh');
});
test('merge never mutates the manifest defaults', () => {
const configuration = require('../gsd-core/bin/lib/configuration.cjs');
const before = JSON.stringify(configuration.CONFIG_DEFAULTS['effort']);
writeConfig(tmpDir, { effort: { routing_tier_defaults: { heavy: 'medium' } } });
resolveEffortInternal(tmpDir, 'gsd-planner');
resolveInstallTimeEffort({ routing_tier_defaults: { heavy: 'medium' } }, 'gsd-planner');
assert.strictEqual(
JSON.stringify(configuration.CONFIG_DEFAULTS['effort']), before,
'resolving must not mutate CANONICAL_CONFIG_DEFAULTS.effort',
);
});
});
describe('#3531 parity: runtime and install-time resolvers agree on the merged tier ladder', () => {
let tmpDir;
beforeEach(() => { tmpDir = makeTempProject(); });
afterEach(() => { cleanup(tmpDir); });
const PARITY_AGENTS = ['gsd-planner', 'gsd-executor', 'gsd-codebase-mapper', 'completely-unknown-agent-xyz'];
const PARITY_EFFORT_CFGS = [
null,
{},
{ default: 'low' },
{ routing_tier_defaults: { heavy: 'medium' } },
{ routing_tier_defaults: { light: 'low', standard: 'medium', heavy: 'low' } },
{ routing_tier_defaults: { heavy: 'turbo' }, default: 'low' },
{ routing_tier_defaults: 'not-an-object' },
{ agent_overrides: { 'gsd-executor': 'max' } },
{ agent_overrides: { 'gsd-executor': 42 }, routing_tier_defaults: { heavy: 'medium' } },
];
for (const effortCfg of PARITY_EFFORT_CFGS) {
for (const agent of PARITY_AGENTS) {
test(`parity: effortCfg=${JSON.stringify(effortCfg)} agent=${agent}`, () => {
writeConfig(tmpDir, effortCfg === null ? {} : { effort: effortCfg });
const runtime = resolveEffortInternal(tmpDir, agent);
const installTime = resolveInstallTimeEffort(effortCfg, agent);
assert.strictEqual(
installTime, runtime,
`install-time and runtime resolvers disagree for effortCfg=${JSON.stringify(effortCfg)} agent=${agent}`,
);
});
}
}
});
describe('#3531 readGsdEffectiveEffortConfig: home/project routing_tier_defaults deep-merge', () => {
test('project partial tier block unions with home partial tier block per-tier', (t) => {
const tmpDir = makeTempProject('gsd-3531-home-merge-');
t.after(() => cleanup(tmpDir));
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3531-home-'));
t.after(() => cleanup(homeDir));
fs.mkdirSync(path.join(homeDir, '.gsd'), { recursive: true });
fs.writeFileSync(
path.join(homeDir, '.gsd', 'defaults.json'),
JSON.stringify({ effort: { routing_tier_defaults: { heavy: 'low' } } }),
);
writeConfig(tmpDir, { effort: { routing_tier_defaults: { standard: 'medium' } } });
const oldHome = process.env.HOME;
const oldUserProfile = process.env.USERPROFILE;
process.env.HOME = homeDir;
process.env.USERPROFILE = homeDir; // os.homedir() is USERPROFILE-driven on win32
t.after(() => {
process.env.HOME = oldHome;
if (oldUserProfile === undefined) delete process.env.USERPROFILE;
else process.env.USERPROFILE = oldUserProfile;
});
const merged = readGsdEffectiveEffortConfig(tmpDir);
assert.deepStrictEqual(merged && merged.routing_tier_defaults, { heavy: 'low', standard: 'medium' });
// The merged config resolves planner from HOME's heavy (low), executor from
// project's standard (medium), mapper from the manifest light (low).
assert.strictEqual(resolveInstallTimeEffort(merged, 'gsd-planner'), 'low');
assert.strictEqual(resolveInstallTimeEffort(merged, 'gsd-executor'), 'medium');
assert.strictEqual(resolveInstallTimeEffort(merged, 'gsd-codebase-mapper'), 'low');
});
});
// ─── nextEffort ────────────────────────────────────────────────────────────────
describe('nextEffort', () => {
@@ -1855,13 +1969,28 @@ describe('#443 integration (f): precedence matrix (property/table-driven)', () =
expected: 'medium',
},
{
label: 'layer 4 (effort.default) when no tier default set',
// #3531 (10c): an effort block without routing_tier_defaults no longer
// discards the manifest tier defaults — gsd-planner (heavy) gets the
// manifest 'xhigh', not effort.default. effort.default is still the
// layer that answers for an agent with NO catalog tier (see the
// unknown-agent row below, which is what this layer actually names).
label: 'layer 4 (effort.default) when agent has no catalog tier',
config: {
effort: { default: 'low' },
},
opts: {},
agent: 'completely-unknown-agent-xyz',
expected: 'low',
},
{
label: '10c: effort block without routing_tier_defaults keeps manifest tier defaults (#3531)',
config: {
effort: { default: 'low' },
},
opts: {},
agent: 'gsd-planner',
expected: 'xhigh',
},
{
label: 'invalid layer 1 (turbo) falls through to layer 2 (agent_override)',
config: {
@@ -1882,7 +2011,10 @@ describe('#443 integration (f): precedence matrix (property/table-driven)', () =
expected: 'high',
},
{
label: 'invalid tier default (turbo) falls through to effort.default',
// #3531 (10c): under the merged tier layer, an invalid config value for
// a tier falls back to the MANIFEST value for that same tier (xhigh for
// heavy), not to effort.default.
label: 'invalid tier default (turbo) falls back to the manifest value for that tier (#3531)',
config: {
effort: {
routing_tier_defaults: { heavy: 'turbo' },
@@ -1890,14 +2022,16 @@ describe('#443 integration (f): precedence matrix (property/table-driven)', () =
},
},
opts: {},
expected: 'low',
expected: 'xhigh',
},
];
for (const row of effortPrecedenceTable) {
test(`effort precedence: ${row.label}`, () => {
writeConfig(tmpDir, row.config);
const result = resolveEffortInternal(tmpDir, 'gsd-planner', row.opts);
// #3531: rows may pin a specific agent (default keeps the historical
// gsd-planner target so existing rows are unchanged in what they assert).
const result = resolveEffortInternal(tmpDir, row.agent || 'gsd-planner', row.opts);
assert.strictEqual(result, row.expected,
`Expected '${row.expected}', got '${result}' — config: ${JSON.stringify(row.config)}`);
});
@@ -2297,22 +2431,25 @@ describe('#443 effort cascade', () => {
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-planner'), 'medium');
});
test('invalid routing_tier_defaults value falls through to effort.default', () => {
test('invalid routing_tier_defaults value falls back to the manifest value for that tier (#3531)', () => {
writeConfig(tmpDir, {
effort: {
routing_tier_defaults: { heavy: 'turbo' },
default: 'low',
},
});
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-planner'), 'low');
// #3531: the config block merges OVER the manifest built-ins; an invalid
// entry is dropped by the merge, so the manifest heavy default surfaces.
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-planner'), 'xhigh');
});
test('invalid effort.default falls through to hardcoded "high" (no routing_tier_defaults set)', () => {
test('invalid effort.default falls through to the manifest tier default (#3531)', () => {
writeConfig(tmpDir, {
effort: { default: 'turbo' },
});
// effortCfg set but no routing_tier_defaults; turbo is invalid; fallback = hardcoded 'high'
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-planner'), 'high');
// #3531: an effort block without routing_tier_defaults keeps the manifest
// tier ladder; the invalid default never answers for a tiered agent.
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-planner'), 'xhigh');
});
test('unknown agent -> uses effort.default', () => {
@@ -2323,12 +2460,12 @@ describe('#443 effort cascade', () => {
assert.strictEqual(resolveEffortInternal(tmpDir, 'unknown-agent-xyz'), 'medium');
});
test('effort.default numeric value (123) ignored, hardcoded "high" fallback', () => {
test('effort.default numeric value (123) ignored, manifest tier default answers (#3531)', () => {
writeConfig(tmpDir, {
effort: { default: 123 },
});
// effortCfg set, no routing_tier_defaults -> no tier default; numeric ignored -> 'high'
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-planner'), 'high');
// #3531: no valid tier override -> manifest heavy default; numeric default ignored.
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-planner'), 'xhigh');
});
test('effort block missing entirely -> uses tier default', () => {
@@ -2344,11 +2481,12 @@ describe('#443 effort cascade', () => {
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-planner'), 'xhigh');
});
test('effort.routing_tier_defaults empty object -> effort.default', () => {
test('effort.routing_tier_defaults empty object -> manifest tier default (#3531)', () => {
writeConfig(tmpDir, {
effort: { routing_tier_defaults: {}, default: 'low' },
});
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-planner'), 'low');
// #3531: an empty override block leaves the manifest built-ins in force.
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-planner'), 'xhigh');
});
});
@@ -2771,8 +2909,9 @@ describe('#443 QA matrix — malformed effort/fast_mode configs', () => {
default: 'medium',
},
});
// boolean true is not a valid effort -> falls through to default 'medium'
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-planner'), 'medium');
// #3531: boolean true is not a valid effort -> the merge drops it and the
// manifest heavy default (xhigh) surfaces, not effort.default 'medium'.
assert.strictEqual(resolveEffortInternal(tmpDir, 'gsd-planner'), 'xhigh');
});
test('effort.agent_overrides is non-object -> falls through gracefully', () => {