From 0908adcb4e6211a4e26bdbaabcd3107f4b687741 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 5 Apr 2026 00:49:29 -0400 Subject: [PATCH] fix: reopen DB on cold resume, recognize heavy check mark (#3319) * fix: reopen DB on cold resume and recognize U+2714 check mark The paused-session resume path in auto.ts called rebuildState/deriveState without first opening the project database, causing state derivation to fall back to markdown parsing. This misparsed roadmap table rows with glyph done markers and could redispatch wrong slices. Export openProjectDbIfPresent from auto-start.ts and call it in the resume path before rebuildState, matching the fresh bootstrap ordering. Also add U+2714 (heavy check mark) to the table parser done-detection regex alongside the existing U+2705/U+2611/U+2713 glyphs. Closes #2940 Co-Authored-By: Claude Opus 4.6 * fix: remove duplicate openProjectDbIfPresent from rebase conflict The rebase onto main introduced a duplicate `openProjectDbIfPresent` function declaration (one from the PR, one from main), causing TS2393. Keep the exported version that uses the static import. Co-Authored-By: Claude Opus 4.6 * fix: use logWarning instead of process.stderr in openProjectDbIfPresent --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: trek-e --- src/resources/extensions/gsd/auto-start.ts | 35 +++++-------- src/resources/extensions/gsd/auto.ts | 5 +- .../extensions/gsd/roadmap-slices.ts | 2 +- .../gsd/tests/cold-resume-db-reopen.test.ts | 51 +++++++++++++++++++ .../gsd/tests/roadmap-slices.test.ts | 13 +++++ 5 files changed, 82 insertions(+), 24 deletions(-) create mode 100644 src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts diff --git a/src/resources/extensions/gsd/auto-start.ts b/src/resources/extensions/gsd/auto-start.ts index 50f137c2b..8751d51c7 100644 --- a/src/resources/extensions/gsd/auto-start.ts +++ b/src/resources/extensions/gsd/auto-start.ts @@ -58,7 +58,7 @@ import { initRoutingHistory } from "./routing-history.js"; import { restoreHookState, resetHookState } from "./post-unit-hooks.js"; import { resetProactiveHealing, setLevelChangeCallback } from "./doctor-proactive.js"; import { snapshotSkills } from "./skill-discovery.js"; -import { isDbAvailable, getMilestone } from "./gsd-db.js"; +import { isDbAvailable, getMilestone, openDatabase } from "./gsd-db.js"; import { hideFooter } from "./auto-dashboard.js"; import { debugLog, @@ -99,33 +99,24 @@ export interface BootstrapDeps { * concurrent session detected). Returns true when ready to dispatch. */ -/** - * Open the project-root DB before the first deriveState call (#2841). - * When auto-mode starts cold (no prior DB handle), state derivation that - * touches DB-backed helpers (queue-order, task status) silently falls back - * to markdown-only data, producing stale or incomplete state. Opening the - * DB first ensures deriveState sees the full picture on its very first run. - */ -async function openProjectDbIfPresent(basePath: string): Promise { - const gsdDbPath = resolveProjectRootDbPath(basePath); - if (!existsSync(gsdDbPath)) return; - if (isDbAvailable()) return; - - try { - const { openDatabase } = await import("./gsd-db.js"); - openDatabase(gsdDbPath); - } catch (err) { - /* non-fatal — DB lifecycle block below will retry */ - logWarning("engine", `DB open failed: ${err instanceof Error ? err.message : String(err)}`); - } -} - /** Guard: tracks consecutive bootstrap attempts that found phase === "complete". * Prevents the recursive dialog loop described in #1348 where * bootstrapAutoSession → showSmartEntry → checkAutoStartAfterDiscuss → startAuto * cycles indefinitely when the discuss workflow doesn't produce a milestone. */ let _consecutiveCompleteBootstraps = 0; const MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS = 2; + +export async function openProjectDbIfPresent(basePath: string): Promise { + const gsdDbPath = resolveProjectRootDbPath(basePath); + if (!existsSync(gsdDbPath) || isDbAvailable()) return; + + try { + openDatabase(gsdDbPath); + } catch (err) { + logWarning("engine", `gsd-db: failed to open existing database: ${err instanceof Error ? err.message : String(err)}`); + } +} + export async function bootstrapAutoSession( s: AutoSession, ctx: ExtensionCommandContext, diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index 0f67fbb65..02d2ce36d 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -185,7 +185,7 @@ import { postUnitPreVerification, postUnitPostVerification, } from "./auto-post-unit.js"; -import { bootstrapAutoSession, type BootstrapDeps } from "./auto-start.js"; +import { bootstrapAutoSession, openProjectDbIfPresent, type BootstrapDeps } from "./auto-start.js"; import { autoLoop, resolveAgentEnd, resolveAgentEndCancelled, _resetPendingResolve, isSessionSwitchInFlight, type LoopDeps, type ErrorContext } from "./auto-loop.js"; import { WorktreeResolver, @@ -1205,6 +1205,9 @@ export async function startAuto( "info", ); restoreHookState(s.basePath); + // Open the project DB before rebuild/derive so resume uses DB-backed + // state instead of falling back to stale markdown parsing (#2940). + await openProjectDbIfPresent(s.basePath); try { await rebuildState(s.basePath); syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath)); diff --git a/src/resources/extensions/gsd/roadmap-slices.ts b/src/resources/extensions/gsd/roadmap-slices.ts index 93fb05038..58f79e361 100644 --- a/src/resources/extensions/gsd/roadmap-slices.ts +++ b/src/resources/extensions/gsd/roadmap-slices.ts @@ -82,7 +82,7 @@ function parseTableSlices(section: string): RoadmapSliceEntry[] { const fullRow = line.toLowerCase(); const done = /\[x\]/i.test(line) || - /[✅☑✓]/.test(line) || + /[✅☑✓✔]/.test(line) || /\bdone\b/.test(fullRow) || /\bcomplete(?:d)?\b/.test(fullRow); diff --git a/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts b/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts new file mode 100644 index 000000000..ae31af280 --- /dev/null +++ b/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts @@ -0,0 +1,51 @@ +/** + * cold-resume-db-reopen.test.ts — Regression test for #2940. + * + * Validates that the paused-session resume path in auto.ts opens the project + * database before calling rebuildState() / deriveState(), matching the fresh + * bootstrap path in auto-start.ts. + * + * Without this, cold resume falls back to markdown parsing which misreads + * done cells and redispatches wrong slices. + */ + +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { createTestContext } from "./test-helpers.ts"; + +const { assertTrue, report } = createTestContext(); + +const autoSrc = readFileSync(join(import.meta.dirname, "..", "auto.ts"), "utf-8"); + +console.log("\n=== #2940: resume path opens DB before rebuildState/deriveState ==="); + +// The resume block is the `if (s.paused) { ... }` section that calls rebuildState/deriveState. +// Locate the resume section by finding `s.paused = false;` followed by `rebuildState`. +const resumeSectionStart = autoSrc.indexOf("if (s.paused) {", autoSrc.indexOf("// If resuming from paused state")); +assertTrue(resumeSectionStart > 0, "auto.ts has the paused-session resume block"); + +const resumeSection = autoSrc.slice(resumeSectionStart, resumeSectionStart + 3000); + +// The resume path must open the DB before rebuildState/deriveState +const rebuildIdx = resumeSection.indexOf("rebuildState("); +assertTrue(rebuildIdx > 0, "resume block calls rebuildState"); + +const deriveIdx = resumeSection.indexOf("deriveState("); +assertTrue(deriveIdx > 0, "resume block calls deriveState"); + +// There must be a DB open call before the first rebuildState call +const dbOpenPatterns = [ + "openProjectDbIfPresent(", + "openDatabase(", + "ensureDbOpen(", +]; + +const preDeriveSection = resumeSection.slice(0, rebuildIdx); +const hasDbOpen = dbOpenPatterns.some(pat => preDeriveSection.includes(pat)); +assertTrue( + hasDbOpen, + "resume path must open DB before rebuildState/deriveState (#2940)", +); + +report(); diff --git a/src/resources/extensions/gsd/tests/roadmap-slices.test.ts b/src/resources/extensions/gsd/tests/roadmap-slices.test.ts index 56364a653..6d4f6ddbc 100644 --- a/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +++ b/src/resources/extensions/gsd/tests/roadmap-slices.test.ts @@ -133,6 +133,19 @@ test("parseRoadmapSlices: table with glyph completion markers (#2841)", () => { assert.equal(slices[3]?.done, true); }); +test("parseRoadmapSlices: table with heavy check mark U+2714 (#2940)", () => { + const tableContent = [ + "# M003: Heavy Check", "", "## Slices", "", + "| Slice | Title | Risk | Status |", "|---|---|---|---|", + "| S01 | First | Low | \u2714 |", + "| S02 | Second | High | Pending |", "", + ].join("\n"); + const slices = parseRoadmapSlices(tableContent); + assert.equal(slices.length, 2); + assert.equal(slices[0]?.done, true, "U+2714 heavy check mark should mark slice as done"); + assert.equal(slices[1]?.done, false); +}); + test("parseRoadmapSlices: table with dependencies column (#1736)", () => { const tableContent = [ "# M004: Deps", "", "## Slices", "",