Repro
$ gstack --version # 1.40.0.0
$ gbrain --version # 0.33.1.0 (any ≥ 0.18.0)
$ /sync-gbrain --full
[gbrain-sync] mode=full engine=supabase
[gbrain-exec] seeded DATABASE_URL from /Users/x/.gbrain/config.json
gstack-gbrain-sync fatal: list.find is not a function. (In 'list.find((s) => s.id === sourceId)', 'list.find' is undefined)
Root cause
bin/gstack-gbrain-sync.ts:291 — sourceLocalPath():
export function sourceLocalPath(sourceId: string, env?: NodeJS.ProcessEnv): string | null {
const list = execGbrainJson<Array<{ id: string; local_path?: string }>>(
["sources", "list", "--json"],
{ baseEnv: env },
);
if (!list) return null;
const found = list.find((s) => s.id === sourceId); // ← crashes
return found?.local_path ?? null;
}
The function types the response as Array<...> but gbrain sources list --json has returned {sources: [...]} since gbrain v0.18.0 (commit 90c5d93, PR #337, "multi-source brains," 2026-04-22).
Reference, gbrain/src/commands/sources.ts:202:
console.log(JSON.stringify({ sources: entries }, null, 2));
Why it didn't show up sooner
sourceLocalPath is only called by runCodeImport, which runs on /sync-gbrain --full. Incremental sync (the hourly LaunchAgent path via gstack-brain-sync --once) doesn't touch it, so users whose code-stage reindex hasn't fired never see the crash.
Suggested patch (4 lines)
export function sourceLocalPath(sourceId: string, env?: NodeJS.ProcessEnv): string | null {
const raw = execGbrainJson<
| Array<{ id: string; local_path?: string }>
| { sources?: Array<{ id: string; local_path?: string }> }
>(["sources", "list", "--json"], { baseEnv: env });
if (!raw) return null;
// gbrain v0.18.0+ returns {sources: [...]}; older versions returned a raw array.
const list = Array.isArray(raw) ? raw : raw.sources ?? [];
const found = list.find((s) => s.id === sourceId);
return found?.local_path ?? null;
}
Defensive against both shapes, so it works on any gbrain version.
Suggested test
A contract test that shells out to a real gbrain sources list --json (against a tmp engine) would have caught this. The current test suite must be using a fixture that returns the raw-array shape, since the function would otherwise fail in unit tests too. Worth adding test/contract/gbrain-cli.test.ts that runs against a real gbrain binary and red-flags any new shape divergence in CI.
Environment
- macOS 15.x, gstack v1.40.0.0
- gbrain v0.33.1.0 (also reproduces on any gbrain ≥ 0.18.0 based on shape history)
- Mode: local-stdio, engine: Supabase postgres
- Repro path:
/sync-gbrain --full on a fully-set-up brain
Happy to send a PR if helpful.
Repro
Root cause
bin/gstack-gbrain-sync.ts:291—sourceLocalPath():The function types the response as
Array<...>butgbrain sources list --jsonhas returned{sources: [...]}since gbrain v0.18.0 (commit90c5d93, PR #337, "multi-source brains," 2026-04-22).Reference,
gbrain/src/commands/sources.ts:202:Why it didn't show up sooner
sourceLocalPathis only called byrunCodeImport, which runs on/sync-gbrain --full. Incremental sync (the hourly LaunchAgent path viagstack-brain-sync --once) doesn't touch it, so users whose code-stage reindex hasn't fired never see the crash.Suggested patch (4 lines)
Defensive against both shapes, so it works on any gbrain version.
Suggested test
A contract test that shells out to a real
gbrain sources list --json(against a tmp engine) would have caught this. The current test suite must be using a fixture that returns the raw-array shape, since the function would otherwise fail in unit tests too. Worth addingtest/contract/gbrain-cli.test.tsthat runs against a real gbrain binary and red-flags any new shape divergence in CI.Environment
/sync-gbrain --fullon a fully-set-up brainHappy to send a PR if helpful.