From 09de02adeebd6b5aed80a19631e34a11f2265e18 Mon Sep 17 00:00:00 2001 From: mastertyko <11311479+mastertyko@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:19:24 +0100 Subject: [PATCH] fix(gsd): resume cold auto bootstrap from db Open the project database before the first auto bootstrap derive so cold-start resume uses DB-backed slice state instead of stale markdown fallback state. Also recognize glyph completion markers in roadmap tables and lock the new bootstrap ordering with regression coverage. Closes #2841 --- src/resources/extensions/gsd/auto-start.ts | 29 ++++++++++++--- .../extensions/gsd/roadmap-slices.ts | 1 + .../auto-start-cold-db-bootstrap.test.ts | 37 +++++++++++++++++++ .../gsd/tests/roadmap-slices.test.ts | 17 +++++++++ .../gsd/tests/sqlite-unavailable-gate.test.ts | 30 ++++++++------- 5 files changed, 94 insertions(+), 20 deletions(-) create mode 100644 src/resources/extensions/gsd/tests/auto-start-cold-db-bootstrap.test.ts diff --git a/src/resources/extensions/gsd/auto-start.ts b/src/resources/extensions/gsd/auto-start.ts index 38012e2c2..85bdbe370 100644 --- a/src/resources/extensions/gsd/auto-start.ts +++ b/src/resources/extensions/gsd/auto-start.ts @@ -58,8 +58,9 @@ 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 { resolveProjectRootDbPath } from "./bootstrap/dynamic-tools.js"; import { debugLog, enableDebug, @@ -103,6 +104,20 @@ export interface BootstrapDeps { * cycles indefinitely when the discuss workflow doesn't produce a milestone. */ let _consecutiveCompleteBootstraps = 0; const MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS = 2; + +async function openProjectDbIfPresent(basePath: string): Promise { + const gsdDbPath = resolveProjectRootDbPath(basePath); + if (!existsSync(gsdDbPath) || isDbAvailable()) return; + + try { + openDatabase(gsdDbPath); + } catch (err) { + process.stderr.write( + `gsd-db: failed to open existing database: ${(err as Error).message}\n`, + ); + } +} + export async function bootstrapAutoSession( s: AutoSession, ctx: ExtensionCommandContext, @@ -265,6 +280,10 @@ export async function bootstrapAutoSession( ctx.ui.notify(`Debug logging enabled → ${getDebugLogPath()}`, "info"); } + // Open the project DB before the first derive so resume uses DB truth + // immediately on cold starts instead of falling back to markdown (#2841). + await openProjectDbIfPresent(base); + // Invalidate caches before initial state derivation invalidateAllCaches(); @@ -535,15 +554,14 @@ export async function bootstrapAutoSession( } // ── DB lifecycle ── - const gsdDbPath = join(s.basePath, ".gsd", "gsd.db"); + const gsdDbPath = resolveProjectRootDbPath(s.basePath); const gsdDirPath = join(s.basePath, ".gsd"); if (existsSync(gsdDirPath) && !existsSync(gsdDbPath)) { const hasDecisions = existsSync(join(gsdDirPath, "DECISIONS.md")); const hasRequirements = existsSync(join(gsdDirPath, "REQUIREMENTS.md")); const hasMilestones = existsSync(join(gsdDirPath, "milestones")); try { - const { openDatabase: openDb } = await import("./gsd-db.js"); - openDb(gsdDbPath); + openDatabase(gsdDbPath); if (hasDecisions || hasRequirements || hasMilestones) { const { migrateFromMarkdown } = await import("./md-importer.js"); migrateFromMarkdown(s.basePath); @@ -556,8 +574,7 @@ export async function bootstrapAutoSession( } if (existsSync(gsdDbPath) && !isDbAvailable()) { try { - const { openDatabase: openDb } = await import("./gsd-db.js"); - openDb(gsdDbPath); + openDatabase(gsdDbPath); } catch (err) { process.stderr.write( `gsd-db: failed to open existing database: ${(err as Error).message}\n`, diff --git a/src/resources/extensions/gsd/roadmap-slices.ts b/src/resources/extensions/gsd/roadmap-slices.ts index c5487ed80..5031f004f 100644 --- a/src/resources/extensions/gsd/roadmap-slices.ts +++ b/src/resources/extensions/gsd/roadmap-slices.ts @@ -82,6 +82,7 @@ function parseTableSlices(section: string): RoadmapSliceEntry[] { const fullRow = line.toLowerCase(); const done = /\[x\]/i.test(line) || + /[✅☑✓]/.test(line) || /\bdone\b/.test(fullRow) || /\bcomplete(?:d)?\b/.test(fullRow); diff --git a/src/resources/extensions/gsd/tests/auto-start-cold-db-bootstrap.test.ts b/src/resources/extensions/gsd/tests/auto-start-cold-db-bootstrap.test.ts new file mode 100644 index 000000000..c43636baa --- /dev/null +++ b/src/resources/extensions/gsd/tests/auto-start-cold-db-bootstrap.test.ts @@ -0,0 +1,37 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { createTestContext } from "./test-helpers.ts"; + +const { assertTrue, report } = createTestContext(); + +const srcPath = join(import.meta.dirname, "..", "auto-start.ts"); +const src = readFileSync(srcPath, "utf-8"); + +console.log("\n=== #2841: cold DB opened before initial deriveState ==="); + +const helperIdx = src.indexOf("async function openProjectDbIfPresent"); +assertTrue(helperIdx >= 0, "auto-start.ts defines a helper for pre-derive DB open (#2841)"); + +const helperRegion = helperIdx >= 0 ? src.slice(helperIdx, helperIdx + 500) : ""; +assertTrue( + helperRegion.includes("resolveProjectRootDbPath(basePath)"), + "pre-derive DB helper resolves the project-root DB path (#2841)", +); +assertTrue( + helperRegion.includes("openDatabase(gsdDbPath)"), + "pre-derive DB helper opens the resolved DB path (#2841)", +); + +const firstDeriveIdx = src.indexOf("let state = await deriveState(base);"); +assertTrue(firstDeriveIdx > 0, "auto-start.ts has the initial deriveState(base) call"); + +const preDeriveRegion = firstDeriveIdx > 0 ? src.slice(0, firstDeriveIdx) : ""; +const preDeriveOpenIdx = preDeriveRegion.lastIndexOf("await openProjectDbIfPresent(base);"); + +assertTrue( + preDeriveOpenIdx > 0, + "bootstrapAutoSession opens the DB before the first deriveState(base) call (#2841)", +); + +report(); diff --git a/src/resources/extensions/gsd/tests/roadmap-slices.test.ts b/src/resources/extensions/gsd/tests/roadmap-slices.test.ts index 182c72732..63f607683 100644 --- a/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +++ b/src/resources/extensions/gsd/tests/roadmap-slices.test.ts @@ -116,6 +116,23 @@ test("parseRoadmapSlices: table with Status Done/Complete text (#1736)", () => { assert.equal(slices[2]?.done, true); }); +test("parseRoadmapSlices: table with glyph completion markers (#2841)", () => { + const tableContent = [ + "# M003: Glyph Status", "", "## Slices", "", + "| Slice | Title | Risk | Status |", "|---|---|---|---|", + "| S01 | First | Low | ✅ |", + "| S02 | Second | High | Pending |", + "| S03 | Third | Medium | ☑ |", + "| S04 | Fourth | Medium | ✓ |", "", + ].join("\n"); + const slices = parseRoadmapSlices(tableContent); + assert.equal(slices.length, 4); + assert.equal(slices[0]?.done, true); + assert.equal(slices[1]?.done, false); + assert.equal(slices[2]?.done, true); + assert.equal(slices[3]?.done, true); +}); + test("parseRoadmapSlices: table with dependencies column (#1736)", () => { const tableContent = [ "# M004: Deps", "", "## Slices", "", diff --git a/src/resources/extensions/gsd/tests/sqlite-unavailable-gate.test.ts b/src/resources/extensions/gsd/tests/sqlite-unavailable-gate.test.ts index 8e1de821e..62d89b0d3 100644 --- a/src/resources/extensions/gsd/tests/sqlite-unavailable-gate.test.ts +++ b/src/resources/extensions/gsd/tests/sqlite-unavailable-gate.test.ts @@ -33,23 +33,25 @@ assertTrue(dbLifecycleIdx > 0, "auto-start.ts has a DB lifecycle section"); const afterDbLifecycle = src.slice(dbLifecycleIdx); -// Find the second isDbAvailable check — the one AFTER the open attempts. -// The first check at line ~543 tries to open the DB. -// There must be a SECOND check that gates bootstrap if it's still unavailable. -const firstCheck = afterDbLifecycle.indexOf("isDbAvailable()"); -assertTrue(firstCheck > 0, "DB lifecycle section has isDbAvailable() check"); - -const afterFirstCheck = afterDbLifecycle.slice(firstCheck + "isDbAvailable()".length); -const secondCheck = afterFirstCheck.indexOf("isDbAvailable()"); - +// The DB lifecycle section may contain multiple isDbAvailable() checks now that +// cold-start bootstrap can pre-open the DB earlier in the file. What matters +// for #2419 is the explicit abort gate after the DB open attempts. assertTrue( - secondCheck > 0, - "auto-start.ts has a SECOND isDbAvailable() check after the open attempt — this is the gate (#2419)", + afterDbLifecycle.includes("!isDbAvailable()"), + "DB lifecycle section still checks for unavailable DB state (#2419)", ); -// The second check must lead to releaseLockAndReturn (abort bootstrap) -if (secondCheck > 0) { - const gateRegion = afterFirstCheck.slice(secondCheck, secondCheck + 500); +const gateMatch = afterDbLifecycle.match( + /if\s*\(existsSync\(gsdDbPath\)\s*&&\s*!isDbAvailable\(\)\)\s*\{[\s\S]*?releaseLockAndReturn\(\);[\s\S]*?\}/, +); + +assertTrue( + !!gateMatch, + "auto-start.ts has a hard abort gate when gsd.db exists but SQLite is still unavailable (#2419)", +); + +if (gateMatch) { + const gateRegion = gateMatch[0]; assertTrue( gateRegion.includes("releaseLockAndReturn"), "The DB availability gate calls releaseLockAndReturn() to abort bootstrap (#2419)",