Merge pull request #3904 from jeremymcs/fix/init-bootstrap-completeness

fix(gsd): create gsd.db, runtime/, and STATE.md during init
This commit is contained in:
Jeremy McSpadden 2026-04-09 17:53:44 -05:00 committed by GitHub
commit 35568d0543
2 changed files with 155 additions and 0 deletions

View file

@ -235,6 +235,20 @@ export async function showProjectInit(
// ── Step 9: Bootstrap .gsd/ ────────────────────────────────────────────────
bootstrapGsdDirectory(basePath, prefs, signals);
// Initialize SQLite database so GSD starts in full-capability mode (#3880).
// Without this, isDbAvailable() returns false and GSD enters degraded
// markdown-only mode until a tool handler happens to call ensureDbOpen().
let dbReady = false;
try {
const { ensureDbOpen } = await import("./bootstrap/dynamic-tools.js");
dbReady = await ensureDbOpen(basePath);
} catch {
// Swallowed — warning surfaced below
}
if (!dbReady) {
ctx.ui.notify("Warning: database initialization failed — GSD will run in degraded mode until the next /gsd invocation.", "warning");
}
// Ensure .gitignore
ensureGitignore(basePath);
untrackRuntimeFiles(basePath);
@ -250,6 +264,25 @@ export async function showProjectInit(
// Non-fatal — codebase map generation failure should never block project init
}
// Write initial STATE.md so it exists before the first /gsd invocation.
// The explicit /gsd init path (ops.ts) returns without entering showSmartEntry(),
// which would otherwise generate STATE.md at guided-flow.ts:1358.
let stateReady = false;
try {
const { deriveState } = await import("./state.js");
const { buildStateMarkdown } = await import("./doctor.js");
const { saveFile } = await import("./files.js");
const { resolveGsdRootFile } = await import("./paths.js");
const state = await deriveState(basePath);
await saveFile(resolveGsdRootFile(basePath, "STATE"), buildStateMarkdown(state));
stateReady = true;
} catch {
// Swallowed — warning surfaced below
}
if (!stateReady) {
ctx.ui.notify("Warning: initial STATE.md generation failed — it will be created on the next /gsd invocation.", "warning");
}
ctx.ui.notify("GSD initialized. Starting your first milestone...", "info");
return { completed: true, bootstrapped: true };
@ -433,6 +466,7 @@ function bootstrapGsdDirectory(
const gsd = gsdRoot(basePath);
mkdirSync(join(gsd, "milestones"), { recursive: true });
mkdirSync(join(gsd, "runtime"), { recursive: true });
// Write PREFERENCES.md from wizard answers
const preferencesContent = buildPreferencesFile(prefs);

View file

@ -0,0 +1,121 @@
/**
* GSD Init Wizard Bootstrap completeness regression tests
*
* Regression test for #3880 fresh install never creates gsd.db.
*
* The init wizard must create all artifacts needed for full-capability
* mode: gsd.db (via ensureDbOpen), runtime/ directory, and STATE.md
* (via deriveState + buildStateMarkdown). Without these, GSD enters
* degraded markdown-only mode on every fresh install.
*
* These are structural tests that verify the init-wizard.ts source
* contains the required calls in the correct order.
*/
import { describe, test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const wizardSrc = readFileSync(
join(__dirname, "..", "init-wizard.ts"),
"utf-8",
);
describe("init-wizard bootstrap completeness (#3880)", () => {
// ── Gap 1: gsd.db must be created during init ─────────────────────────
test("bootstrapGsdDirectory is followed by ensureDbOpen", () => {
const bootstrapIdx = wizardSrc.indexOf("bootstrapGsdDirectory(basePath");
const ensureDbIdx = wizardSrc.indexOf("ensureDbOpen(basePath)");
assert.ok(bootstrapIdx > -1, "bootstrapGsdDirectory call should exist");
assert.ok(ensureDbIdx > -1, "ensureDbOpen(basePath) call should exist");
assert.ok(
ensureDbIdx > bootstrapIdx,
"ensureDbOpen must appear after bootstrapGsdDirectory so .gsd/ exists first",
);
});
test("ensureDbOpen is imported from dynamic-tools", () => {
assert.match(
wizardSrc,
/import.*dynamic-tools/,
"init-wizard should import from dynamic-tools for ensureDbOpen",
);
});
// ── Gap 2: runtime/ directory must be created during init ──────────────
test("bootstrapGsdDirectory creates runtime/ directory", () => {
// Find the bootstrapGsdDirectory function body
const fnStart = wizardSrc.indexOf("function bootstrapGsdDirectory(");
assert.ok(fnStart > -1, "bootstrapGsdDirectory function should exist");
// Find the next function definition to bound the search
const fnBody = wizardSrc.slice(fnStart, wizardSrc.indexOf("\nfunction ", fnStart + 1));
assert.match(
fnBody,
/mkdirSync\(.*"runtime"/,
'bootstrapGsdDirectory should create "runtime" directory',
);
});
// ── Gap 3: STATE.md must be written during init ────────────────────────
test("showProjectInit generates STATE.md after bootstrap", () => {
const bootstrapIdx = wizardSrc.indexOf("bootstrapGsdDirectory(basePath");
const deriveIdx = wizardSrc.indexOf("deriveState(basePath)");
const stateIdx = wizardSrc.indexOf("buildStateMarkdown(state)");
const saveIdx = wizardSrc.indexOf('resolveGsdRootFile(basePath, "STATE")');
assert.ok(deriveIdx > -1, "deriveState call should exist in init-wizard");
assert.ok(stateIdx > -1, "buildStateMarkdown call should exist in init-wizard");
assert.ok(saveIdx > -1, "resolveGsdRootFile STATE call should exist in init-wizard");
assert.ok(
deriveIdx > bootstrapIdx,
"deriveState must appear after bootstrapGsdDirectory",
);
});
// ── Ordering: DB must be open before deriveState ───────────────────────
test("ensureDbOpen appears before deriveState", () => {
const ensureDbIdx = wizardSrc.indexOf("ensureDbOpen(basePath)");
const deriveIdx = wizardSrc.indexOf("deriveState(basePath)");
assert.ok(ensureDbIdx > -1, "ensureDbOpen should exist");
assert.ok(deriveIdx > -1, "deriveState should exist");
assert.ok(
ensureDbIdx < deriveIdx,
"ensureDbOpen must appear before deriveState so DB is ready for state derivation",
);
});
// ── Failure visibility: user must be warned on partial bootstrap ───────
test("ensureDbOpen failure surfaces a warning to the user", () => {
assert.match(
wizardSrc,
/if\s*\(\s*!dbReady\s*\)/,
"init-wizard should check dbReady and warn the user on failure",
);
// The warning must reference degraded mode so the user knows what happened
assert.match(
wizardSrc,
/degraded mode/,
"DB failure warning should mention degraded mode",
);
});
test("STATE.md failure surfaces a warning to the user", () => {
assert.match(
wizardSrc,
/if\s*\(\s*!stateReady\s*\)/,
"init-wizard should check stateReady and warn the user on failure",
);
});
});