What's happening
`/sync-gbrain` (and any gstack skill that calls `detectEngineTier()`) always reports `engine=unknown` for anyone running gbrain ≥0.25 against a Supabase brain that has any unhealthy check. That covers basically everyone on Supabase — because a fresh install always has at least one warning (resolver_health, or a pending migration) until everything is perfectly tuned.
When engine is unknown, the orchestrator logs `[gbrain-sync] mode=X engine=unknown` and skips all three sync stages silently. No error. No hint that anything is wrong. You just... don't get a brain sync.
Root cause
It's two things hitting at the same time, which is why it's subtle.
Problem 1: execSync throws on non-zero exit, swallowing the JSON
`freshDetectEngineTier()` in `lib/gstack-memory-helpers.ts` (line 252) does this:
```typescript
const out = execSync("gbrain doctor --json --fast 2>/dev/null", { encoding: "utf-8", timeout: 5000 });
const parsed = JSON.parse(out);
```
`gbrain doctor` exits with code `1` whenever health_score < 100. `execSync` throws on non-zero exit — the stdout never reaches `out`. The catch block returns `{ engine: "unknown" }` immediately. The JSON that gbrain wrote to stdout is gone.
Problem 2: gbrain ≥0.25 changed doctor's output format
Even if the exit code were zero, it wouldn't help. gbrain ≥0.25 switched to `schema_version:2` for doctor output, which dropped the top-level `engine` field entirely. The old format had `parsed.engine === "pglite"` or `parsed.engine === "supabase"`. The new format looks like:
```json
{
"schema_version": 2,
"status": "unhealthy",
"health_score": 70,
"checks": [...]
}
```
No `engine` key. `parsed?.engine` is `undefined`. Falls straight to `"unknown"`.
Both problems need fixing. Either one alone would still break things.
Repro
- Install gbrain ≥0.25, connect to Supabase
- Make sure at least one doctor check is non-green (resolver_health warning is always present on a fresh install — this is basically always true)
- Run `/sync-gbrain` or call `detectEngineTier()` directly:
```bash
bun run ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts --dry-run
Output: [gbrain-sync] mode=dry-run engine=unknown
```
Or inline:
```bash
cd ~/.claude/skills/gstack && bun -e "
const { detectEngineTier } = await import('./lib/gstack-memory-helpers.ts');
console.log(detectEngineTier());
"
{ engine: 'unknown', ... }
```
Environment:
- gstack v1.31.0.0
- gbrain v0.31.3
- Supabase Postgres brain (schema_version:2 doctor output)
- macOS 26.4.1
Fix
Two changes to `freshDetectEngineTier()`:
- Catch the `execSync` throw and read stdout off the error object (Node.js puts it there when `stdio: 'pipe'` — the default for execSync)
- When `parsed?.engine` is still undefined after that (schema_version:2 case), fall back to reading `~/.gbrain/config.json`
```typescript
function freshDetectEngineTier(): EngineDetect {
const now = Date.now();
let parsed: Record<string, unknown> | null = null;
try {
const out = execSync("gbrain doctor --json --fast 2>/dev/null", { encoding: "utf-8", timeout: 5000 });
parsed = JSON.parse(out);
} catch (err: unknown) {
// execSync throws on non-zero exit; stdout is still on the error object
try {
const stdout = (err as { stdout?: string })?.stdout ?? "";
if (stdout) parsed = JSON.parse(stdout);
} catch { /* unparseable stdout — stay null */ }
}
// gbrain >=0.25 uses schema_version:2 which dropped the top-level engine
// field. Fall back to ~/.gbrain/config.json when doctor doesn't provide it.
let engine: EngineTier = parsed?.engine === "supabase" ? "supabase" : parsed?.engine === "pglite" ? "pglite" : "unknown";
if (engine === "unknown") {
try {
const configPath = join(homedir(), ".gbrain", "config.json");
const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
if (cfg?.engine === "pglite") engine = "pglite";
else if (cfg?.engine === "postgres" || cfg?.database_url) engine = "supabase";
} catch { /* config unreadable — stay unknown */ }
}
return {
engine,
supabase_url: parsed?.supabase_url as string | undefined,
detected_at: now,
schema_version: 1,
};
}
```
Tested on gstack v1.31.0.0 + gbrain v0.31.3 + Supabase. Before the patch: `engine=unknown` on every invocation. After: `engine=supabase`.
Why this matters
Everyone using `/sync-gbrain` with Supabase has been getting silent no-ops. There's no error — just a dry-run-like outcome where all stages skip. The brain never syncs. This is especially painful because the resolver_health warning (which triggers non-zero exit from doctor) is present on basically every install.
What's happening
`/sync-gbrain` (and any gstack skill that calls `detectEngineTier()`) always reports `engine=unknown` for anyone running gbrain ≥0.25 against a Supabase brain that has any unhealthy check. That covers basically everyone on Supabase — because a fresh install always has at least one warning (resolver_health, or a pending migration) until everything is perfectly tuned.
When engine is unknown, the orchestrator logs `[gbrain-sync] mode=X engine=unknown` and skips all three sync stages silently. No error. No hint that anything is wrong. You just... don't get a brain sync.
Root cause
It's two things hitting at the same time, which is why it's subtle.
Problem 1:
execSyncthrows on non-zero exit, swallowing the JSON`freshDetectEngineTier()` in `lib/gstack-memory-helpers.ts` (line 252) does this:
```typescript
const out = execSync("gbrain doctor --json --fast 2>/dev/null", { encoding: "utf-8", timeout: 5000 });
const parsed = JSON.parse(out);
```
`gbrain doctor` exits with code `1` whenever health_score < 100. `execSync` throws on non-zero exit — the stdout never reaches `out`. The catch block returns `{ engine: "unknown" }` immediately. The JSON that gbrain wrote to stdout is gone.
Problem 2: gbrain ≥0.25 changed doctor's output format
Even if the exit code were zero, it wouldn't help. gbrain ≥0.25 switched to `schema_version:2` for doctor output, which dropped the top-level `engine` field entirely. The old format had `parsed.engine === "pglite"` or `parsed.engine === "supabase"`. The new format looks like:
```json
{
"schema_version": 2,
"status": "unhealthy",
"health_score": 70,
"checks": [...]
}
```
No `engine` key. `parsed?.engine` is `undefined`. Falls straight to `"unknown"`.
Both problems need fixing. Either one alone would still break things.
Repro
```bash
bun run ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts --dry-run
Output: [gbrain-sync] mode=dry-run engine=unknown
```
Or inline:
```bash
cd ~/.claude/skills/gstack && bun -e "
const { detectEngineTier } = await import('./lib/gstack-memory-helpers.ts');
console.log(detectEngineTier());
"
{ engine: 'unknown', ... }
```
Environment:
Fix
Two changes to `freshDetectEngineTier()`:
```typescript
function freshDetectEngineTier(): EngineDetect {
const now = Date.now();
let parsed: Record<string, unknown> | null = null;
try {
const out = execSync("gbrain doctor --json --fast 2>/dev/null", { encoding: "utf-8", timeout: 5000 });
parsed = JSON.parse(out);
} catch (err: unknown) {
// execSync throws on non-zero exit; stdout is still on the error object
try {
const stdout = (err as { stdout?: string })?.stdout ?? "";
if (stdout) parsed = JSON.parse(stdout);
} catch { /* unparseable stdout — stay null */ }
}
// gbrain >=0.25 uses schema_version:2 which dropped the top-level
engine// field. Fall back to ~/.gbrain/config.json when doctor doesn't provide it.
let engine: EngineTier = parsed?.engine === "supabase" ? "supabase" : parsed?.engine === "pglite" ? "pglite" : "unknown";
if (engine === "unknown") {
try {
const configPath = join(homedir(), ".gbrain", "config.json");
const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
if (cfg?.engine === "pglite") engine = "pglite";
else if (cfg?.engine === "postgres" || cfg?.database_url) engine = "supabase";
} catch { /* config unreadable — stay unknown */ }
}
return {
engine,
supabase_url: parsed?.supabase_url as string | undefined,
detected_at: now,
schema_version: 1,
};
}
```
Tested on gstack v1.31.0.0 + gbrain v0.31.3 + Supabase. Before the patch: `engine=unknown` on every invocation. After: `engine=supabase`.
Why this matters
Everyone using `/sync-gbrain` with Supabase has been getting silent no-ops. There's no error — just a dry-run-like outcome where all stages skip. The brain never syncs. This is especially painful because the resolver_health warning (which triggers non-zero exit from doctor) is present on basically every install.