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
- Reproduce the exact command from the message in the same cwd to see git's real error.
- Remove a stale .git/index.lock if a prior git was killed (`rm .git/index.lock` only if no git is running).
- Pass tolerateFailure=true for genuinely optional probes (e.g. rev-parse to detect a repo).
- 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
- Confirm cwd is inside a git worktree before issuing mutations.
- Use tolerateFailure=true for optional probes (rev-parse, symbolic-ref).
- Never hold a git process open across a long async hop (it leaves index.lock).
- Ensure git is installed in minimal containers (`git --version`).
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
- gbrain sources remove ${id} failed: ${rm.stderr || rm.stdout
- gbrain sources add ${id} failed: ${add.stderr || add.stdout
- browse ${args[0] || "unknown"} exited ${exitCode}: ${stderr}
- pdftotext failed on ${pdfPath}: ${err.message}
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/0d68f1866351b8b7.
Report an issue: GitHub.