From 2dcf374da0a56731e7d4b931f51e83c3fb30501c Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 4 May 2026 23:17:00 -0400 Subject: [PATCH 1/2] fix(milestone): normalize STATE narrative after milestone completion --- get-shit-done/bin/lib/milestone.cjs | 20 +++++++++++++++++++- sdk/src/query/phase-lifecycle.ts | 16 ++++++++++++++++ tests/milestone.test.cjs | 20 ++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/get-shit-done/bin/lib/milestone.cjs b/get-shit-done/bin/lib/milestone.cjs index 23c8dd4ba..82cfd85d8 100644 --- a/get-shit-done/bin/lib/milestone.cjs +++ b/get-shit-done/bin/lib/milestone.cjs @@ -196,7 +196,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) { atomicWriteFileSync(milestonesPath, normalizeMd(`# Milestones\n\n${milestoneEntry}`)); } - // Update STATE.md — use shared helpers that handle both **bold:** and plain Field: formats + // Update STATE.md — keep frontmatter/body semantically aligned after closure if (fs.existsSync(statePath)) { let stateContent = fs.readFileSync(statePath, 'utf-8'); @@ -205,6 +205,24 @@ function cmdMilestoneComplete(cwd, version, options, raw) { stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null, `${version} milestone completed and archived`); + // Reset Current Position narrative so resume/progress flows do not keep + // pointing at closed-phase execution instructions. + const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i; + const closedPositionBody = + `\nPhase: Milestone ${version} complete\n` + + `Plan: —\n` + + `Status: Awaiting next milestone\n` + + `Last activity: ${today} — Milestone ${version} completed and archived\n\n`; + if (positionPattern.test(stateContent)) { + stateContent = stateContent.replace(positionPattern, (_m, header) => `${header}${closedPositionBody}`); + } + + // Normalize operator-next-step tails that can become stale after close. + stateContent = stateContent.replace( + /(##\s*Operator Next Steps\s*\n)([\s\S]*?)(?=\n##|$)/i, + `$1\n- Start the next milestone with /gsd-new-milestone\n\n`, + ); + writeStateMd(statePath, stateContent, cwd); } diff --git a/sdk/src/query/phase-lifecycle.ts b/sdk/src/query/phase-lifecycle.ts index 306e787c2..c11afb924 100644 --- a/sdk/src/query/phase-lifecycle.ts +++ b/sdk/src/query/phase-lifecycle.ts @@ -1841,6 +1841,22 @@ export const milestoneComplete: QueryHandler = async (args, projectDir, workstre null, `${version} milestone completed and archived`, ); + + const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i; + const closedPositionBody = + `\nPhase: Milestone ${version} complete\n` + + `Plan: —\n` + + `Status: Awaiting next milestone\n` + + `Last activity: ${today} — Milestone ${version} completed and archived\n\n`; + if (positionPattern.test(next)) { + next = next.replace(positionPattern, (_m, header) => `${header}${closedPositionBody}`); + } + + next = next.replace( + /(##\s*Operator Next Steps\s*\n)([\s\S]*?)(?=\n##|$)/i, + `$1\n- Start the next milestone with /gsd-new-milestone\n\n`, + ); + return next; }, workstream); } diff --git a/tests/milestone.test.cjs b/tests/milestone.test.cjs index b3d9b9e32..5dd4411b7 100644 --- a/tests/milestone.test.cjs +++ b/tests/milestone.test.cjs @@ -227,6 +227,26 @@ describe('milestone complete command', () => { ); }); + test('normalizes stale STATE.md narrative tails after milestone complete (#3088)', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0\n` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n\n## Current Position\n\nPhase: 03 — EXECUTING\nPlan: 03-02\nStatus: Executing\nLast activity: 2025-01-01 — Running phase\n\n## Operator Next Steps\n\n- Re-run /gsd-complete-milestone v1.0\n` + ); + + const result = runGsdTools('milestone complete v1.0 --name Test', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(state.includes('Phase: Milestone v1.0 complete')); + assert.ok(state.includes('Status: Awaiting next milestone')); + assert.ok(!state.includes('Re-run /gsd-complete-milestone')); + assert.ok(state.includes('/gsd-new-milestone')); + }); + test('handles missing ROADMAP.md gracefully', () => { // Only STATE.md — no ROADMAP.md, no REQUIREMENTS.md fs.writeFileSync( From 67684626d84b3679e8ca96b2f0b452709c9faae5 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 4 May 2026 23:29:45 -0400 Subject: [PATCH 2/2] fix(3088): append missing STATE narrative sections on milestone close --- ...ix-3088-milestone-state-fallback-sections.md | 5 +++++ get-shit-done/bin/lib/milestone.cjs | 15 +++++++++++---- sdk/src/query/phase-lifecycle.ts | 15 +++++++++++---- tests/milestone.test.cjs | 17 +++++++++++++++++ 4 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 .changeset/fix-3088-milestone-state-fallback-sections.md diff --git a/.changeset/fix-3088-milestone-state-fallback-sections.md b/.changeset/fix-3088-milestone-state-fallback-sections.md new file mode 100644 index 000000000..4c4966acb --- /dev/null +++ b/.changeset/fix-3088-milestone-state-fallback-sections.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3122 +--- +**Milestone close now repairs missing STATE narrative sections** — when `## Current Position` or `## Operator Next Steps` headings are absent, milestone completion appends canonical sections so state remains deterministic and consistently points operators to `/gsd-new-milestone`. \ No newline at end of file diff --git a/get-shit-done/bin/lib/milestone.cjs b/get-shit-done/bin/lib/milestone.cjs index 82cfd85d8..a7daa36a7 100644 --- a/get-shit-done/bin/lib/milestone.cjs +++ b/get-shit-done/bin/lib/milestone.cjs @@ -215,13 +215,20 @@ function cmdMilestoneComplete(cwd, version, options, raw) { `Last activity: ${today} — Milestone ${version} completed and archived\n\n`; if (positionPattern.test(stateContent)) { stateContent = stateContent.replace(positionPattern, (_m, header) => `${header}${closedPositionBody}`); + } else { + stateContent = `${stateContent.trimEnd()}\n\n## Current Position\n${closedPositionBody}`; } // Normalize operator-next-step tails that can become stale after close. - stateContent = stateContent.replace( - /(##\s*Operator Next Steps\s*\n)([\s\S]*?)(?=\n##|$)/i, - `$1\n- Start the next milestone with /gsd-new-milestone\n\n`, - ); + const operatorPattern = /(##\s*Operator Next Steps\s*\n)([\s\S]*?)(?=\n##|$)/i; + if (operatorPattern.test(stateContent)) { + stateContent = stateContent.replace( + operatorPattern, + `$1\n- Start the next milestone with /gsd-new-milestone\n\n`, + ); + } else { + stateContent = `${stateContent.trimEnd()}\n\n## Operator Next Steps\n\n- Start the next milestone with /gsd-new-milestone\n`; + } writeStateMd(statePath, stateContent, cwd); } diff --git a/sdk/src/query/phase-lifecycle.ts b/sdk/src/query/phase-lifecycle.ts index c11afb924..a9a3e5c5c 100644 --- a/sdk/src/query/phase-lifecycle.ts +++ b/sdk/src/query/phase-lifecycle.ts @@ -1850,12 +1850,19 @@ export const milestoneComplete: QueryHandler = async (args, projectDir, workstre `Last activity: ${today} — Milestone ${version} completed and archived\n\n`; if (positionPattern.test(next)) { next = next.replace(positionPattern, (_m, header) => `${header}${closedPositionBody}`); + } else { + next = `${next.trimEnd()}\n\n## Current Position\n${closedPositionBody}`; } - next = next.replace( - /(##\s*Operator Next Steps\s*\n)([\s\S]*?)(?=\n##|$)/i, - `$1\n- Start the next milestone with /gsd-new-milestone\n\n`, - ); + const operatorPattern = /(##\s*Operator Next Steps\s*\n)([\s\S]*?)(?=\n##|$)/i; + if (operatorPattern.test(next)) { + next = next.replace( + operatorPattern, + `$1\n- Start the next milestone with /gsd-new-milestone\n\n`, + ); + } else { + next = `${next.trimEnd()}\n\n## Operator Next Steps\n\n- Start the next milestone with /gsd-new-milestone\n`; + } return next; }, workstream); diff --git a/tests/milestone.test.cjs b/tests/milestone.test.cjs index 5dd4411b7..79c0dba93 100644 --- a/tests/milestone.test.cjs +++ b/tests/milestone.test.cjs @@ -247,6 +247,23 @@ describe('milestone complete command', () => { assert.ok(state.includes('/gsd-new-milestone')); }); + test('appends canonical narrative sections when STATE.md headings are missing (#3088)', () => { + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), `# Roadmap v1.0\n`); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` + ); + + const result = runGsdTools('milestone complete v1.0 --name Test', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(state.includes('## Current Position')); + assert.ok(state.includes('Phase: Milestone v1.0 complete')); + assert.ok(state.includes('## Operator Next Steps')); + assert.ok(state.includes('/gsd-new-milestone')); + }); + test('handles missing ROADMAP.md gracefully', () => { // Only STATE.md — no ROADMAP.md, no REQUIREMENTS.md fs.writeFileSync(