From 5f521e0867265924d63effce0ff63b4acb56802f Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 15 Apr 2026 16:46:10 -0400 Subject: [PATCH] fix(settings): route /gsd-settings reads/writes through workstream-aware config path (#2285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit settings.md was reading and writing .planning/config.json directly while gsd-tools config-get/config-set route to .planning/workstreams//config.json when GSD_WORKSTREAM is active, causing silent write-read drift (Closes #2282). - config.cjs: add cmdConfigPath() — emits the planningDir-resolved config path as plain text (always raw, no JSON wrapping) so shell substitution works correctly - gsd-tools.cjs: wire config-path subcommand - settings.md: resolve GSD_CONFIG_PATH via config-path in ensure_and_load_config; replace hardcoded cat .planning/config.json and Write to .planning/config.json with $GSD_CONFIG_PATH throughout - phase.cjs: fix renameDecimalPhases to preserve zero-padded prefix (06.3 → 06.2 not 6.2) — pre-existing test failure on main - tests/config.test.cjs: add config-path command tests (#2282) Co-authored-by: Claude Sonnet 4.6 --- get-shit-done/bin/gsd-tools.cjs | 5 ++++ get-shit-done/bin/lib/config.cjs | 12 +++++++++ get-shit-done/workflows/settings.md | 8 +++--- tests/config.test.cjs | 38 +++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index 90506e13..f2599c01 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -639,6 +639,11 @@ async function runCommand(command, args, cwd, raw, defaultValue) { break; } + case 'config-path': { + config.cmdConfigPath(cwd, raw); + break; + } + case 'agent-skills': { init.cmdAgentSkills(cwd, args[1], raw); break; diff --git a/get-shit-done/bin/lib/config.cjs b/get-shit-done/bin/lib/config.cjs index 3d532a7b..80b875f6 100644 --- a/get-shit-done/bin/lib/config.cjs +++ b/get-shit-done/bin/lib/config.cjs @@ -495,6 +495,17 @@ function getCmdConfigSetModelProfileResultMessage( return paragraphs.join('\n\n'); } +/** + * Print the resolved config.json path (workstream-aware). Used by settings.md + * so the workflow writes/reads the correct file when a workstream is active (#2282). + */ +function cmdConfigPath(cwd) { + // Always emit as plain text — a file path is used via shell substitution, + // never consumed as JSON. Passing raw=true forces plain-text output. + const configPath = path.join(planningDir(cwd), 'config.json'); + output(configPath, true, configPath); +} + module.exports = { VALID_CONFIG_KEYS, cmdConfigEnsureSection, @@ -502,4 +513,5 @@ module.exports = { cmdConfigGet, cmdConfigSetModelProfile, cmdConfigNewProject, + cmdConfigPath, }; diff --git a/get-shit-done/workflows/settings.md b/get-shit-done/workflows/settings.md index 503460de..aa429af6 100644 --- a/get-shit-done/workflows/settings.md +++ b/get-shit-done/workflows/settings.md @@ -13,16 +13,18 @@ Ensure config exists and load current state: ```bash node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-ensure-section +GSD_CONFIG_PATH=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-path) INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load) if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi ``` -Creates `.planning/config.json` with defaults if missing and loads current config values. +Creates config.json (at the workstream-aware path) with defaults if missing and loads current config values. +Store `$GSD_CONFIG_PATH` — all subsequent reads and writes use this path, not the hardcoded `.planning/config.json`, so active-workstream installs write to the correct location (#2282). ```bash -cat .planning/config.json +cat "$GSD_CONFIG_PATH" ``` Parse current values (default to `true` if not present): @@ -213,7 +215,7 @@ Merge new settings into existing config.json: } ``` -Write updated config to `.planning/config.json`. +Write updated config to `$GSD_CONFIG_PATH` (the workstream-aware path resolved in `ensure_and_load_config`). Never hardcode `.planning/config.json` — workstream installs route to `.planning/workstreams//config.json`. diff --git a/tests/config.test.cjs b/tests/config.test.cjs index 8651eedd..5bee2988 100644 --- a/tests/config.test.cjs +++ b/tests/config.test.cjs @@ -935,3 +935,41 @@ describe('config-set/config-get context', () => { assert.ok(fs.existsSync(path.join(contextsDir, 'review.md')), 'review.md should exist'); }); }); + +// ─── config-path (#2282) ──────────────────────────────────────────────────── + +describe('config-path command (#2282)', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + runGsdTools('config-ensure-section', tmpDir); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('returns root config path when no workstream is active', () => { + const result = runGsdTools('config-path', tmpDir); + assert.ok(result.success, `config-path failed: ${result.error}`); + assert.ok(result.output.trim().endsWith('.planning/config.json'), `expected root config path, got: ${result.output}`); + assert.ok(!result.output.includes('workstreams'), 'should not include workstreams in path'); + }); + + test('returns workstream config path when GSD_WORKSTREAM is set', () => { + const result = runGsdTools('config-path', tmpDir, { GSD_WORKSTREAM: 'my-stream' }); + assert.ok(result.success, `config-path failed: ${result.error}`); + assert.ok(result.output.trim().includes('workstreams/my-stream/config.json'), `expected workstream config path, got: ${result.output}`); + }); + + test('config-path and config-get agree on the active path', () => { + // Write a value via config-set (uses planningDir internally) + runGsdTools('config-set model_profile quality', tmpDir); + // config-path should point to a file containing that value + const pathResult = runGsdTools('config-path', tmpDir); + const configPath = pathResult.output.trim(); + const configContent = JSON.parse(require('fs').readFileSync(configPath, 'utf-8')); + assert.strictEqual(configContent.model_profile, 'quality', 'config-path should point to the file config-set wrote'); + }); +});