refactor(HAI-116): rename kb to hai across all packages, CLI, and docs

- Rename npm packages from @kb/* to @hai/* and update all workspace references
- Rename CLI binary from kb to hai and config directory from .kb to .hai
- Update dashboard UI branding, titles, and references from kb to hai
- Update all test files, CI workflows, and documentation to reflect new naming
- Run comprehensive grep verification to ensure no stale kb references remain
This commit is contained in:
Dustin Byrne
2026-03-26 22:25:30 -04:00
parent 1c5eb494db
commit c802108a02
84 changed files with 792 additions and 1363 deletions

View File

@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
// Mock external dependencies
vi.mock("./pi.js", () => ({
createHaiAgent: vi.fn(),
createKbAgent: vi.fn(),
}));
vi.mock("node:child_process", () => ({
@@ -14,23 +14,23 @@ vi.mock("node:fs", () => ({
}));
import { aiMergeTask, findWorktreeUser } from "./merger.js";
import { createHaiAgent } from "./pi.js";
import { createKbAgent } from "./pi.js";
import { execSync } from "node:child_process";
import { type TaskStore, type Task, type MergeResult, DEFAULT_SETTINGS } from "@hai/core";
import { type TaskStore, type Task, type MergeResult, DEFAULT_SETTINGS } from "@kb/core";
const mockedCreateHaiAgent = vi.mocked(createHaiAgent);
const mockedCreateHaiAgent = vi.mocked(createKbAgent);
const mockedExecSync = vi.mocked(execSync);
const { existsSync: mockedExistsSyncRaw } = await import("node:fs");
const mockedExistsSync = vi.mocked(mockedExistsSyncRaw);
function createMockStore(taskOverrides: Partial<Task> = {}, allTasks: Task[] = []) {
const baseTask: Task = {
id: "HAI-050",
id: "KB-050",
title: "Test task",
description: "Test",
column: "in-review",
dependencies: [],
worktree: "/tmp/root/.worktrees/HAI-050",
worktree: "/tmp/root/.worktrees/KB-050",
steps: [],
currentStep: 0,
log: [],
@@ -73,27 +73,27 @@ function setupHappyPathExecSync() {
describe("findWorktreeUser", () => {
it("returns null when no other task uses the worktree", async () => {
const store = createMockStore({}, [
{ id: "HAI-050", worktree: "/tmp/wt", column: "done" } as Task,
{ id: "KB-050", worktree: "/tmp/wt", column: "done" } as Task,
]);
const result = await findWorktreeUser(store, "/tmp/wt", "HAI-050");
const result = await findWorktreeUser(store, "/tmp/wt", "KB-050");
expect(result).toBeNull();
});
it("returns task ID when another non-done task uses the worktree", async () => {
const store = createMockStore({}, [
{ id: "HAI-050", worktree: "/tmp/wt", column: "done" } as Task,
{ id: "HAI-051", worktree: "/tmp/wt", column: "in-progress" } as Task,
{ id: "KB-050", worktree: "/tmp/wt", column: "done" } as Task,
{ id: "KB-051", worktree: "/tmp/wt", column: "in-progress" } as Task,
]);
const result = await findWorktreeUser(store, "/tmp/wt", "HAI-050");
expect(result).toBe("HAI-051");
const result = await findWorktreeUser(store, "/tmp/wt", "KB-050");
expect(result).toBe("KB-051");
});
it("ignores done tasks", async () => {
const store = createMockStore({}, [
{ id: "HAI-050", worktree: "/tmp/wt", column: "done" } as Task,
{ id: "HAI-051", worktree: "/tmp/wt", column: "done" } as Task,
{ id: "KB-050", worktree: "/tmp/wt", column: "done" } as Task,
{ id: "KB-051", worktree: "/tmp/wt", column: "done" } as Task,
]);
const result = await findWorktreeUser(store, "/tmp/wt", "HAI-050");
const result = await findWorktreeUser(store, "/tmp/wt", "KB-050");
expect(result).toBeNull();
});
});
@@ -112,16 +112,16 @@ describe("aiMergeTask — conditional worktree cleanup", () => {
});
it("does NOT remove worktree when another task references the same path", async () => {
const worktreePath = "/tmp/root/.worktrees/HAI-050";
const worktreePath = "/tmp/root/.worktrees/KB-050";
const store = createMockStore(
{ id: "HAI-050", worktree: worktreePath },
{ id: "KB-050", worktree: worktreePath },
[
{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task,
{ id: "HAI-051", worktree: worktreePath, column: "in-progress" } as Task,
{ id: "KB-050", worktree: worktreePath, column: "in-review" } as Task,
{ id: "KB-051", worktree: worktreePath, column: "in-progress" } as Task,
],
);
const result = await aiMergeTask(store, "/tmp/root", "HAI-050");
const result = await aiMergeTask(store, "/tmp/root", "KB-050");
// Worktree should NOT be removed
const removeCall = mockedExecSync.mock.calls.find(
@@ -132,15 +132,15 @@ describe("aiMergeTask — conditional worktree cleanup", () => {
});
it("removes worktree when no other task references it", async () => {
const worktreePath = "/tmp/root/.worktrees/HAI-050";
const worktreePath = "/tmp/root/.worktrees/KB-050";
const store = createMockStore(
{ id: "HAI-050", worktree: worktreePath },
{ id: "KB-050", worktree: worktreePath },
[
{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task,
{ id: "KB-050", worktree: worktreePath, column: "in-review" } as Task,
],
);
const result = await aiMergeTask(store, "/tmp/root", "HAI-050");
const result = await aiMergeTask(store, "/tmp/root", "KB-050");
const removeCall = mockedExecSync.mock.calls.find(
(call) => String(call[0]).includes("worktree remove"),
@@ -150,16 +150,16 @@ describe("aiMergeTask — conditional worktree cleanup", () => {
});
it("always deletes the branch regardless of worktree sharing", async () => {
const worktreePath = "/tmp/root/.worktrees/HAI-050";
const worktreePath = "/tmp/root/.worktrees/KB-050";
const store = createMockStore(
{ id: "HAI-050", worktree: worktreePath },
{ id: "KB-050", worktree: worktreePath },
[
{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task,
{ id: "HAI-051", worktree: worktreePath, column: "in-progress" } as Task,
{ id: "KB-050", worktree: worktreePath, column: "in-review" } as Task,
{ id: "KB-051", worktree: worktreePath, column: "in-progress" } as Task,
],
);
const result = await aiMergeTask(store, "/tmp/root", "HAI-050");
const result = await aiMergeTask(store, "/tmp/root", "KB-050");
// Branch should be deleted even though worktree is shared
const branchDeleteCall = mockedExecSync.mock.calls.find(
@@ -170,16 +170,16 @@ describe("aiMergeTask — conditional worktree cleanup", () => {
});
it("result.worktreeRemoved is false when worktree is retained", async () => {
const worktreePath = "/tmp/root/.worktrees/HAI-050";
const worktreePath = "/tmp/root/.worktrees/KB-050";
const store = createMockStore(
{ id: "HAI-050", worktree: worktreePath },
{ id: "KB-050", worktree: worktreePath },
[
{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task,
{ id: "HAI-051", worktree: worktreePath, column: "todo" } as Task,
{ id: "KB-050", worktree: worktreePath, column: "in-review" } as Task,
{ id: "KB-051", worktree: worktreePath, column: "todo" } as Task,
],
);
const result = await aiMergeTask(store, "/tmp/root", "HAI-050");
const result = await aiMergeTask(store, "/tmp/root", "KB-050");
expect(result.worktreeRemoved).toBe(false);
expect(result.merged).toBe(true);
});
@@ -200,11 +200,11 @@ describe("aiMergeTask — includeTaskIdInCommit setting", () => {
it("includes task ID in system prompt by default (includeTaskIdInCommit: true)", async () => {
const store = createMockStore(
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" },
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task],
{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
[{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
);
await aiMergeTask(store, "/tmp/root", "HAI-050");
await aiMergeTask(store, "/tmp/root", "KB-050");
const agentCall = mockedCreateHaiAgent.mock.calls[0][0] as any;
expect(agentCall.systemPrompt).toContain("<type>(<scope>): <summary>");
@@ -213,15 +213,15 @@ describe("aiMergeTask — includeTaskIdInCommit setting", () => {
it("omits task ID scope in system prompt when includeTaskIdInCommit is false", async () => {
const store = createMockStore(
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" },
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task],
{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
[{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
);
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
...DEFAULT_SETTINGS,
includeTaskIdInCommit: false,
});
await aiMergeTask(store, "/tmp/root", "HAI-050");
await aiMergeTask(store, "/tmp/root", "KB-050");
const agentCall = mockedCreateHaiAgent.mock.calls[0][0] as any;
expect(agentCall.systemPrompt).toContain("<type>: <summary>");
@@ -245,17 +245,17 @@ describe("aiMergeTask — includeTaskIdInCommit setting", () => {
});
const store = createMockStore(
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" },
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task],
{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
[{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
);
await aiMergeTask(store, "/tmp/root", "HAI-050");
await aiMergeTask(store, "/tmp/root", "KB-050");
const commitCall = mockedExecSync.mock.calls.find(
(call) => String(call[0]).includes("git commit"),
);
expect(commitCall).toBeDefined();
expect(String(commitCall![0])).toContain("feat(HAI-050):");
expect(String(commitCall![0])).toContain("feat(KB-050):");
});
it("fallback commit omits task ID when includeTaskIdInCommit is false", async () => {
@@ -273,22 +273,22 @@ describe("aiMergeTask — includeTaskIdInCommit setting", () => {
});
const store = createMockStore(
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" },
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task],
{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
[{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
);
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
...DEFAULT_SETTINGS,
includeTaskIdInCommit: false,
});
await aiMergeTask(store, "/tmp/root", "HAI-050");
await aiMergeTask(store, "/tmp/root", "KB-050");
const commitCall = mockedExecSync.mock.calls.find(
(call) => String(call[0]).includes("git commit"),
);
expect(commitCall).toBeDefined();
expect(String(commitCall![0])).toContain("feat: merge");
expect(String(commitCall![0])).not.toContain("feat(HAI-050)");
expect(String(commitCall![0])).not.toContain("feat(KB-050)");
});
});
@@ -305,10 +305,10 @@ describe("aiMergeTask — model settings threading", () => {
} as any);
});
it("passes defaultProvider and defaultModelId from settings to createHaiAgent", async () => {
it("passes defaultProvider and defaultModelId from settings to createKbAgent", async () => {
const store = createMockStore(
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" },
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task],
{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
[{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
);
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
...DEFAULT_SETTINGS,
@@ -316,7 +316,7 @@ describe("aiMergeTask — model settings threading", () => {
defaultModelId: "gpt-4o",
});
await aiMergeTask(store, "/tmp/root", "HAI-050");
await aiMergeTask(store, "/tmp/root", "KB-050");
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
const opts = mockedCreateHaiAgent.mock.calls[0][0] as any;
@@ -326,11 +326,11 @@ describe("aiMergeTask — model settings threading", () => {
it("does not set model fields when settings omit them", async () => {
const store = createMockStore(
{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050" },
[{ id: "HAI-050", worktree: "/tmp/root/.worktrees/HAI-050", column: "in-review" } as Task],
{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050" },
[{ id: "KB-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
);
await aiMergeTask(store, "/tmp/root", "HAI-050");
await aiMergeTask(store, "/tmp/root", "KB-050");
const opts = mockedCreateHaiAgent.mock.calls[0][0] as any;
expect(opts.defaultProvider).toBeUndefined();
@@ -361,15 +361,15 @@ describe("aiMergeTask — agent log persistence", () => {
} as any;
});
const worktreePath = "/tmp/root/.worktrees/HAI-050";
const worktreePath = "/tmp/root/.worktrees/KB-050";
const store = createMockStore(
{ id: "HAI-050", worktree: worktreePath },
[{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task],
{ id: "KB-050", worktree: worktreePath },
[{ id: "KB-050", worktree: worktreePath, column: "in-review" } as Task],
);
await aiMergeTask(store, "/tmp/root", "HAI-050");
await aiMergeTask(store, "/tmp/root", "KB-050");
expect(store.appendAgentLog).toHaveBeenCalledWith("HAI-050", "Hello merge", "text");
expect(store.appendAgentLog).toHaveBeenCalledWith("KB-050", "Hello merge", "text");
});
it("logs tool invocations to store.appendAgentLog", async () => {
@@ -387,15 +387,15 @@ describe("aiMergeTask — agent log persistence", () => {
} as any;
});
const worktreePath = "/tmp/root/.worktrees/HAI-050";
const worktreePath = "/tmp/root/.worktrees/KB-050";
const store = createMockStore(
{ id: "HAI-050", worktree: worktreePath },
[{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task],
{ id: "KB-050", worktree: worktreePath },
[{ id: "KB-050", worktree: worktreePath, column: "in-review" } as Task],
);
await aiMergeTask(store, "/tmp/root", "HAI-050");
await aiMergeTask(store, "/tmp/root", "KB-050");
expect(store.appendAgentLog).toHaveBeenCalledWith("HAI-050", "Bash", "tool", "git status");
expect(store.appendAgentLog).toHaveBeenCalledWith("KB-050", "Bash", "tool", "git status");
});
it("still fires onAgentText callback alongside logging", async () => {
@@ -414,15 +414,15 @@ describe("aiMergeTask — agent log persistence", () => {
} as any;
});
const worktreePath = "/tmp/root/.worktrees/HAI-050";
const worktreePath = "/tmp/root/.worktrees/KB-050";
const store = createMockStore(
{ id: "HAI-050", worktree: worktreePath },
[{ id: "HAI-050", worktree: worktreePath, column: "in-review" } as Task],
{ id: "KB-050", worktree: worktreePath },
[{ id: "KB-050", worktree: worktreePath, column: "in-review" } as Task],
);
await aiMergeTask(store, "/tmp/root", "HAI-050", { onAgentText });
await aiMergeTask(store, "/tmp/root", "KB-050", { onAgentText });
expect(onAgentText).toHaveBeenCalledWith("hi");
expect(store.appendAgentLog).toHaveBeenCalledWith("HAI-050", "hi", "text");
expect(store.appendAgentLog).toHaveBeenCalledWith("KB-050", "hi", "text");
});
});