garrytan/gstack · error · Error

gbrain sources list returned no JSON

Error message

gbrain sources list returned no JSON

What it means

Thrown by fetchSources() in gbrain-guards when execGbrainJson(['sources','list','--json']) returns null — meaning the gbrain CLI ran but emitted no JSON on stdout at all. This is a fail-closed signal: the destructive-op guards cannot prove a source row is safe without the list, so they refuse to proceed rather than treat an unreadable list as empty. Distinct from 'CLI not on PATH' (spawn failed) and 'non-JSON output' (stdout existed but unparseable).

Source

Thrown at lib/gbrain-guards.ts:202

  _keepStorageMemo = { key, value };
  return value;
}

/** Test-only: reset the per-process capability memo. */
export function _resetCapabilityMemo(): void {
  _keepStorageMemo = undefined;
}

// ── Destructive-op decisions ────────────────────────────────────────────────

/**
 * Fetch + normalize the source list. Throws on read/parse failure so callers can
 * distinguish "couldn't read" (fail closed) from "empty list" (source absent).
 * Injectable for hermetic tests.
 */
export function fetchSources(env: NodeJS.ProcessEnv = process.env): GbrainSourceRow[] {
  const raw = execGbrainJson(["sources", "list", "--json"], { baseEnv: env });
  if (raw === null) throw new Error("gbrain sources list returned no JSON");
  return parseSourcesList(raw);
}

export interface RemoveDecision {
  allow: boolean;
  /** Extra args to append to `sources remove` (e.g. --keep-storage). */
  extraArgs: string[];
  reason: string;
}

/**
 * Decide whether `sources remove <id>` is safe, and with what flags.
 *
 * Fail-closed cases (allow=false):
 *   - sources list unreadable/unparseable (can't prove the row is safe).
 *   - the row is user-managed (remote_url set AND local_path outside gbrain's
 *     clones) and gbrain has no --keep-storage to protect the files.
 *

View on GitHub (pinned to 94993f7401)

Solutions

  1. Run `gbrain sources list --json` manually in the same environment and inspect stdout — if no JSON appears, upgrade gbrain to a version that supports --json.
  2. Ensure no wrapper/pipe is stripping stdout before the Node process reads it.
  3. Confirm gbrain is the intended binary on PATH (which gbrain) and not a stub.
  4. Run /setup-gbrain to (re)configure the gbrain installation.
Defensive patterns

Strategy: try-catch

Validate before calling

import { execFileSync } from 'child_process';
function gbrainEmitsJson(env: NodeJS.ProcessEnv = process.env): boolean {
  try {
    const out = execFileSync('gbrain', ['sources', 'list', '--json'], { encoding: 'utf-8', env, timeout: 5_000 });
    return out.trim().startsWith('[') || out.trim().startsWith('{');
  } catch { return false; }
}

Try / catch

try {
  return fetchSources(env);
} catch (e) {
  if (e instanceof Error && e.message === 'gbrain sources list returned no JSON') {
    // Treat as fail-closed: abort the destructive op, do not assume empty list
    throw new Error('Cannot verify gbrain sources; aborting destructive operation. Run /setup-gbrain.');
  }
  throw e;
}

Prevention

When it happens

Trigger: execGbrainJson runs gbrain sources list --json; the command exits but stdout contains no JSON token (e.g. only log output, a help banner, or empty stdout from a misconfigured subcommand). raw === null triggers the throw.

Common situations: gbrain version where --json is not yet supported and prints human text; GSTDOUT redirected/captured incorrectly; gbrain printed an error to stdout instead of stderr and exited 0; a wrapper script swallowed the JSON stream.

Related errors


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/959a04f14616dd7c. Report an issue: GitHub.