fix: resolve GBRAIN_HOME with gbrain's parent-dir semantics (#2521)

gstack treated GBRAIN_HOME as the config directory; gbrain's configDir()
treats it as the PARENT and always appends `.gbrain` itself (the contract
is explicit in gbrain's source: GBRAIN_HOME=/tmp/x → /tmp/x/.gbrain/
config.json). With GBRAIN_HOME set, gstack classified engine status from
a file gbrain never reads — the probe's two halves (file checks vs the
spawned `gbrain sources list`) looked at DIFFERENT installs, so any
resulting status was arbitrary: missing-config/broken-config against
healthy installs, or a thin-client marker gstack saw that gbrain itself
reported as "No brain configured".

New shared resolver `gbrainConfigDir()` in lib/gbrain-exec.ts is the
single source of truth. All seven gstack sites route through the contract:

- lib/gbrain-local-status.ts gbrainConfigPath (the classifier's file half)
- bin/gstack-gbrain-detect GBRAIN_CONFIG + readRemoteMcpUrl
- lib/gbrain-exec.ts buildGbrainEnv (the probe's DATABASE_URL seed —
  fixing only the classifier would have left the split-brain in the
  spawn half, flagged by the reporter)
- lib/gbrain-guards.ts gbrainHome (clones-dir + autopilot-lock paths)
- lib/gstack-memory-helpers.ts gbrainConfigPath (engine-tier fallback)
- bin/gstack-gbrain-install pre-doctor config check (shell)

Unit tests cover GBRAIN_HOME set (config found at $GBRAIN_HOME/.gbrain),
the old flat layout explicitly NOT read (both classifier and
buildGbrainEnv), and unset (~/.gbrain unchanged). Existing fixtures that
encoded the deviant flat layout are updated to gbrain's contract.

Root-cause analysis by @d-danielsun in #2521.

Deviation from the 3-site plan spec: the same deviant resolution existed
in four more sites (buildGbrainEnv, gbrain-guards, memory-helpers,
gbrain-install); fixing only three would have left gstack disagreeing
with itself as well as with gbrain, so the whole class moved to the
shared resolver in one change.

Fixes #2521

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-16 10:01:44 -07:00
co-authored by Claude Fable 5
parent ce4a7bbb7e
commit 5854d122d3
10 changed files with 115 additions and 41 deletions
+22 -9
View File
@@ -21,10 +21,12 @@
* spawn. This is the central bug the helper exists to prevent
* regressing on.
*
* 3. **`GBRAIN_HOME` honored consistently.** Other gstack helpers
* (`detectEngineTier`) already honor `GBRAIN_HOME`. `buildGbrainEnv`
* reads from `${GBRAIN_HOME:-$HOME/.gbrain}/config.json` so all
* gstack-side gbrain calls agree on which config file matters.
* 3. **`GBRAIN_HOME` honored consistently — with gbrain's own semantics
* (#2521).** gbrain's configDir() treats `GBRAIN_HOME` as a PARENT
* directory and always appends `.gbrain` itself (GBRAIN_HOME=/tmp/x
* → /tmp/x/.gbrain/config.json). Every gstack-side read goes through
* `gbrainConfigDir()` below so gstack and gbrain agree on which
* config file matters.
*
* **Escape hatch:** `GSTACK_RESPECT_ENV_DATABASE_URL=1` returns the
* caller's env unchanged. Use only when the brain intentionally lives in
@@ -75,8 +77,21 @@ export function isTransactionModePooler(url: string): boolean {
}
/**
* Build an env dict with DATABASE_URL seeded from
* `${GBRAIN_HOME:-$HOME/.gbrain}/config.json`. Returns the base env
* gbrain's config directory, matching gbrain's own configDir() contract
* (#2521): `GBRAIN_HOME` is a PARENT directory — gbrain always appends
* `.gbrain` itself, so GBRAIN_HOME=/tmp/x reads /tmp/x/.gbrain/config.json.
* Unset → ~/.gbrain. Every gstack-side gbrain-config read MUST resolve
* through this helper, or gstack classifies engine status from a file
* gbrain never reads.
*/
export function gbrainConfigDir(env: NodeJS.ProcessEnv = process.env): string {
if (env.GBRAIN_HOME) return join(env.GBRAIN_HOME, ".gbrain");
return join(env.HOME || homedir(), ".gbrain");
}
/**
* Build an env dict with DATABASE_URL seeded from gbrain's config.json
* (resolved via `gbrainConfigDir`). Returns the base env
* unchanged when:
* - `GSTACK_RESPECT_ENV_DATABASE_URL=1` (intentional opt-out),
* - the config file is missing or unparseable,
@@ -98,9 +113,7 @@ export function buildGbrainEnv(opts: BuildGbrainEnvOptions = {}): NodeJS.Process
const out: NodeJS.ProcessEnv = { ...baseEnv };
if (baseEnv.GSTACK_RESPECT_ENV_DATABASE_URL === "1") return out;
const homeBase = baseEnv.HOME || homedir();
const gbrainHome = baseEnv.GBRAIN_HOME || join(homeBase, ".gbrain");
const configPath = join(gbrainHome, "config.json");
const configPath = join(gbrainConfigDir(baseEnv), "config.json");
if (!existsSync(configPath)) return out;
let cfg: GbrainConfig = {};