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", "",