feat(HAI-033): add dependency-chain worktree reuse and conditional cleanup

- Add executor logic to resolve and reuse dependency worktrees for warm build caches
- Add reuseWorktree method that creates a new branch in an existing worktree
- Add findWorktreeUser helper to check if a worktree is shared across tasks
- Update executor and merger cleanup to skip worktree removal when still in use
- Add comprehensive tests for worktree reuse and conditional cleanup paths
This commit is contained in:
Dustin Byrne
2026-03-25 22:51:43 -04:00
parent 2fc6cba0b5
commit b41e215246
4 changed files with 554 additions and 17 deletions

View File

@@ -44,6 +44,27 @@ git commit -m "feat(HAI-003): add user profile page" -m "- Add /profile route wi
Do NOT use generic messages like "merge branch" or "resolve conflicts".
Base the message on the ACTUAL work done in the branch commits.`;
/**
* Check if any non-done task (other than `excludeTaskId`) references the given
* worktree path. Returns the first matching task ID, or null if the worktree
* is safe to remove. Used by both the merger and executor cleanup to avoid
* deleting worktrees that are shared across dependent tasks.
*/
export async function findWorktreeUser(
store: TaskStore,
worktreePath: string,
excludeTaskId: string,
): Promise<string | null> {
const tasks = await store.listTasks();
for (const t of tasks) {
if (t.id === excludeTaskId) continue;
if (t.worktree === worktreePath && t.column !== "done") {
return t.id;
}
}
return null;
}
export interface MergerOptions {
/** Called with agent text output */
onAgentText?: (delta: string) => void;
@@ -190,18 +211,7 @@ export async function aiMergeTask(
session.dispose();
}
// 7. Clean up worktree
if (existsSync(worktreePath)) {
try {
execSync(`git worktree remove "${worktreePath}" --force`, {
cwd: rootDir,
stdio: "pipe",
});
result.worktreeRemoved = true;
} catch { /* non-fatal */ }
}
// 8. Delete branch
// 7. Delete branch (always per-task, regardless of worktree sharing)
try {
execSync(`git branch -d "${branch}"`, { cwd: rootDir, stdio: "pipe" });
result.branchDeleted = true;
@@ -212,6 +222,23 @@ export async function aiMergeTask(
} catch { /* non-fatal */ }
}
// 8. Clean up worktree — only if no other non-done task still references it
if (existsSync(worktreePath)) {
const otherUser = await findWorktreeUser(store, worktreePath, taskId);
if (otherUser) {
console.log(`[merger] Worktree retained — still needed by ${otherUser}`);
result.worktreeRemoved = false;
} else {
try {
execSync(`git worktree remove "${worktreePath}" --force`, {
cwd: rootDir,
stdio: "pipe",
});
result.worktreeRemoved = true;
} catch { /* non-fatal */ }
}
}
// 9. Move task to done
await completeTask(store, taskId, result);
return result;