From 8b03c357ef374f55f61f54e237d68d4193686fcf Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Mon, 18 May 2026 20:34:05 -0700 Subject: [PATCH] fix(brain-context-load): probe gbrain via execFile, not shell builtin (#1559) gbrainAvailable() used `execFileSync("command", ["-v", "gbrain"])`, which fails in any environment where the `command` builtin isn't on the spawned process's PATH (most non-interactive shells). The probe then reported gbrain as missing even when it was installed, and context-load silently skipped vector/list queries. Fix: probe `gbrain --version` directly with a 500ms timeout (matching the rest of the file's MCP_TIMEOUT_MS). Same semantics, works everywhere execFile works. Contributed by @jbetala7 via #1560. Closes #1559. Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/gstack-brain-context-load.ts | 5 ++- test/gstack-brain-context-load.test.ts | 52 +++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/bin/gstack-brain-context-load.ts b/bin/gstack-brain-context-load.ts index e68e46e2a..8ad4eb63c 100644 --- a/bin/gstack-brain-context-load.ts +++ b/bin/gstack-brain-context-load.ts @@ -192,7 +192,10 @@ function resolveSkillFile(args: CliArgs): string | null { function gbrainAvailable(): boolean { try { - execFileSync("command", ["-v", "gbrain"], { stdio: "ignore" }); + execFileSync("gbrain", ["--version"], { + stdio: "ignore", + timeout: MCP_TIMEOUT_MS, + }); return true; } catch { return false; diff --git a/test/gstack-brain-context-load.test.ts b/test/gstack-brain-context-load.test.ts index 459a20e2e..61985f0fc 100644 --- a/test/gstack-brain-context-load.test.ts +++ b/test/gstack-brain-context-load.test.ts @@ -7,9 +7,9 @@ */ import { describe, it, expect } from "bun:test"; -import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs"; +import { chmodSync, mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs"; import { tmpdir } from "os"; -import { join } from "path"; +import { delimiter, join } from "path"; import { spawnSync } from "child_process"; const SCRIPT = join(import.meta.dir, "..", "bin", "gstack-brain-context-load.ts"); @@ -27,6 +27,37 @@ function runScript(args: string[], env: Record = {}): { stdout: }; } +function writeFakeGbrain(binDir: string): void { + if (process.platform === "win32") { + writeFileSync( + join(binDir, "gbrain.cmd"), + "@echo off\r\nif \"%1\"==\"--version\" (\r\n echo gbrain 0.test\r\n) else (\r\n echo fake gbrain %*\r\n)\r\n", + "utf-8", + ); + return; + } + + const fakeBin = join(binDir, "gbrain"); + writeFileSync( + fakeBin, + `#!/bin/sh +if [ "$1" = "--version" ]; then + echo "gbrain 0.test" +else + echo "fake gbrain $*" +fi +`, + "utf-8", + ); + chmodSync(fakeBin, 0o755); +} + +function prependPath(binDir: string): Record { + const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") || "PATH"; + const currentPath = process.env[pathKey] || ""; + return { [pathKey]: `${binDir}${delimiter}${currentPath}` }; +} + describe("gstack-brain-context-load CLI", () => { it("--help exits 0 with usage", () => { const r = runScript(["--help"]); @@ -204,6 +235,23 @@ gbrain: }); describe("gstack-brain-context-load — graceful gbrain absence", () => { + it("uses gbrain when a binary is available on PATH", () => { + const dir = mkdtempSync(join(tmpdir(), "gstack-bcl-")); + const binDir = join(dir, "bin"); + mkdirSync(binDir); + writeFakeGbrain(binDir); + + try { + const r = runScript(["--repo", "test-repo", "--explain"], prependPath(binDir)); + expect(r.exitCode).toBe(0); + expect(r.stderr).toContain("OK"); + expect(r.stderr).not.toContain("gbrain CLI missing"); + expect(r.stdout).toContain("fake gbrain list_pages"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it("vector + list queries still complete (with SKIP) when gbrain CLI is missing", () => { // We can't easily un-install gbrain; rely on the helper's own missing-binary // detection. The default manifest uses kind: list which calls gbrain. If