diff --git a/bin/gstack-config b/bin/gstack-config index 70bcf27ce..c92befed8 100755 --- a/bin/gstack-config +++ b/bin/gstack-config @@ -434,11 +434,14 @@ case "${1:-}" in fi case "$STATUS" in - ok|timeout|thin-client) + ok|timeout|thin-client|engine-locked) # "timeout" = slow-but-healthy engine (#1964); "thin-client" = - # remote-HTTP MCP brain, no local engine by design (#2051) — same - # treatment as "ok", matching gstack-gbrain-detect --is-ok and - # gen-skill-docs. + # remote-HTTP MCP brain, no local engine by design (#2051); + # "engine-locked" = same class (#2456): PGLite is single-writer, so a + # live `gbrain serve` (typically an MCP server) holds the embedded DB. + # gbrain is installed and healthy; a transient lock must not strip + # brain blocks out of every SKILL.md. All get the same treatment as + # "ok", matching gstack-gbrain-detect --is-ok and gen-skill-docs. echo "Detected gbrain v$VERSION (local-status: $STATUS)." # Render brain-aware blocks into an UNTRACKED out-dir (#2569) and # repoint the installed skills at it — the old in-place render wrote diff --git a/bin/gstack-gbrain-detect b/bin/gstack-gbrain-detect index 19797a495..4ec4f239d 100755 --- a/bin/gstack-gbrain-detect +++ b/bin/gstack-gbrain-detect @@ -288,17 +288,23 @@ function main(): void { } // --is-ok: live engine-status gate. Exits 0 iff gbrain is usable ("ok"; -// "timeout" — a slow-but-healthy engine, #1964; or "thin-client" — remote-HTTP -// MCP brain with no local engine by design, #2051 — neither slow nor remote -// must silently suppress brain features), 1 otherwise. Runs detection live -// (never reads the possibly-stale gbrain-detection.json), so callers — setup, -// bin/dev-setup, and `gstack-config gbrain-refresh` — can decide whether to -// render the gbrain :user variant without duplicating the JSON grep. -// Prints nothing on stdout. +// "timeout" — a slow-but-healthy engine, #1964; "thin-client" — remote-HTTP +// MCP brain with no local engine by design, #2051; or "engine-locked" — +// PGLite is single-writer, so a live `gbrain serve` (typically an MCP +// server) holds the embedded DB, #2456 — gbrain is installed and healthy in +// all four; none must silently suppress brain features), 1 otherwise. Runs +// detection live (never reads the possibly-stale gbrain-detection.json), so +// callers — setup, bin/dev-setup, and `gstack-config gbrain-refresh` — can +// decide whether to render the gbrain :user variant without duplicating the +// JSON grep. Prints nothing on stdout. if (process.argv.includes("--is-ok")) { const noCache = process.env.GSTACK_DETECT_NO_CACHE === "1"; const status = localEngineStatus({ noCache }); - process.exit(status === "ok" || status === "timeout" || status === "thin-client" ? 0 : 1); + process.exit( + status === "ok" || status === "timeout" || status === "thin-client" || status === "engine-locked" + ? 0 + : 1, + ); } main(); diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts index 0f7ac97dd..9596466f6 100644 --- a/scripts/gen-skill-docs.ts +++ b/scripts/gen-skill-docs.ts @@ -44,14 +44,14 @@ function loadGbrainOverride(): { detected: boolean } { try { const json = JSON.parse(fs.readFileSync(detectionPath, 'utf-8')) as { gbrain_local_status?: string }; // "timeout" = slow-but-healthy engine (#1964); "thin-client" = remote-HTTP - // MCP brain with no local engine by design (#2051). Both usable — same - // treatment as "ok", matching gstack-gbrain-detect --is-ok. - return { - detected: - json.gbrain_local_status === 'ok' || - json.gbrain_local_status === 'timeout' || - json.gbrain_local_status === 'thin-client', - }; + // MCP brain with no local engine by design (#2051); "engine-locked" = same + // class (#2456): PGLite is single-writer, so a live `gbrain serve` (e.g. + // an MCP server) owns the embedded DB — gbrain is installed and healthy, + // a legitimate holder has the lock, so a transient lock must not silently + // strip brain blocks from every SKILL.md. All usable — same treatment as + // "ok", matching gstack-gbrain-detect --is-ok. + const USABLE = ['ok', 'timeout', 'thin-client', 'engine-locked']; + return { detected: USABLE.includes(json.gbrain_local_status ?? '') }; } catch { return { detected: false }; } diff --git a/test/gbrain-detection-override.test.ts b/test/gbrain-detection-override.test.ts index c4905d897..7ecae820b 100644 --- a/test/gbrain-detection-override.test.ts +++ b/test/gbrain-detection-override.test.ts @@ -157,6 +157,33 @@ describe('gbrain detection override → gen-skill-docs', () => { } }); + test('with status "engine-locked" (PGLite single-writer, #2456), brain blocks render like "ok"', () => { + const { tmpHome, cleanup } = makeFixture( + JSON.stringify({ + gbrain_local_status: 'engine-locked', + gbrain_on_path: true, + gbrain_version: 'test-0.42.26', + }), + ); + try { + const snap = regenAndSnapshot({ + respectDetection: true, + tmpHome, + files: PROBE_FILES, + }); + const content = probeUnion(snap); + + // PGLite is single-writer: a live `gbrain serve` (the recommended + // /setup-gbrain default spawns one at session start) legitimately owns + // the embedded DB. gbrain is installed and healthy — a transient lock + // must not silently strip brain blocks (same reasoning as "timeout"). + expect(content).toContain('## Save Results to Brain'); + expect(content).toContain('gbrain put "office-hours/'); + } finally { + cleanup(); + } + }); + test('with detected:false (status != "ok"), brain blocks stay suppressed', () => { const { tmpHome, cleanup } = makeFixture( JSON.stringify({ gbrain_local_status: 'no-cli', gbrain_on_path: false, gbrain_version: null }),