fix(engine): recoverable worktree failures + prevent nested/gitlink worktrees
Fixes two classes of task failures found while investigating stuck in-review tasks FN-2165 (worktree base ref missing) and FN-2152 (stray .tmp-fn-2152 gitlink accidentally committed via merger amend). FN-2165 — stale baseBranch: - resolveWorktreeStartPoint now returns null instead of throwing NonRetryableWorktreeError when the stored baseBranch is gone. Caller clears task.baseBranch and falls back to branching from the default base (HEAD) so the task self-heals instead of failing permanently. - New TaskStore.clearStaleBaseBranchReferences() nulls baseBranch on any dependent task when its upstream branch is deleted. Wired into cleanupBranchForTask (archive/delete), merger branch cleanup, self-healing orphan-branch sweep, executor dep-abort and conflict-cleanup paths, and stale-branch recovery. Nested worktrees: - assertWorktreePathNotNested guard in tryCreateWorktree refuses to create a worktree inside another registered worktree (previously produced pathological paths like .worktrees/green-finch/.worktrees/amber-panda when rootDir pointed at a worktree instead of the main repo). Context-overflow recovery (FN-2182 class): - Reduced-prompt retry budget raised from 1 → 3 within the same session. - Adds a fresh-session requeue path when same-session retries still overflow: task moves back to todo with worktree retained, bounded by computeRecoveryDecision / MAX_RECOVERY_RETRIES. Prevents late-step context exhaustion from becoming terminal. Gitlink prevention (FN-2152 class): - .gitignore now excludes .tmp-fn-* and .tmp-kb-* so stray worktrees at the repo root cannot be captured by git add -A. - Merger amend flow now scans staged entries for 160000 gitlinks and unstages them with a loud warning; the project uses no submodules, so any such entry is a bug (this is how f8f90f26 landed in HEAD as .tmp-fn-2152). Tests: new coverage for baseBranch fallback, nested-worktree guard, and clearStaleBaseBranchReferences. Full engine + core + dashboard + cli suites pass (15349 tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -761,6 +761,31 @@ async function amendMergeCommitWithFixes(
|
||||
await execAsync("git add -A", { cwd: rootDir });
|
||||
}
|
||||
|
||||
// FN-2152 regression guard: `git add -A` at the repo root will capture any
|
||||
// directory with a `.git` file/dir (nested worktree, orphaned checkout) as
|
||||
// a 160000 gitlink. The project uses no submodules, so any staged gitlink
|
||||
// is a bug. Unstage such entries before amending so they cannot land in
|
||||
// HEAD. Loud log so operators can clean up the offending directory.
|
||||
const { stdout: staged } = await execAsync("git diff --cached --raw", {
|
||||
cwd: rootDir,
|
||||
encoding: "utf-8",
|
||||
});
|
||||
const gitlinkPaths: string[] = [];
|
||||
for (const line of staged.split("\n")) {
|
||||
// raw format: `:<srcMode> <dstMode> <srcSha> <dstSha> <status>\t<path>`
|
||||
const match = line.match(/^:\d{6} 160000 [^\t]+\t(.+)$/);
|
||||
if (match) gitlinkPaths.push(match[1]);
|
||||
}
|
||||
for (const path of gitlinkPaths) {
|
||||
mergerLog.warn(`${taskId}: refusing to stage gitlink "${path}" (project uses no submodules — likely a nested worktree). Unstaging.`);
|
||||
try {
|
||||
await execAsync(`git reset HEAD -- "${path}"`, { cwd: rootDir });
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
mergerLog.warn(`${taskId}: failed to unstage gitlink "${path}": ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are staged changes to amend
|
||||
const { stdout: finalStaged } = await execAsync("git diff --cached --name-only", {
|
||||
cwd: rootDir,
|
||||
@@ -2107,6 +2132,22 @@ export async function aiMergeTask(
|
||||
} catch { /* non-fatal */ }
|
||||
}
|
||||
|
||||
if (result.branchDeleted) {
|
||||
// FN-2165 regression guard: if any other task had this branch stored as
|
||||
// its baseBranch (common when a dependent task was dispatched off a
|
||||
// conflict-suffixed branch), null it so the dependent task doesn't
|
||||
// hard-fail at worktree creation once this branch is gone.
|
||||
try {
|
||||
const cleared = store.clearStaleBaseBranchReferences([branch], taskId);
|
||||
if (cleared.length > 0) {
|
||||
mergerLog.log(`${taskId}: cleared stale baseBranch on ${cleared.length} dependent task(s): ${cleared.join(", ")}`);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
mergerLog.warn(`${taskId}: failed to clear stale baseBranch references: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Clean up worktree
|
||||
if (worktreePath && existsSync(worktreePath)) {
|
||||
const otherUser = await findWorktreeUser(store, worktreePath, taskId);
|
||||
|
||||
Reference in New Issue
Block a user