fix(HAI-092): use generateWorktreeName for fresh worktree directories

- Replace task ID with generateWorktreeName for worktree directory paths
- Worktrees now use human-friendly adjective-noun names (e.g. swift-falcon)
- Add tests for fresh worktree naming, task ID exclusion, and resumed task reuse
This commit is contained in:
Dustin Byrne
2026-03-26 01:16:41 -04:00
parent 2ab34bc11a
commit fc57042522
2 changed files with 76 additions and 1 deletions

View File

@@ -32,6 +32,7 @@ import { createHaiAgent } from "./pi.js";
import { execSync } from "node:child_process";
import { findWorktreeUser, aiMergeTask } from "./merger.js";
import { WorktreePool } from "./worktree-pool.js";
import { generateWorktreeName } from "./worktree-names.js";
import type { Column, Task, TaskDetail } from "@hai/core";
const mockedCreateHaiAgent = vi.mocked(createHaiAgent);
@@ -349,6 +350,78 @@ describe("TaskExecutor worktreeInitCommand", () => {
});
});
describe("TaskExecutor worktree naming", () => {
const makeTask = (id = "HAI-030", worktree?: string) => ({
id,
title: "Test",
description: "Test",
column: "in-progress" as const,
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...(worktree ? { worktree } : {}),
});
const mockedGenerateWorktreeName = vi.mocked(generateWorktreeName);
beforeEach(() => {
vi.clearAllMocks();
mockedExistsSync.mockReturnValue(false);
mockedGenerateWorktreeName.mockReturnValue("swift-falcon");
mockedCreateHaiAgent.mockResolvedValue({
session: {
prompt: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn(),
},
} as any);
});
it("uses generateWorktreeName for fresh worktree directories", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
await executor.execute(makeTask());
// The worktree path stored should use the generated name, not the task ID
expect(store.updateTask).toHaveBeenCalledWith("HAI-030", {
worktree: "/tmp/test/.worktrees/swift-falcon",
});
expect(mockedGenerateWorktreeName).toHaveBeenCalledWith("/tmp/test");
});
it("does NOT use task ID as worktree directory name for fresh worktrees", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
await executor.execute(makeTask("HAI-099"));
// Verify the worktree path does NOT contain the task ID
const updateCalls = store.updateTask.mock.calls;
const worktreeUpdate = updateCalls.find(
(call: any[]) => call[1]?.worktree !== undefined,
);
expect(worktreeUpdate).toBeDefined();
expect(worktreeUpdate![1].worktree).not.toContain("HAI-099");
expect(worktreeUpdate![1].worktree).toContain("swift-falcon");
});
it("reuses stored worktree path for resumed tasks", async () => {
const existingPath = "/tmp/test/.worktrees/calm-river";
mockedExistsSync.mockReturnValue(true);
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
await executor.execute(makeTask("HAI-031", existingPath));
// Should NOT generate a new name — reuse the stored path
expect(mockedGenerateWorktreeName).not.toHaveBeenCalled();
});
});
describe("TaskExecutor worktree pool integration", () => {
const makeTask = (id = "HAI-020") => ({
id,

View File

@@ -271,7 +271,9 @@ export class TaskExecutor {
// Create or reuse worktree — try pool first when recycling is enabled
const branchName = `hai/${task.id.toLowerCase()}`;
let worktreePath = task.worktree || join(this.rootDir, ".worktrees", task.id);
// Use generateWorktreeName for human-friendly directory names (adjective-noun pattern)
// instead of task.id, so worktrees are named like ".worktrees/swift-falcon"
let worktreePath = task.worktree || join(this.rootDir, ".worktrees", generateWorktreeName(this.rootDir));
let isResume = existsSync(worktreePath);
let acquiredFromPool = false;
const settings = await this.store.getSettings();