diff --git a/src/cli.ts b/src/cli.ts index 32d2e5081..72555c931 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -898,8 +898,21 @@ async function handleCliOnly(command: string, args: string[]) { await runDoctor(eng, args); await eng.disconnect(); } catch { - // DB unavailable — still run filesystem checks - await runDoctor(null, args, getDbUrlSource()); + // DB unavailable — still run filesystem checks. Detect the common + // case where PGLite is locked by an attached `gbrain serve` MCP + // (co-existence is expected, not an error) so doctor can demote + // the connection warning to an info-level ok message. + const fs = await import('fs'); + const path = await import('path'); + const { gbrainPath } = await import('./core/config.ts'); + let lockHeldByMcp = false; + try { + const pidFile = path.join(gbrainPath('brain.pglite'), 'postmaster.pid'); + lockHeldByMcp = fs.existsSync(pidFile); + } catch { + // best-effort detection + } + await runDoctor(null, args, getDbUrlSource(), { lockHeldByMcp }); } } return; diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index c5658fc52..d587db12c 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -783,7 +783,7 @@ export async function checkSyncFreshness(engine: BrainEngine): Promise { * user has no DB configured anywhere; otherwise the caller chose --fast or * we failed to connect despite a configured URL. */ -export async function runDoctor(engine: BrainEngine | null, args: string[], dbSource?: DbUrlSource) { +export async function runDoctor(engine: BrainEngine | null, args: string[], dbSource?: DbUrlSource, opts?: { lockHeldByMcp?: boolean }) { const jsonOutput = args.includes('--json'); const fastMode = args.includes('--fast'); const doFix = args.includes('--fix'); @@ -1260,14 +1260,24 @@ export async function runDoctor(engine: BrainEngine | null, args: string[], dbSo // skipped the connection. When null, there really is no config // anywhere. let msg: string; + let status: 'ok' | 'warn' | 'fail' = 'warn'; if (fastMode && dbSource) { msg = `Skipping DB checks (--fast mode, URL present from ${dbSource})`; + } else if (!fastMode && dbSource && opts?.lockHeldByMcp) { + // PGLite single-writer lock held by another process (typically + // `gbrain serve` MCP). Co-existence with MCP is expected, not an + // error: filesystem checks still ran; DB-backed health is reachable + // via the MCP surface (mcp__gbrain__get_health). Demoting to `ok` + // with an info-prefixed message so this stops surfacing as a warning + // every time a user runs `gbrain doctor` alongside an attached MCP. + msg = `info: DB owned by another process (likely 'gbrain serve' MCP holding the PGLite single-writer lock). Filesystem checks ran; use 'mcp__gbrain__get_health' for DB-backed checks.`; + status = 'ok'; } else if (!fastMode && dbSource) { msg = `Could not connect to configured DB (URL from ${dbSource}); filesystem checks only`; } else { msg = 'No database configured (filesystem checks only). Set GBRAIN_DATABASE_URL or run `gbrain init`.'; } - checks.push({ name: 'connection', status: 'warn', message: msg }); + checks.push({ name: 'connection', status, message: msg }); } const earlyFail1 = outputResults(checks, jsonOutput); process.exit(earlyFail1 ? 1 : 0); diff --git a/src/core/check-resolvable.ts b/src/core/check-resolvable.ts index bb1249d1c..490a7486e 100644 --- a/src/core/check-resolvable.ts +++ b/src/core/check-resolvable.ts @@ -163,7 +163,7 @@ export function parseResolverEntries(resolverContent: string): ResolverEntry[] { /** Simple YAML frontmatter parser — extracts triggers array if present. */ function extractTriggers(skillContent: string): string[] { - const fmMatch = skillContent.match(/^---\n([\s\S]*?)\n---/); + const fmMatch = skillContent.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (!fmMatch) return []; const fm = fmMatch[1]; const triggersMatch = fm.match(/^triggers:\s*\n((?:\s+-\s+.+\n?)*)/m);