garrytan/gstack · error · Error

gbrain sources remove ${id} failed: ${rm.stderr || rm.stdout

Error message

gbrain sources remove ${id} failed: ${rm.stderr || rm.stdout || `exit ${rm.status}`}

What it means

Thrown after spawnSync('gbrain', ['sources','remove',id,'--yes','--confirm-destructive', ...]) returns a non-zero status during drift re-registration. The message embeds the child's stderr, then stdout, then the raw exit code so the underlying gbrain failure is visible. This is a wrapper around a failed subprocess, not a logic error in this library.

Source

Thrown at lib/gbrain-sources.ts:238

        );
      }
      const decision = decideSourceRemove(id, env ?? process.env, options.removeDecision ?? {});
      if (!decision.allow) {
        throw new Error(`refusing drift re-register of ${id}: ${decision.reason}`);
      }

      const rm = spawnSync(
        "gbrain",
        ["sources", "remove", id, "--yes", "--confirm-destructive", ...decision.extraArgs],
        {
          encoding: "utf-8",
          timeout: 30_000,
          env,
          shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows
        },
      );
      if (rm.status !== 0) {
        throw new Error(`gbrain sources remove ${id} failed: ${rm.stderr || rm.stdout || `exit ${rm.status}`}`);
      }
    }

    // Add.
    const addArgs = ["sources", "add", id, "--path", path];
    if (federated) addArgs.push("--federated");
    const add = spawnSync("gbrain", addArgs, {
      encoding: "utf-8",
      timeout: 30_000,
      env,
      shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows
    });
    if (add.status !== 0) {
      throw new Error(`gbrain sources add ${id} failed: ${add.stderr || add.stdout || `exit ${add.status}`}`);
    }

    return {
      changed: true,

View on GitHub (pinned to 94993f7401)

Solutions

  1. Run `gbrain sources remove <id> --yes --confirm-destructive` manually and read the real error from gbrain directly.
  2. Verify gbrain is installed and on PATH: `gbrain --version` (on Windows ensure the .cmd shim resolves).
  3. Upgrade gbrain to >= 0.42 so --confirm-destructive is recognized.
  4. If the source is already gone, re-probe registration (the next sync should see status 'absent' and skip straight to add).

Example fix

// before
const rm = spawnSync('gbrain', ['sources','remove',id,'--yes','--confirm-destructive', ...decision.extraArgs], { encoding:'utf-8', timeout:30_000, env, shell: NEEDS_SHELL_ON_WINDOWS });
if (rm.status !== 0) throw new Error(`gbrain sources remove ${id} failed: ${rm.stderr || rm.stdout || `exit ${rm.status}`}`);

// after: surface signal/timeout too, and pre-flight the id existence
if (probe.status === 'absent') { /* nothing to remove */ }
else {
  const rm = spawnSync('gbrain', [...], {...});
  if (rm.error) throw new Error(`gbrain remove ${id} spawn failed: ${rm.error.message}`);
  if (rm.signal) throw new Error(`gbrain remove ${id} killed by ${rm.signal}`);
  if (rm.status !== 0) throw new Error(`gbrain sources remove ${id} failed: ${rm.stderr || rm.stdout || `exit ${rm.status}`}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight: is gbrain present and is the source registered before attempting a drift remove?
import { spawnSync } from 'node:child_process';

function gbrainWorks(env = process.env): boolean {
  const r = spawnSync('gbrain', ['--version'], { encoding: 'utf-8', env, shell: process.platform === 'win32' });
  return r.status === 0;
}

function sourceExists(id: string, env = process.env): boolean {
  const r = spawnSync('gbrain', ['sources', 'show', id, '--json'], { encoding: 'utf-8', env, shell: process.platform === 'win32' });
  return r.status === 0;
}

Try / catch

try {
  await ensureSourceRegistered(id, path, { reregister_on_drift: true });
} catch (e) {
  const msg = String(e?.message ?? e);
  if (msg.startsWith('gbrain sources remove ') && /not registered|not found/i.test(msg)) {
    // source already gone — fall through to a plain add
    await ensureSourceRegistered(id, path, { reregister_on_drift: false });
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: The drift branch of ensureSourceRegistered() runs the remove and rm.status !== 0. Causes include: gbrain not on PATH (especially on Windows where it is a .cmd shim requiring shell:true), the source id already removed by a concurrent process, gbrain < 0.42 rejecting the flags, a corrupt index, or a timeout-style exit.

Common situations: gbrain binary missing or not on PATH in a fresh CI container; Windows where NEEDS_SHELL_ON_WINDOWS mishandles the .cmd shim; a previous partial sync left the source half-removed; gbrain version skew where --confirm-destructive is unknown (#1985 regression); a 30s timeout hit on a slow disk.

Related errors


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