feat(HAI-034): use humanized worktree names instead of task-ID-based directories

- Add worktree-names module with adjective-animal name generator (generateWorktreeName)
- Update executor to assign humanized random worktree names and support dependency worktree reuse
- Remove task-ID fallback paths from merger and store; add shared worktree cleanup via findWorktreeUser
- Remove worktree pool module and related scheduler/integration tests
- Update dashboard worktree label tests and add JSDoc documentation for worktree naming
This commit is contained in:
Dustin Byrne
2026-03-25 23:09:25 -04:00
parent 74379bdbce
commit b9d7c89905
7 changed files with 233 additions and 36 deletions

View File

@@ -1,5 +1,4 @@
import { execSync } from "node:child_process";
import { join } from "node:path";
import { existsSync } from "node:fs";
import type { TaskStore, Task, MergeResult } from "@hai/core";
import { createHaiAgent } from "./pi.js";
@@ -101,7 +100,7 @@ export async function aiMergeTask(
}
const branch = `hai/${taskId.toLowerCase()}`;
const worktreePath = task.worktree || join(rootDir, ".worktrees", taskId);
const worktreePath = task.worktree;
const result: MergeResult = {
task,
branch,
@@ -110,6 +109,10 @@ export async function aiMergeTask(
branchDeleted: false,
};
if (!worktreePath) {
console.warn(`[merger] ${taskId}: no worktree path set — skipping worktree cleanup`);
}
// 2. Check branch exists
try {
execSync(`git rev-parse --verify "${branch}"`, {
@@ -221,17 +224,23 @@ export async function aiMergeTask(
session.dispose();
}
// 7. Clean up worktree — release to pool if recycling is enabled
if (existsSync(worktreePath)) {
const settings = await store.getSettings();
if (options.pool && settings.recycleWorktrees) {
// Detach HEAD so the task branch can be deleted in step 8
try {
execSync("git checkout --detach", { cwd: worktreePath, stdio: "pipe" });
} catch { /* non-fatal — prepareForTask will reset HEAD on next acquire */ }
options.pool.release(worktreePath);
// 7. Delete branch (always per-task, regardless of worktree sharing)
try {
execSync(`git branch -d "${branch}"`, { cwd: rootDir, stdio: "pipe" });
result.branchDeleted = true;
} catch {
try {
execSync(`git branch -D "${branch}"`, { cwd: rootDir, stdio: "pipe" });
result.branchDeleted = true;
} catch { /* non-fatal */ }
}
// 8. Clean up worktree — only if no other non-done task still references it
if (worktreePath && existsSync(worktreePath)) {
const otherUser = await findWorktreeUser(store, worktreePath, taskId);
if (otherUser) {
console.log(`[merger] Worktree retained — still needed by ${otherUser}`);
result.worktreeRemoved = false;
console.log(`[merger] Worktree returned to pool: ${worktreePath}`);
} else {
try {
execSync(`git worktree remove "${worktreePath}" --force`, {
@@ -243,17 +252,6 @@ export async function aiMergeTask(
}
}
// 8. Delete branch
try {
execSync(`git branch -d "${branch}"`, { cwd: rootDir, stdio: "pipe" });
result.branchDeleted = true;
} catch {
try {
execSync(`git branch -D "${branch}"`, { cwd: rootDir, stdio: "pipe" });
result.branchDeleted = true;
} catch { /* non-fatal */ }
}
// 9. Move task to done
await completeTask(store, taskId, result);
return result;