From afb93a37c63bdf9501c8b48b5cc2e832efc73783 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Sun, 15 Feb 2026 11:14:17 -0600 Subject: [PATCH] fix(tools): normalize phase padding in insert command (closes #494) Phase insert failed when zero-padding differed between user input and ROADMAP.md headers (e.g. "9.05" vs "09.05"). Normalize input and use flexible regex matching with optional leading zeros. Co-Authored-By: Claude Opus 4.6 --- get-shit-done/bin/gsd-tools.cjs | 30 ++++++++++++++++++++++------ get-shit-done/bin/gsd-tools.test.cjs | 25 +++++++++++++++++++++++ 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index 08e893779..c9002259f 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -594,8 +594,19 @@ function cmdConfigEnsureSection(cwd, raw) { const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key'); const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile)); - // Create default config - const defaults = { + // Load user-level defaults from ~/.gsd/defaults.json if available + const globalDefaultsPath = path.join(homedir, '.gsd', 'defaults.json'); + let userDefaults = {}; + try { + if (fs.existsSync(globalDefaultsPath)) { + userDefaults = JSON.parse(fs.readFileSync(globalDefaultsPath, 'utf-8')); + } + } catch (err) { + // Ignore malformed global defaults, fall back to hardcoded + } + + // Create default config (user-level defaults override hardcoded defaults) + const hardcoded = { model_profile: 'balanced', commit_docs: true, search_gitignored: false, @@ -610,6 +621,11 @@ function cmdConfigEnsureSection(cwd, raw) { parallelization: true, brave_search: hasBraveSearch, }; + const defaults = { + ...hardcoded, + ...userDefaults, + workflow: { ...hardcoded.workflow, ...(userDefaults.workflow || {}) }, + }; try { fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2), 'utf-8'); @@ -2646,9 +2662,11 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) { const content = fs.readFileSync(roadmapPath, 'utf-8'); const slug = generateSlugInternal(description); - // Verify target phase exists - const afterPhaseEscaped = afterPhase.replace(/\./g, '\\.'); - const targetPattern = new RegExp(`#{2,3}\\s*Phase\\s+${afterPhaseEscaped}:`, 'i'); + // Normalize input then strip leading zeros for flexible matching + const normalizedAfter = normalizePhaseName(afterPhase); + const unpadded = normalizedAfter.replace(/^0+/, ''); + const afterPhaseEscaped = unpadded.replace(/\./g, '\\.'); + const targetPattern = new RegExp(`#{2,3}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i'); if (!targetPattern.test(content)) { error(`Phase ${afterPhase} not found in ROADMAP.md`); } @@ -2680,7 +2698,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) { const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${decimalPhase} to break down)\n`; // Insert after the target phase section - const headerPattern = new RegExp(`(#{2,3}\\s*Phase\\s+${afterPhaseEscaped}:[^\\n]*\\n)`, 'i'); + const headerPattern = new RegExp(`(#{2,3}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i'); const headerMatch = content.match(headerPattern); if (!headerMatch) { error(`Could not find Phase ${afterPhase} header`); diff --git a/get-shit-done/bin/gsd-tools.test.cjs b/get-shit-done/bin/gsd-tools.test.cjs index 8e9cf5624..462b9dbc9 100644 --- a/get-shit-done/bin/gsd-tools.test.cjs +++ b/get-shit-done/bin/gsd-tools.test.cjs @@ -1468,6 +1468,31 @@ describe('phase insert command', () => { assert.ok(!result.success, 'should fail for missing phase'); assert.ok(result.error.includes('not found'), 'error mentions not found'); }); + + test('handles padding mismatch between input and roadmap', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +## Phase 09.05: Existing Decimal Phase +**Goal:** Test padding + +## Phase 09.1: Next Phase +**Goal:** Test +` + ); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '09.05-existing'), { recursive: true }); + + // Pass unpadded "9.05" but roadmap has "09.05" + const result = runGsdTools('phase insert 9.05 Padding Test', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.after_phase, '9.05'); + + const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + assert.ok(roadmap.includes('(INSERTED)'), 'roadmap should include inserted phase'); + }); }); // ─────────────────────────────────────────────────────────────────────────────