feat(HAI-078): add configurable task prefix to settings
- Add taskPrefix field to Settings type and use it in allocateId - Update store listTasks and handleFsChange to support any task prefix - Update triage duplicate regex to match configurable prefix - Add Task Prefix input to dashboard SettingsModal UI - Add tests for prefix-aware task allocation and settings modal
This commit is contained in:
@@ -401,6 +401,42 @@ describe("TaskStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Task prefix tests ──────────────────────────────────────────
|
||||
|
||||
describe("taskPrefix setting", () => {
|
||||
it("default prefix produces HAI-001 IDs", async () => {
|
||||
const task = await store.createTask({ description: "Default prefix" });
|
||||
expect(task.id).toBe("HAI-001");
|
||||
});
|
||||
|
||||
it("custom prefix produces PROJ-001 IDs", async () => {
|
||||
await store.updateSettings({ taskPrefix: "PROJ" });
|
||||
const task = await store.createTask({ description: "Custom prefix" });
|
||||
expect(task.id).toBe("PROJ-001");
|
||||
});
|
||||
|
||||
it("prefix change mid-stream continues sequence", async () => {
|
||||
const t1 = await store.createTask({ description: "First" });
|
||||
const t2 = await store.createTask({ description: "Second" });
|
||||
expect(t1.id).toBe("HAI-001");
|
||||
expect(t2.id).toBe("HAI-002");
|
||||
|
||||
await store.updateSettings({ taskPrefix: "PROJ" });
|
||||
const t3 = await store.createTask({ description: "Third" });
|
||||
expect(t3.id).toBe("PROJ-003");
|
||||
});
|
||||
|
||||
it("listTasks returns tasks regardless of prefix", async () => {
|
||||
await store.createTask({ description: "HAI task" });
|
||||
await store.updateSettings({ taskPrefix: "PROJ" });
|
||||
await store.createTask({ description: "PROJ task" });
|
||||
|
||||
const tasks = await store.listTasks();
|
||||
expect(tasks).toHaveLength(2);
|
||||
expect(tasks.map((t) => t.id).sort()).toEqual(["HAI-001", "PROJ-002"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("agent log persistence", () => {
|
||||
it("appendAgentLog creates agent.log and getAgentLogs reads it back", async () => {
|
||||
const task = await createTestTask();
|
||||
|
||||
@@ -160,7 +160,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
private async allocateId(): Promise<string> {
|
||||
return this.withConfigLock(async () => {
|
||||
const config = await this.readConfig();
|
||||
const id = `HAI-${String(config.nextId).padStart(3, "0")}`;
|
||||
const prefix = config.settings?.taskPrefix || "HAI";
|
||||
const id = `${prefix}-${String(config.nextId).padStart(3, "0")}`;
|
||||
config.nextId++;
|
||||
await this.writeConfig(config);
|
||||
return id;
|
||||
@@ -235,7 +236,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
const tasks: Task[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name.startsWith("HAI-")) {
|
||||
if (entry.isDirectory() && /^[A-Z]+-\d+$/.test(entry.name)) {
|
||||
try {
|
||||
tasks.push(await this.readTaskJson(join(this.tasksDir, entry.name)));
|
||||
} catch {
|
||||
@@ -655,7 +656,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
const taskId = normalizedParts[0];
|
||||
const file = normalizedParts[normalizedParts.length - 1];
|
||||
if (file !== "task.json") return;
|
||||
if (!taskId.startsWith("HAI-")) return;
|
||||
if (!/^[A-Z]+-\d+$/.test(taskId)) return;
|
||||
|
||||
const fullPath = join(this.tasksDir, taskId, "task.json");
|
||||
|
||||
|
||||
@@ -87,6 +87,10 @@ export interface Settings {
|
||||
* of being deleted. New tasks acquire a warm worktree from the pool,
|
||||
* preserving build caches (node_modules, target/, dist/). Default: false. */
|
||||
recycleWorktrees?: boolean;
|
||||
/** Prefix for generated task IDs (e.g. `"HAI"` produces `HAI-001`).
|
||||
* Defaults to `"HAI"`. Only affects new tasks — existing tasks retain
|
||||
* their original IDs. */
|
||||
taskPrefix?: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: Settings = {
|
||||
@@ -97,6 +101,7 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
autoMerge: false,
|
||||
worktreeInitCommand: undefined,
|
||||
recycleWorktrees: false,
|
||||
taskPrefix: undefined,
|
||||
};
|
||||
|
||||
export interface BoardConfig {
|
||||
|
||||
Reference in New Issue
Block a user