{"record":{"id":"0d68f1866351b8b7","repo":"garrytan/gstack","slug":"git-args-join-failed-exit-result-status","errorCode":null,"errorMessage":"git ${args.join(' ')} failed (exit ${result.status}): ${stderr || stdout}","messagePattern":"git (.+?) failed \\(exit (.+?)\\): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"lib/worktree.ts","lineNumber":60,"sourceCode":"    // Skip symlinks to avoid infinite recursion (e.g., .claude/skills/gstack → repo root)\n    if (entry.isSymbolicLink()) continue;\n    const srcPath = path.join(src, entry.name);\n    const destPath = path.join(dest, entry.name);\n    if (entry.isDirectory()) {\n      copyDirSync(srcPath, destPath);\n    } else {\n      fs.copyFileSync(srcPath, destPath);\n    }\n  }\n}\n\n/** Run a git command and return stdout. Throws on failure unless tolerateFailure is set. */\nfunction git(args: string[], cwd: string, tolerateFailure = false): string {\n  const result = spawnSync('git', args, { cwd, stdio: 'pipe', timeout: 30_000 });\n  const stdout = result.stdout?.toString().trim() ?? '';\n  const stderr = result.stderr?.toString().trim() ?? '';\n  if (result.status !== 0 && !tolerateFailure) {\n    throw new Error(`git ${args.join(' ')} failed (exit ${result.status}): ${stderr || stdout}`);\n  }\n  return stdout;\n}\n\n// --- Dedup index ---\n\ninterface DedupIndex {\n  hashes: Record<string, string>; // hash → first-seen runId\n}\n\nfunction getDedupPath(): string {\n  return path.join(os.homedir(), '.gstack-dev', 'harvests', 'dedup.json');\n}\n\nfunction loadDedupIndex(): DedupIndex {\n  try {\n    const raw = fs.readFileSync(getDedupPath(), 'utf-8');\n    return JSON.parse(raw);","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/lib/worktree.ts#L42-L78","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before\nfunction git(args: string[], cwd: string, tolerateFailure = false): string {\n  const result = spawnSync('git', args, { cwd, stdio: 'pipe', timeout: 30_000 });\n  const stdout = result.stdout?.toString().trim() ?? '';\n  const stderr = result.stderr?.toString().trim() ?? '';\n  if (result.status !== 0 && !tolerateFailure) {\n    throw new Error(`git ${args.join(' ')} failed (exit ${result.status}): ${stderr || stdout}`);\n  }\n  return stdout;\n}\n\n// after: distinguish spawn error, signal, and missing git\nfunction git(args: string[], cwd: string, tolerateFailure = false): string {\n  const result = spawnSync('git', args, { cwd, stdio: 'pipe', timeout: 30_000 });\n  const stdout = result.stdout?.toString().trim() ?? '';\n  const stderr = result.stderr?.toString().trim() ?? '';\n  if (result.error) { if (tolerateFailure) return ''; throw new Error(`git ${args.join(' ')} failed to spawn: ${result.error.message}`); }\n  if (result.signal) { if (tolerateFailure) return ''; throw new Error(`git ${args.join(' ')} killed by ${result.signal}`); }\n  if (result.status !== 0 && !tolerateFailure) {\n    throw new Error(`git ${args.join(' ')} failed (exit ${result.status}): ${stderr || stdout}`);\n  }\n  return stdout;\n}","handlingStrategy":"try-catch","validationCode":"// Probe repo-ness with tolerateFailure instead of letting the helper throw.\nfunction isGitRepo(cwd: string): boolean {\n  return git(['rev-parse', '--is-inside-work-tree'], cwd, true).trim() === 'true';\n}\n\n// usage: gate the real command on this\nif (!isGitRepo(cwd)) throw new Error(`not a git repo: ${cwd}`);","typeGuard":null,"tryCatchPattern":"try {\n  git(['checkout', branch], cwd);\n} catch (e) {\n  if (/index.lock/.test(String(e?.message ?? ''))) {\n    // transient lock — one bounded retry after releasing\n    fs.rmSync(path.join(cwd, '.git', 'index.lock'), { force: true });\n    git(['checkout', branch], cwd);\n  } else {\n    throw e;\n  }\n}","preventionTips":["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`)."],"tags":["git","subprocess","spawnsync","worktree","harvest"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}