garrytan/gstack · error · Error

git ${args.join(' ')} failed (exit ${result.status}): ${stde

Error message

git ${args.join(' ')} failed (exit ${result.status}): ${stderr || stdout}

What it means

The internal git() helper in lib/worktree.ts throws whenever a git subprocess exits non-zero and tolerateFailure is false. It formats the full argv, exit status, and stderr (or stdout) so the failing command is identifiable. It is the single chokepoint for all git invocations in the worktree/harvest module.

Source

Thrown at lib/worktree.ts:60

    // Skip symlinks to avoid infinite recursion (e.g., .claude/skills/gstack → repo root)
    if (entry.isSymbolicLink()) continue;
    const srcPath = path.join(src, entry.name);
    const destPath = path.join(dest, entry.name);
    if (entry.isDirectory()) {
      copyDirSync(srcPath, destPath);
    } else {
      fs.copyFileSync(srcPath, destPath);
    }
  }
}

/** Run a git command and return stdout. Throws on failure unless tolerateFailure is set. */
function git(args: string[], cwd: string, tolerateFailure = false): string {
  const result = spawnSync('git', args, { cwd, stdio: 'pipe', timeout: 30_000 });
  const stdout = result.stdout?.toString().trim() ?? '';
  const stderr = result.stderr?.toString().trim() ?? '';
  if (result.status !== 0 && !tolerateFailure) {
    throw new Error(`git ${args.join(' ')} failed (exit ${result.status}): ${stderr || stdout}`);
  }
  return stdout;
}

// --- Dedup index ---

interface DedupIndex {
  hashes: Record<string, string>; // hash → first-seen runId
}

function getDedupPath(): string {
  return path.join(os.homedir(), '.gstack-dev', 'harvests', 'dedup.json');
}

function loadDedupIndex(): DedupIndex {
  try {
    const raw = fs.readFileSync(getDedupPath(), 'utf-8');
    return JSON.parse(raw);

View on GitHub (pinned to 94993f7401)

Solutions

  1. Reproduce the exact command from the message in the same cwd to see git's real error.
  2. Remove a stale .git/index.lock if a prior git was killed (`rm .git/index.lock` only if no git is running).
  3. Pass tolerateFailure=true for genuinely optional probes (e.g. rev-parse to detect a repo).
  4. Ensure git is installed and the cwd is inside the intended worktree.

Example fix

// before
function git(args: string[], cwd: string, tolerateFailure = false): string {
  const result = spawnSync('git', args, { cwd, stdio: 'pipe', timeout: 30_000 });
  const stdout = result.stdout?.toString().trim() ?? '';
  const stderr = result.stderr?.toString().trim() ?? '';
  if (result.status !== 0 && !tolerateFailure) {
    throw new Error(`git ${args.join(' ')} failed (exit ${result.status}): ${stderr || stdout}`);
  }
  return stdout;
}

// after: distinguish spawn error, signal, and missing git
function git(args: string[], cwd: string, tolerateFailure = false): string {
  const result = spawnSync('git', args, { cwd, stdio: 'pipe', timeout: 30_000 });
  const stdout = result.stdout?.toString().trim() ?? '';
  const stderr = result.stderr?.toString().trim() ?? '';
  if (result.error) { if (tolerateFailure) return ''; throw new Error(`git ${args.join(' ')} failed to spawn: ${result.error.message}`); }
  if (result.signal) { if (tolerateFailure) return ''; throw new Error(`git ${args.join(' ')} killed by ${result.signal}`); }
  if (result.status !== 0 && !tolerateFailure) {
    throw new Error(`git ${args.join(' ')} failed (exit ${result.status}): ${stderr || stdout}`);
  }
  return stdout;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Probe repo-ness with tolerateFailure instead of letting the helper throw.
function isGitRepo(cwd: string): boolean {
  return git(['rev-parse', '--is-inside-work-tree'], cwd, true).trim() === 'true';
}

// usage: gate the real command on this
if (!isGitRepo(cwd)) throw new Error(`not a git repo: ${cwd}`);

Try / catch

try {
  git(['checkout', branch], cwd);
} catch (e) {
  if (/index.lock/.test(String(e?.message ?? ''))) {
    // transient lock — one bounded retry after releasing
    fs.rmSync(path.join(cwd, '.git', 'index.lock'), { force: true });
    git(['checkout', branch], cwd);
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Any git(args, cwd) call in worktree.ts where the command fails: rev-parse outside a repo, checkout of a missing branch, add/commit with a lock conflict, status with a corrupt index, or a 30s timeout (status null, signal SIGTERM). Also when cwd does not exist or git is absent.

Common situations: Running the harvest/worktree flow in a directory that is not a git repo; a concurrent `git` process holding .git/index.lock; a shallow clone missing the requested ref; git not installed in a minimal container; branch names with shell-unsafe characters passed unquoted (note: argv form mitigates this).

Related errors


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