From 5fa34d438b1b68d47cb1b8531c0fbc24de96c6ff Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 5 Apr 2026 01:27:25 -0400 Subject: [PATCH] fix(gsd): promote milestone status from queued to active in plan-milestone (#3317) * fix(gsd): promote milestone status from queued to active in plan-milestone upsertMilestonePlanning() did not include title or status in its UPDATE statement. When a milestone row was pre-created by ensureMilestoneDbRow with status "queued", the INSERT OR IGNORE in insertMilestone() silently skipped the row, and upsertMilestonePlanning() never updated the status. This left the milestone permanently stuck as "queued", preventing proper state-machine phase transitions during milestone completion. Add title and status columns to the upsertMilestonePlanning() UPDATE statement and pass them from handlePlanMilestone(). Uses COALESCE with NULLIF to preserve existing values when empty strings are passed. Closes #3022 Co-Authored-By: Claude Opus 4.6 * fix(ts): remove extra title arg from upsertMilestonePlanning call * fix(test): move title into planning object for 2-param upsertMilestonePlanning --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: trek-e --- src/resources/extensions/gsd/gsd-db.ts | 8 +++--- .../gsd/tests/plan-milestone-title.test.ts | 3 ++- .../gsd/tests/plan-milestone.test.ts | 27 ++++++++++++++++++- .../extensions/gsd/tools/plan-milestone.ts | 4 ++- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/resources/extensions/gsd/gsd-db.ts b/src/resources/extensions/gsd/gsd-db.ts index 9a42646dd..fdb53492a 100644 --- a/src/resources/extensions/gsd/gsd-db.ts +++ b/src/resources/extensions/gsd/gsd-db.ts @@ -1136,11 +1136,12 @@ export function insertMilestone(m: { }); } -export function upsertMilestonePlanning(milestoneId: string, planning: Partial, title?: string): void { +export function upsertMilestonePlanning(milestoneId: string, planning: Partial & { title?: string; status?: string }): void { if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open"); currentDb.prepare( `UPDATE milestones SET - title = COALESCE(:title, title), + title = COALESCE(NULLIF(:title, ''), title), + status = COALESCE(NULLIF(:status, ''), status), vision = COALESCE(:vision, vision), success_criteria = COALESCE(:success_criteria, success_criteria), key_risks = COALESCE(:key_risks, key_risks), @@ -1155,7 +1156,8 @@ export function upsertMilestonePlanning(milestoneId: string, planning: Partial { + const base = makeTmpBase(); + const dbPath = join(base, '.gsd', 'gsd.db'); + openDatabase(dbPath); + + try { + // Simulate ensureMilestoneDbRow: pre-create row with status "queued" + // (this is what gsd_milestone_generate_id does) + insertMilestone({ id: 'M001', status: 'queued' }); + + const before = getMilestone('M001'); + assert.equal(before?.status, 'queued', 'pre-condition: milestone should start as queued'); + + // Now plan the milestone — status should be promoted to "active" + const result = await handlePlanMilestone(validParams(), base); + assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`); + + const after = getMilestone('M001'); + assert.equal(after?.status, 'active', 'milestone status should be promoted from queued to active'); + assert.equal(after?.title, 'DB-backed planning', 'milestone title should be set'); + } finally { + cleanup(base); + } +}); diff --git a/src/resources/extensions/gsd/tools/plan-milestone.ts b/src/resources/extensions/gsd/tools/plan-milestone.ts index 4cc39fe8b..51b3120af 100644 --- a/src/resources/extensions/gsd/tools/plan-milestone.ts +++ b/src/resources/extensions/gsd/tools/plan-milestone.ts @@ -241,6 +241,8 @@ export async function handlePlanMilestone( }); upsertMilestonePlanning(params.milestoneId, { + title: params.title, + status: params.status ?? "active", vision: params.vision, successCriteria: params.successCriteria, keyRisks: params.keyRisks, @@ -252,7 +254,7 @@ export async function handlePlanMilestone( definitionOfDone: params.definitionOfDone, requirementCoverage: params.requirementCoverage, boundaryMapMarkdown: params.boundaryMapMarkdown, - }, params.title); + }); for (const slice of params.slices) { // Preserve completed/done status on re-plan (#2558).