diff --git a/src/resources/extensions/gsd/milestone-actions.ts b/src/resources/extensions/gsd/milestone-actions.ts index 49102dc25..06562a893 100644 --- a/src/resources/extensions/gsd/milestone-actions.ts +++ b/src/resources/extensions/gsd/milestone-actions.ts @@ -20,7 +20,7 @@ import { } from "./paths.js"; import { invalidateAllCaches } from "./cache.js"; import { loadQueueOrder, saveQueueOrder } from "./queue-order.js"; -import { isDbAvailable, updateMilestoneStatus } from "./gsd-db.js"; +import { getMilestone, isDbAvailable, updateMilestoneStatus } from "./gsd-db.js"; import { logWarning } from "./workflow-logger.js"; // ─── Park ────────────────────────────────────────────────────────────────── @@ -77,9 +77,16 @@ export function unparkMilestone(basePath: string, milestoneId: string): boolean if (!mDir || !existsSync(mDir)) return false; const parkedPath = join(mDir, buildMilestoneFileName(milestoneId, "PARKED")); - if (!existsSync(parkedPath)) return false; // not parked + const hadParkedFile = existsSync(parkedPath); + const dbThinksParked = isDbAvailable() && getMilestone(milestoneId)?.status === "parked"; - unlinkSync(parkedPath); + // Recover the reverse desync too: DB can still say "parked" even when the + // PARKED marker was lost on disk, and /gsd unpark should repair that state. + if (!hadParkedFile && !dbThinksParked) return false; + + if (hadParkedFile) { + unlinkSync(parkedPath); + } // Sync DB status so deriveStateFromDb picks up the unparked milestone (#2694) if (isDbAvailable()) { try { diff --git a/src/resources/extensions/gsd/tests/park-db-sync.test.ts b/src/resources/extensions/gsd/tests/park-db-sync.test.ts index 0580337e2..684f7904d 100644 --- a/src/resources/extensions/gsd/tests/park-db-sync.test.ts +++ b/src/resources/extensions/gsd/tests/park-db-sync.test.ts @@ -69,6 +69,24 @@ test("unparkMilestone updates DB status to 'active' (#2694)", () => { } }); +test("unparkMilestone repairs parked DB state when PARKED.md is missing (#3707)", () => { + const base = createBase(); + try { + openDatabase(":memory:"); + insertMilestone({ id: "M001", title: "Test", status: "parked" }); + + const unparked = unparkMilestone(base, "M001"); + + assert.ok(unparked, "unparkMilestone should recover DB-only parked state"); + assert.equal(getMilestone("M001")!.status, "active", "DB status should be repaired to active"); + + closeDatabase(); + } finally { + closeDatabase(); + rmSync(base, { recursive: true, force: true }); + } +}); + test("park/unpark are safe when DB is not available (#2694 guard)", () => { const base = createBase(); try {