From aa7697fe97763f3fb32dc52556084b7833037d42 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 5 Aug 2026 21:26:38 -0400 Subject: [PATCH] fix(#2997): include phase_id_convention in resolved config (#3098) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(#2997): include phase_id_convention in resolved config _baseConfig in config-loader.cts is an explicit allowlist of keys copied from parsed into the resolved config. phase_id_convention was in VALID_CONFIG_KEYS (manifest line 83) and survived the unknown-key filter, but was never copied into _baseConfig — silently dropped on a clean read. The milestone-prefix validation check could only be activated via the ROADMAP frontmatter fallback, not the documented project-config surface. Added phase_id_convention: get('phase_id_convention') ?? null to _baseConfig. 3 tests: survives resolution, null round-trips, absent resolves to null. * chore(#2997): backfill changeset PR number 3098 --------- Co-authored-by: sim --- .changeset/wise-jays-romp.md | 5 +++++ src/config-loader.cts | 1 + tests/config-loader.test.cjs | 41 ++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 .changeset/wise-jays-romp.md 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); } + }); +});