#2869·gstack

gbrain >= 0.50: JSON `pglite_busy` on stdout makes `gbrain-refresh`/`setup` classify a healthy locked PGLite brain as `broken-config`

Author: tldkhoaCreated Sep 15, 2026Updated Sep 15, 2026

Summary

With gbrain >= 0.50, gstack-config gbrain-refresh (and ./setup) run inside a live Claude Code session classify a perfectly healthy local PGLite brain as broken-config and suppress the brain-aware blocks in every planning-skill SKILL.md.

Root cause: the detection probe gbrain sources list --json now reports the held-lock case as a JSON object on stdout with empty stderr. lib/gbrain-local-status.ts only inspects stderr, matches none of its patterns, and falls through to the defensive broken-config default. The accurate status is engine-locked, which gstack already treats as usable.

Environment

  • gstack v1.87.0.0 (global git install, macOS, Darwin 27.0.0)
  • gbrain 0.50.0.0, engine: pglite, MCP registered as gbrain serve (local-stdio)
  • The Claude Code session's gbrain serve MCP process holds the PGLite lock, so any CLI probe from inside that session hits the busy path.

Repro

  1. Have a working PGLite gbrain (gbrain doctor ok) and a Claude Code session open with the gbrain MCP registered (so gbrain serve holds the lock).
  2. From that session run ~/.claude/skills/gstack/bin/gstack-config gbrain-refresh.

Output:

gbrain not detected (local-status: broken-config) → brain-aware blocks will be suppressed in planning-skill SKILL.md files.
Install gbrain (see /setup-gbrain) and re-run 'gstack-config gbrain-refresh' once it's configured.

gstack-gbrain-detect at the same time reports gbrain_config_exists: true, gbrain_doctor_ok: true, gbrain_engine: "pglite", but gbrain_local_status: "broken-config".

The exact probe the classifier runs:

$ gbrain sources list --json; echo "EXIT=$?"
{"error":"pglite_busy","retryable":true,"reason":"live_serve","next_action":"Wait for the current command or server to close, then retry. Do not remove a live lock."}
EXIT=1

stderr is empty. Without --json, gbrain prints the human message on stderr ("GBrain's local database is already open through gbrain serve (MCP, PID N) ..."), which the existing already open through branch (#2194 follow-up) handles correctly. The JSON mode bypasses that branch.

Impact

  • Every ./setup run inside a Claude session (including the auto-upgrade flow) silently disables gbrain integration in the planning skills, with a remediation message that tells the user to reinstall a working brain.
  • The 60s detection cache and ~/.gstack/gbrain-detection.json then persist the wrong status.

Fix that works locally

Capture stdout from the failed execFileSync and treat the pglite_busy JSON like the held-lock stderr message:

    const stderr = (e.stderr ? e.stderr.toString() : "") || "";
    // gbrain >= 0.50 with --json reports the held PGLite lock as a JSON object on
    // STDOUT ({"error":"pglite_busy","reason":"live_serve",...}) with empty stderr.
    const stdout = (e.stdout ? e.stdout.toString() : "") || "";
    ...
      if (stderr.includes("already open through")) {
        return configuredEngine(env) === "pglite" ? "engine-locked" : "broken-db";
      }
      if (stdout.includes("\"pglite_busy\"") || stderr.includes("\"pglite_busy\"")) {
        return configuredEngine(env) === "pglite" ? "engine-locked" : "broken-db";
      }

With that patch gstack-gbrain-detect reports engine-locked and gbrain-refresh renders the brain-aware blocks:

Detected gbrain vgbrain0.50.0.0 (local-status: engine-locked).
Rendered brain-aware blocks into ~/.gstack/render/claude — now live across all your projects' Claude sessions.

A more robust variant would parse stdout as JSON when it starts with { and switch on error/reason, since gbrain's --json error envelope seems to be the direction its CLI is heading. Happy to open a PR if that shape is preferred.

Related: #2194 (same held-lock case, pre-JSON stderr message).