fix: self-healing stale worktree recovery in SimpleGitWorktreeManager

When git worktree add fails with "branch already used by worktree at
<path>", parse the stale path, force-remove it, and retry once. Fixes
blocked task retries where the old agent-workdirs directory still exists
on disk and git worktree prune alone can't clear the reference.
This commit is contained in:
Lukas May
2026-03-07 00:13:24 +01:00
parent c52fa86542
commit 40900a5641

View File

@@ -85,10 +85,32 @@ export class SimpleGitWorktreeManager implements WorktreeManager {
} }
// Prune stale worktree references before adding new one // Prune stale worktree references before adding new one
await this.git.raw(['worktree', 'prune']); await this.git.raw(['worktree', 'prune']);
await this.git.raw(['worktree', 'add', worktreePath, branch]); try {
await this.git.raw(['worktree', 'add', worktreePath, branch]);
} catch (err) {
const stalePath = this.parseStaleWorktreePath(err);
if (stalePath) {
log.warn({ branch, stalePath }, 'branch locked to stale worktree, force-removing and retrying');
await this.forceRemoveWorktree(stalePath);
await this.git.raw(['worktree', 'add', worktreePath, branch]);
} else {
throw err;
}
}
} else { } else {
// git worktree add -b <branch> <path> <base-branch> // git worktree add -b <branch> <path> <base-branch>
await this.git.raw(['worktree', 'add', '-b', branch, worktreePath, baseBranch]); try {
await this.git.raw(['worktree', 'add', '-b', branch, worktreePath, baseBranch]);
} catch (err) {
const stalePath = this.parseStaleWorktreePath(err);
if (stalePath) {
log.warn({ branch, stalePath }, 'branch locked to stale worktree, force-removing and retrying');
await this.forceRemoveWorktree(stalePath);
await this.git.raw(['worktree', 'add', '-b', branch, worktreePath, baseBranch]);
} else {
throw err;
}
}
} }
const worktree: Worktree = { const worktree: Worktree = {
@@ -300,6 +322,25 @@ export class SimpleGitWorktreeManager implements WorktreeManager {
} }
} }
/** Parse stale worktree path from "already used by worktree at '<path>'" git error. */
private parseStaleWorktreePath(err: unknown): string | null {
const msg = err instanceof Error ? err.message : String(err);
const match = msg.match(/already used by worktree at '([^']+)'/);
return match ? match[1] : null;
}
/** Force-remove a stale worktree (git ref + directory). */
private async forceRemoveWorktree(worktreePath: string): Promise<void> {
try {
await this.git.raw(['worktree', 'remove', '--force', worktreePath]);
} catch {
// Corrupted state fallback: delete directory and prune
const { rm } = await import('node:fs/promises');
await rm(worktreePath, { recursive: true, force: true });
await this.git.raw(['worktree', 'prune']);
}
}
/** /**
* Parse the porcelain output of git worktree list. * Parse the porcelain output of git worktree list.
*/ */