{"record":{"id":"959a04f14616dd7c","repo":"garrytan/gstack","slug":"gbrain-sources-list-returned-no-json","errorCode":null,"errorMessage":"gbrain sources list returned no JSON","messagePattern":"gbrain sources list returned no JSON","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"lib/gbrain-guards.ts","lineNumber":202,"sourceCode":"  _keepStorageMemo = { key, value };\n  return value;\n}\n\n/** Test-only: reset the per-process capability memo. */\nexport function _resetCapabilityMemo(): void {\n  _keepStorageMemo = undefined;\n}\n\n// ── Destructive-op decisions ────────────────────────────────────────────────\n\n/**\n * Fetch + normalize the source list. Throws on read/parse failure so callers can\n * distinguish \"couldn't read\" (fail closed) from \"empty list\" (source absent).\n * Injectable for hermetic tests.\n */\nexport function fetchSources(env: NodeJS.ProcessEnv = process.env): GbrainSourceRow[] {\n  const raw = execGbrainJson([\"sources\", \"list\", \"--json\"], { baseEnv: env });\n  if (raw === null) throw new Error(\"gbrain sources list returned no JSON\");\n  return parseSourcesList(raw);\n}\n\nexport interface RemoveDecision {\n  allow: boolean;\n  /** Extra args to append to `sources remove` (e.g. --keep-storage). */\n  extraArgs: string[];\n  reason: string;\n}\n\n/**\n * Decide whether `sources remove <id>` is safe, and with what flags.\n *\n * Fail-closed cases (allow=false):\n *   - sources list unreadable/unparseable (can't prove the row is safe).\n *   - the row is user-managed (remote_url set AND local_path outside gbrain's\n *     clones) and gbrain has no --keep-storage to protect the files.\n *","sourceCodeStart":184,"sourceCodeEnd":220,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/lib/gbrain-guards.ts#L184-L220","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","Ensure no wrapper/pipe is stripping stdout before the Node process reads it.","Confirm gbrain is the intended binary on PATH (which gbrain) and not a stub.","Run /setup-gbrain to (re)configure the gbrain installation."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"import { execFileSync } from 'child_process';\nfunction gbrainEmitsJson(env: NodeJS.ProcessEnv = process.env): boolean {\n  try {\n    const out = execFileSync('gbrain', ['sources', 'list', '--json'], { encoding: 'utf-8', env, timeout: 5_000 });\n    return out.trim().startsWith('[') || out.trim().startsWith('{');\n  } catch { return false; }\n}","typeGuard":null,"tryCatchPattern":"try {\n  return fetchSources(env);\n} catch (e) {\n  if (e instanceof Error && e.message === 'gbrain sources list returned no JSON') {\n    // Treat as fail-closed: abort the destructive op, do not assume empty list\n    throw new Error('Cannot verify gbrain sources; aborting destructive operation. Run /setup-gbrain.');\n  }\n  throw e;\n}","preventionTips":["Run /setup-gbrain and confirm `gbrain sources list --json` prints JSON before invoking guards.","Pin a gbrain version known to support --json.","Ensure no wrapper swallows gbrain's stdout.","Treat this error as fail-closed in destructive-op flows."],"tags":["gbrain","cli","json","config","destructive-op-guard"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}