diff --git a/.changeset/wise-jays-romp.md b/.changeset/wise-jays-romp.md new file mode 100644 index 000000000..dddaff2ad --- /dev/null +++ b/.changeset/wise-jays-romp.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3098 +--- +**`phase_id_convention` set in `.planning/config.json` is no longer silently dropped** — the config loader's resolved-config constructor omitted the key despite it being in the valid-keys manifest, so the milestone-prefix validation check could only be activated via the ROADMAP frontmatter fallback. The key now survives resolution. (#2997) diff --git a/src/config-loader.cts b/src/config-loader.cts index 6159709a6..cf0a95594 100644 --- a/src/config-loader.cts +++ b/src/config-loader.cts @@ -812,6 +812,7 @@ function loadConfigResolved(cwd: string, options: Record = {}): response_language: get('response_language') || null, claude_md_path: get('claude_md_path') || null, claude_md_assembly: (parsed['claude_md_assembly']) || null, + phase_id_convention: get('phase_id_convention') ?? null, }; // ADR-857 phase 3b: federated config overlay diff --git a/tests/config-loader.test.cjs b/tests/config-loader.test.cjs index e0a545528..f10c57e8a 100644 --- a/tests/config-loader.test.cjs +++ b/tests/config-loader.test.cjs @@ -1255,3 +1255,44 @@ describe("loadConfigResolved — corrupt config is distinguishable from absent", assert.equal(res.degraded, false, "an empty file is not corruption"); }); }); + +// ─── #2997: phase_id_convention survives config resolution ───────────────── + +describe('#2997: phase_id_convention is not silently dropped on a clean read', () => { + const { createTempDir } = require('./helpers.cjs'); + const cfgPath = (dir) => path.join(dir, '.planning', 'config.json'); + + test('setting phase_id_convention in config.json survives into the resolved config', () => { + const tmpDir = createTempDir('gsd-2997-'); + try { + fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + fs.writeFileSync(cfgPath(tmpDir), JSON.stringify({ phase_id_convention: 'milestone-prefixed' }), 'utf-8'); + const res = loadConfigResolved(tmpDir); + assert.equal(res.degraded, false, 'read must report as non-degraded'); + assert.equal(res.config.phase_id_convention, 'milestone-prefixed', + `phase_id_convention must survive resolution; got: ${JSON.stringify(res.config.phase_id_convention)}`); + } finally { cleanup(tmpDir); } + }); + + test('phase_id_convention set to null round-trips correctly', () => { + const tmpDir = createTempDir('gsd-2997-null-'); + try { + fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + fs.writeFileSync(cfgPath(tmpDir), JSON.stringify({ phase_id_convention: null }), 'utf-8'); + const res = loadConfigResolved(tmpDir); + assert.equal(res.config.phase_id_convention, null, + 'null phase_id_convention must round-trip as null'); + } finally { cleanup(tmpDir); } + }); + + test('phase_id_convention absent → null in resolved config (no false default)', () => { + const tmpDir = createTempDir('gsd-2997-absent-'); + try { + fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + fs.writeFileSync(cfgPath(tmpDir), JSON.stringify({ commit_docs: true }), 'utf-8'); + const res = loadConfigResolved(tmpDir); + assert.equal(res.config.phase_id_convention, null, + 'absent phase_id_convention must resolve to null, not undefined'); + } finally { cleanup(tmpDir); } + }); +});