Switch to double quotes and reformat codebase with Biome
This commit is contained in:
@@ -4,10 +4,7 @@ import { spawnSync } from "node:child_process";
|
||||
import { appendFileSync, existsSync, openSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { FileTaskStore } from "./task-store.js";
|
||||
import {
|
||||
computeExecutionOrder,
|
||||
buildSortedChildrenMap,
|
||||
} from "./execution-order.js";
|
||||
import { computeExecutionOrder, buildSortedChildrenMap } from "./execution-order.js";
|
||||
import { resolvePackageVersion } from "../server/package-version.js";
|
||||
import type { AgentType, ModelName, Task } from "./types.js";
|
||||
|
||||
@@ -164,7 +161,7 @@ Acceptance Criteria vs Body Guidance:
|
||||
"git status shows clean working tree" to each subtask's criteria.
|
||||
|
||||
This makes process requirements verifiable at each step, not just at the end.
|
||||
`
|
||||
`,
|
||||
);
|
||||
|
||||
program
|
||||
@@ -177,7 +174,12 @@ program
|
||||
.option("--assignee <agent>", "Agent to assign (claude or codex)")
|
||||
.option("--draft", "Create as draft (not actionable)")
|
||||
.option("-p, --priority <n>", "Priority (lower number = higher priority)")
|
||||
.option("-a, --accept <criterion>", "Acceptance criterion (repeatable)", (val: string, prev: string[]) => prev.concat(val), [] as string[])
|
||||
.option(
|
||||
"-a, --accept <criterion>",
|
||||
"Acceptance criterion (repeatable)",
|
||||
(val: string, prev: string[]) => prev.concat(val),
|
||||
[] as string[],
|
||||
)
|
||||
.action(async (title, opts) => {
|
||||
let body = opts.body ?? "";
|
||||
if (body === "-") {
|
||||
@@ -186,9 +188,7 @@ program
|
||||
|
||||
const task = await store.create(title, {
|
||||
body,
|
||||
deps: opts.deps
|
||||
? opts.deps.split(",").map((s: string) => s.trim())
|
||||
: [],
|
||||
deps: opts.deps ? opts.deps.split(",").map((s: string) => s.trim()) : [],
|
||||
parentId: opts.parent,
|
||||
status: opts.draft ? "draft" : "open",
|
||||
assignee: opts.assignee as AgentType | undefined,
|
||||
@@ -207,9 +207,7 @@ program
|
||||
.option("--roots", "Show only root tasks (no parent)")
|
||||
.action(async (opts) => {
|
||||
const tasks = await store.list();
|
||||
let filtered = opts.status
|
||||
? tasks.filter((t) => t.status === opts.status)
|
||||
: tasks;
|
||||
let filtered = opts.status ? tasks.filter((t) => t.status === opts.status) : tasks;
|
||||
|
||||
if (opts.roots) {
|
||||
filtered = filtered.filter((t) => !t.parentId);
|
||||
@@ -220,7 +218,9 @@ program
|
||||
const assignee = t.assignee ? ` @${t.assignee}` : "";
|
||||
const parent = t.parentId ? ` ^${t.parentId}` : "";
|
||||
const priority = t.priority !== undefined ? ` !${t.priority}` : "";
|
||||
process.stdout.write(`${t.id} [${t.status}] ${t.title}${priority}${assignee}${parent}${deps}\n`);
|
||||
process.stdout.write(
|
||||
`${t.id} [${t.status}] ${t.title}${priority}${assignee}${parent}${deps}\n`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -326,10 +326,7 @@ program
|
||||
.option("--flat", "Show flat list instead of tree")
|
||||
.option("-d, --depth <n>", "Limit tree depth (0 = root only)")
|
||||
.action(async (scopeId: string | undefined, opts) => {
|
||||
const { timeline, orderMap, blocked } = await computeExecutionOrder(
|
||||
store,
|
||||
scopeId
|
||||
);
|
||||
const { timeline, orderMap, blocked } = await computeExecutionOrder(store, scopeId);
|
||||
const allTasks = await store.list();
|
||||
const taskMap = new Map(allTasks.map((t) => [t.id, t]));
|
||||
const maxDepth = opts.depth !== undefined ? parseInt(opts.depth, 10) : Infinity;
|
||||
@@ -347,9 +344,7 @@ program
|
||||
const num = String(idx + 1).padStart(3, " ");
|
||||
const mark = t.status === "done" ? "✓" : " ";
|
||||
const deps = formatDeps(t);
|
||||
process.stdout.write(
|
||||
`${mark}${num}. ${priority}${t.id} ${t.title}${assignee}${deps}\n`
|
||||
);
|
||||
process.stdout.write(`${mark}${num}. ${priority}${t.id} ${t.title}${assignee}${deps}\n`);
|
||||
};
|
||||
|
||||
for (let i = 0; i < timeline.length; i++) {
|
||||
@@ -370,30 +365,23 @@ program
|
||||
|
||||
const sortedChildrenMap = buildSortedChildrenMap(allTasks, orderMap);
|
||||
|
||||
const printTask = (
|
||||
task: Task,
|
||||
prefix: string,
|
||||
connector: string,
|
||||
depth: number
|
||||
) => {
|
||||
const printTask = (task: Task, prefix: string, connector: string, depth: number) => {
|
||||
const assignee = task.assignee ? ` @${task.assignee}` : "";
|
||||
const priority =
|
||||
task.priority !== undefined ? ` !${task.priority}` : "";
|
||||
const priority = task.priority !== undefined ? ` !${task.priority}` : "";
|
||||
const mark = task.status === "done" ? "✓ " : " ";
|
||||
const deps = formatDeps(task);
|
||||
process.stdout.write(
|
||||
`${mark}${prefix}${connector}${task.id} [${task.status}] ${task.title}${priority}${assignee}${deps}\n`
|
||||
`${mark}${prefix}${connector}${task.id} [${task.status}] ${task.title}${priority}${assignee}${deps}\n`,
|
||||
);
|
||||
};
|
||||
|
||||
// Print root
|
||||
const rootAssignee = root.assignee ? ` @${root.assignee}` : "";
|
||||
const rootPriority =
|
||||
root.priority !== undefined ? ` !${root.priority}` : "";
|
||||
const rootPriority = root.priority !== undefined ? ` !${root.priority}` : "";
|
||||
const rootMark = root.status === "done" ? "✓ " : " ";
|
||||
const rootDeps = formatDeps(root);
|
||||
process.stdout.write(
|
||||
`${rootMark}${root.id} [${root.status}] ${root.title}${rootPriority}${rootAssignee}${rootDeps}\n`
|
||||
`${rootMark}${root.id} [${root.status}] ${root.title}${rootPriority}${rootAssignee}${rootDeps}\n`,
|
||||
);
|
||||
|
||||
// Recursively print children in execution order
|
||||
@@ -450,7 +438,12 @@ program
|
||||
.option("-p, --priority <n>", "Priority (lower number = higher priority)")
|
||||
.option("-s, --status <status>", "Set status (draft, open, in_progress, done, failed)")
|
||||
.option("--clear-acceptance", "Clear all acceptance criteria (combine with -a to replace)")
|
||||
.option("-a, --accept <criterion>", "Add acceptance criterion (repeatable)", (val: string, prev: string[]) => prev.concat(val), [] as string[])
|
||||
.option(
|
||||
"-a, --accept <criterion>",
|
||||
"Add acceptance criterion (repeatable)",
|
||||
(val: string, prev: string[]) => prev.concat(val),
|
||||
[] as string[],
|
||||
)
|
||||
.action(async (id, opts) => {
|
||||
const task = await store.get(id);
|
||||
if (!task) {
|
||||
@@ -478,7 +471,9 @@ program
|
||||
|
||||
if (opts.status) {
|
||||
if (!VALID_STATUSES.includes(opts.status)) {
|
||||
process.stderr.write(`Invalid status: ${opts.status}. Must be one of: ${VALID_STATUSES.join(", ")}\n`);
|
||||
process.stderr.write(
|
||||
`Invalid status: ${opts.status}. Must be one of: ${VALID_STATUSES.join(", ")}\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
changes.status = opts.status as Task["status"];
|
||||
@@ -547,7 +542,9 @@ program
|
||||
for (const child of children) {
|
||||
const assignee = child.assignee ? ` @${child.assignee}` : "";
|
||||
const priority = child.priority !== undefined ? ` !${child.priority}` : "";
|
||||
process.stdout.write(`${child.id} [${child.status}] ${child.title}${priority}${assignee}\n`);
|
||||
process.stdout.write(
|
||||
`${child.id} [${child.status}] ${child.title}${priority}${assignee}\n`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -644,7 +641,7 @@ function getAgentConfig(modelStr: string): AgentConfig {
|
||||
function runAgentWithModel(
|
||||
prompt: string,
|
||||
modelStr: string,
|
||||
logFile: string
|
||||
logFile: string,
|
||||
): { success: boolean; output: string } {
|
||||
const config = getAgentConfig(modelStr);
|
||||
let args: string[];
|
||||
@@ -1070,7 +1067,11 @@ program
|
||||
.description("Run agent loop on tasks with planner/worker/judge")
|
||||
.option("--plan", "Enable planner agent")
|
||||
.option("--planner <model>", "Planner model (default: gpt-5.2)", "gpt-5.2")
|
||||
.option("--worker-model <model>", "Worker model (default: sonnet). For GPT models, append effort: gpt-5.2-high", "sonnet")
|
||||
.option(
|
||||
"--worker-model <model>",
|
||||
"Worker model (default: sonnet). For GPT models, append effort: gpt-5.2-high",
|
||||
"sonnet",
|
||||
)
|
||||
.option("--judge-model <model>", "Judge model (default: haiku)", "haiku")
|
||||
.option("--max-iterations <n>", "Max worker/judge iterations per task (0 = no limit)", "0")
|
||||
.option("-w, --watch", "Keep running and wait for new tasks")
|
||||
@@ -1092,7 +1093,10 @@ program
|
||||
process.stdout.write(`Log: ${logFile}\n`);
|
||||
process.stdout.write("\n");
|
||||
|
||||
log(logFile, `Started with planner=${enablePlanner ? plannerModel : "disabled"} worker=${baseWorkerModel} judge=${judgeModel} maxIter=${maxIterations} scope=${scopeId || "all"}`);
|
||||
log(
|
||||
logFile,
|
||||
`Started with planner=${enablePlanner ? plannerModel : "disabled"} worker=${baseWorkerModel} judge=${judgeModel} maxIter=${maxIterations} scope=${scopeId || "all"}`,
|
||||
);
|
||||
|
||||
const runPlanner = async (task: Task, reason: string): Promise<boolean> => {
|
||||
log(logFile, `[PLANNER] Running ${plannerModel} (${reason})...`);
|
||||
@@ -1121,12 +1125,17 @@ program
|
||||
// First check for in_progress tasks (resuming from crash)
|
||||
const allTasks = await store.list();
|
||||
let candidates = scopeId
|
||||
? [await store.get(scopeId), ...(await store.getDescendants(scopeId))].filter(Boolean) as Task[]
|
||||
? ([await store.get(scopeId), ...(await store.getDescendants(scopeId))].filter(
|
||||
Boolean,
|
||||
) as Task[])
|
||||
: allTasks;
|
||||
|
||||
const inProgress = candidates.filter((t) => t.status === "in_progress");
|
||||
if (inProgress.length > 0) {
|
||||
log(logFile, `Found ${inProgress.length} in_progress task(s) from previous run, resuming...`);
|
||||
log(
|
||||
logFile,
|
||||
`Found ${inProgress.length} in_progress task(s) from previous run, resuming...`,
|
||||
);
|
||||
for (const t of inProgress) {
|
||||
await store.update(t.id, { status: "open" });
|
||||
log(logFile, `Reset ${t.id} to open`);
|
||||
@@ -1172,7 +1181,8 @@ program
|
||||
let consecutiveNotDone = 0;
|
||||
// Track steering notes on scope (not leaf task) since that's where user adds them
|
||||
const scopeTaskForSteer = scopeId ? await store.get(scopeId) : null;
|
||||
let lastSeenSteerCount = scopeTaskForSteer?.notes.filter(n => n.content.startsWith("STEER:")).length ?? 0;
|
||||
let lastSeenSteerCount =
|
||||
scopeTaskForSteer?.notes.filter((n) => n.content.startsWith("STEER:")).length ?? 0;
|
||||
|
||||
while ((maxIterations === 0 || iteration <= maxIterations) && !taskDone) {
|
||||
const iterLabel = maxIterations === 0 ? `${iteration}` : `${iteration}/${maxIterations}`;
|
||||
@@ -1182,12 +1192,14 @@ program
|
||||
if (enablePlanner && scopeId) {
|
||||
const freshScope = await store.get(scopeId);
|
||||
if (freshScope) {
|
||||
const currentSteerCount = freshScope.notes.filter(n => n.content.startsWith("STEER:")).length;
|
||||
const currentSteerCount = freshScope.notes.filter((n) =>
|
||||
n.content.startsWith("STEER:"),
|
||||
).length;
|
||||
if (currentSteerCount > lastSeenSteerCount) {
|
||||
const newSteers = freshScope.notes
|
||||
.filter(n => n.content.startsWith("STEER:"))
|
||||
.filter((n) => n.content.startsWith("STEER:"))
|
||||
.slice(lastSeenSteerCount)
|
||||
.map(n => n.content.replace(/^STEER:\s*/, ""));
|
||||
.map((n) => n.content.replace(/^STEER:\s*/, ""));
|
||||
log(logFile, `[STEER] New steering note detected - triggering planner`);
|
||||
lastSeenSteerCount = currentSteerCount;
|
||||
const reason = `User steering: ${newSteers.join("; ")}`;
|
||||
@@ -1236,7 +1248,10 @@ program
|
||||
}
|
||||
// Only replan after 5 consecutive NOT_DONEs - use scope for bird's eye view
|
||||
if (enablePlanner && consecutiveNotDone >= 5 && scopeId) {
|
||||
log(logFile, `[JUDGE] ${consecutiveNotDone} consecutive NOT_DONE on ${task.id} - triggering planner`);
|
||||
log(
|
||||
logFile,
|
||||
`[JUDGE] ${consecutiveNotDone} consecutive NOT_DONE on ${task.id} - triggering planner`,
|
||||
);
|
||||
const scopeTask = await store.get(scopeId);
|
||||
if (scopeTask) {
|
||||
const reason = `task ${task.id} "${task.title}" got ${consecutiveNotDone} consecutive NOT_DONE`;
|
||||
|
||||
@@ -100,7 +100,7 @@ function makeTask(
|
||||
deps?: string[];
|
||||
priority?: number;
|
||||
created?: string;
|
||||
} = {}
|
||||
} = {},
|
||||
): Task {
|
||||
return {
|
||||
id,
|
||||
@@ -202,17 +202,15 @@ describe("computeExecutionOrder", () => {
|
||||
parentId: "epic",
|
||||
priority: 0,
|
||||
created: "2024-01-02",
|
||||
})
|
||||
);
|
||||
store.addTask(
|
||||
makeTask("p1-task1", { parentId: "phase1", created: "2024-01-03" })
|
||||
}),
|
||||
);
|
||||
store.addTask(makeTask("p1-task1", { parentId: "phase1", created: "2024-01-03" }));
|
||||
store.addTask(
|
||||
makeTask("p1-task2", {
|
||||
parentId: "phase1",
|
||||
deps: ["p1-task1"],
|
||||
created: "2024-01-04",
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// Phase 2 - depends on phase1
|
||||
@@ -221,14 +219,10 @@ describe("computeExecutionOrder", () => {
|
||||
parentId: "epic",
|
||||
deps: ["phase1"],
|
||||
created: "2024-01-05",
|
||||
})
|
||||
);
|
||||
store.addTask(
|
||||
makeTask("p2-task1", { parentId: "phase2", created: "2024-01-06" })
|
||||
);
|
||||
store.addTask(
|
||||
makeTask("p2-task2", { parentId: "phase2", created: "2024-01-07" })
|
||||
}),
|
||||
);
|
||||
store.addTask(makeTask("p2-task1", { parentId: "phase2", created: "2024-01-06" }));
|
||||
store.addTask(makeTask("p2-task2", { parentId: "phase2", created: "2024-01-07" }));
|
||||
|
||||
// Cleanup - no priority, depends on phase2
|
||||
store.addTask(
|
||||
@@ -236,7 +230,7 @@ describe("computeExecutionOrder", () => {
|
||||
parentId: "epic",
|
||||
deps: ["phase2"],
|
||||
created: "2024-01-08",
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await computeExecutionOrder(store, "epic");
|
||||
@@ -274,22 +268,20 @@ describe("computeExecutionOrder", () => {
|
||||
*/
|
||||
|
||||
store.addTask(makeTask("parent", { created: "2024-01-01" }));
|
||||
store.addTask(
|
||||
makeTask("child-a", { parentId: "parent", created: "2024-01-02" })
|
||||
);
|
||||
store.addTask(makeTask("child-a", { parentId: "parent", created: "2024-01-02" }));
|
||||
store.addTask(
|
||||
makeTask("child-b", {
|
||||
parentId: "parent",
|
||||
priority: 0,
|
||||
created: "2024-01-03",
|
||||
})
|
||||
}),
|
||||
);
|
||||
store.addTask(
|
||||
makeTask("child-c", {
|
||||
parentId: "parent",
|
||||
priority: 1,
|
||||
created: "2024-01-04",
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await computeExecutionOrder(store, "parent");
|
||||
@@ -300,12 +292,8 @@ describe("computeExecutionOrder", () => {
|
||||
});
|
||||
|
||||
it("should place done tasks first in historical order", async () => {
|
||||
store.addTask(
|
||||
makeTask("done-later", { status: "done", created: "2024-01-03" })
|
||||
);
|
||||
store.addTask(
|
||||
makeTask("done-first", { status: "done", created: "2024-01-01" })
|
||||
);
|
||||
store.addTask(makeTask("done-later", { status: "done", created: "2024-01-03" }));
|
||||
store.addTask(makeTask("done-first", { status: "done", created: "2024-01-01" }));
|
||||
store.addTask(makeTask("pending", { created: "2024-01-02" }));
|
||||
|
||||
const result = await computeExecutionOrder(store);
|
||||
@@ -318,9 +306,7 @@ describe("computeExecutionOrder", () => {
|
||||
it("should mark tasks with unresolvable deps as blocked", async () => {
|
||||
// b depends on external-dep which doesn't exist in scope
|
||||
store.addTask(makeTask("a", { created: "2024-01-01" }));
|
||||
store.addTask(
|
||||
makeTask("b", { deps: ["external-dep"], created: "2024-01-02" })
|
||||
);
|
||||
store.addTask(makeTask("b", { deps: ["external-dep"], created: "2024-01-02" }));
|
||||
|
||||
const result = await computeExecutionOrder(store);
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export interface ExecutionOrderResult {
|
||||
*/
|
||||
export async function computeExecutionOrder(
|
||||
store: TaskStore,
|
||||
scopeId?: string
|
||||
scopeId?: string,
|
||||
): Promise<ExecutionOrderResult> {
|
||||
const allTasks = await store.list();
|
||||
const taskMap = new Map(allTasks.map((t) => [t.id, t]));
|
||||
@@ -57,13 +57,9 @@ export async function computeExecutionOrder(
|
||||
|
||||
// Simulate execution: track done status
|
||||
// Include all done tasks (not just in scope) for dep resolution
|
||||
const simDone = new Set(
|
||||
allTasks.filter((t) => t.status === "done").map((t) => t.id)
|
||||
);
|
||||
const simDone = new Set(allTasks.filter((t) => t.status === "done").map((t) => t.id));
|
||||
const remaining = new Set(
|
||||
candidates
|
||||
.filter((t) => t.status === "open" || t.status === "in_progress")
|
||||
.map((t) => t.id)
|
||||
candidates.filter((t) => t.status === "open" || t.status === "in_progress").map((t) => t.id),
|
||||
);
|
||||
|
||||
const isReady = (taskId: string): boolean => {
|
||||
@@ -72,9 +68,7 @@ export async function computeExecutionOrder(
|
||||
// All deps done (deps can be outside scope)
|
||||
const depsOk = task.deps.every((depId) => simDone.has(depId));
|
||||
// All children done (only consider children in scope)
|
||||
const children = (childrenMap.get(taskId) ?? []).filter((c) =>
|
||||
candidateIds.has(c.id)
|
||||
);
|
||||
const children = (childrenMap.get(taskId) ?? []).filter((c) => candidateIds.has(c.id));
|
||||
const childrenOk = children.every((c) => simDone.has(c.id));
|
||||
return depsOk && childrenOk;
|
||||
};
|
||||
@@ -119,7 +113,7 @@ export async function computeExecutionOrder(
|
||||
*/
|
||||
export function buildSortedChildrenMap(
|
||||
tasks: Task[],
|
||||
orderMap: Map<string, number>
|
||||
orderMap: Map<string, number>,
|
||||
): Map<string, Task[]> {
|
||||
const childrenMap = new Map<string, Task[]>();
|
||||
|
||||
|
||||
@@ -60,9 +60,7 @@ describe("FileTaskStore", () => {
|
||||
body: "This is a **long** body\n\nWith multiple lines.",
|
||||
});
|
||||
|
||||
expect(task.body).toBe(
|
||||
"This is a **long** body\n\nWith multiple lines."
|
||||
);
|
||||
expect(task.body).toBe("This is a **long** body\n\nWith multiple lines.");
|
||||
});
|
||||
|
||||
it("creates a task with parentId", async () => {
|
||||
@@ -73,9 +71,9 @@ describe("FileTaskStore", () => {
|
||||
});
|
||||
|
||||
it("throws when creating task with non-existent parent", async () => {
|
||||
await expect(
|
||||
store.create("Child", { parentId: "nonexistent" })
|
||||
).rejects.toThrow("Parent task not found");
|
||||
await expect(store.create("Child", { parentId: "nonexistent" })).rejects.toThrow(
|
||||
"Parent task not found",
|
||||
);
|
||||
});
|
||||
|
||||
it("creates a task with assignee", async () => {
|
||||
@@ -174,11 +172,7 @@ describe("FileTaskStore", () => {
|
||||
const tasks = await store.list();
|
||||
|
||||
expect(tasks).toHaveLength(3);
|
||||
expect(tasks.map((t) => t.title).sort()).toEqual([
|
||||
"Task 1",
|
||||
"Task 2",
|
||||
"Task 3",
|
||||
]);
|
||||
expect(tasks.map((t) => t.title).sort()).toEqual(["Task 1", "Task 2", "Task 3"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -227,9 +221,7 @@ describe("FileTaskStore", () => {
|
||||
});
|
||||
|
||||
it("throws for non-existent task", async () => {
|
||||
await expect(
|
||||
store.update("nonexistent", { title: "New" })
|
||||
).rejects.toThrow();
|
||||
await expect(store.update("nonexistent", { title: "New" })).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("replaces acceptance criteria", async () => {
|
||||
@@ -907,9 +899,7 @@ describe("FileTaskStore", () => {
|
||||
const tree = await store.getDepTree(root.id);
|
||||
|
||||
expect(tree).toHaveLength(3);
|
||||
expect(tree.map((t) => t.id).sort()).toEqual(
|
||||
[shared.id, left.id, right.id].sort()
|
||||
);
|
||||
expect(tree.map((t) => t.id).sort()).toEqual([shared.id, left.id, right.id].sort());
|
||||
});
|
||||
|
||||
it("handles circular deps gracefully", async () => {
|
||||
@@ -1048,9 +1038,7 @@ describe("FileTaskStore", () => {
|
||||
const children = await store.getChildren(parent.id);
|
||||
|
||||
expect(children).toHaveLength(2);
|
||||
expect(children.map((c) => c.id).sort()).toEqual(
|
||||
[child1.id, child2.id].sort()
|
||||
);
|
||||
expect(children.map((c) => c.id).sort()).toEqual([child1.id, child2.id].sort());
|
||||
});
|
||||
|
||||
it("does not return grandchildren", async () => {
|
||||
@@ -1100,10 +1088,7 @@ describe("FileTaskStore", () => {
|
||||
|
||||
const children = await store.getChildren(parent.id);
|
||||
|
||||
expect(children.map((c) => c.id)).toEqual([
|
||||
withPriority.id,
|
||||
noPriority.id,
|
||||
]);
|
||||
expect(children.map((c) => c.id)).toEqual([withPriority.id, noPriority.id]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1131,16 +1116,14 @@ describe("FileTaskStore", () => {
|
||||
it("throws for non-existent task", async () => {
|
||||
const parent = await store.create("Parent");
|
||||
|
||||
await expect(store.setParent("nonexistent", parent.id)).rejects.toThrow(
|
||||
"Task not found"
|
||||
);
|
||||
await expect(store.setParent("nonexistent", parent.id)).rejects.toThrow("Task not found");
|
||||
});
|
||||
|
||||
it("throws for non-existent parent", async () => {
|
||||
const task = await store.create("Task");
|
||||
|
||||
await expect(store.setParent(task.id, "nonexistent")).rejects.toThrow(
|
||||
"Parent task not found"
|
||||
"Parent task not found",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1148,7 +1131,7 @@ describe("FileTaskStore", () => {
|
||||
const task = await store.create("Task");
|
||||
|
||||
await expect(store.setParent(task.id, task.id)).rejects.toThrow(
|
||||
"Task cannot be its own parent"
|
||||
"Task cannot be its own parent",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1156,9 +1139,7 @@ describe("FileTaskStore", () => {
|
||||
const parent = await store.create("Parent");
|
||||
const child = await store.create("Child", { parentId: parent.id });
|
||||
|
||||
await expect(store.setParent(parent.id, child.id)).rejects.toThrow(
|
||||
"circular reference"
|
||||
);
|
||||
await expect(store.setParent(parent.id, child.id)).rejects.toThrow("circular reference");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { readdir, readFile, writeFile, mkdir, unlink } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import type {
|
||||
Task,
|
||||
TaskStore,
|
||||
CreateTaskOptions,
|
||||
TaskStatus,
|
||||
AgentType,
|
||||
} from "./types.js";
|
||||
import type { Task, TaskStore, CreateTaskOptions, TaskStatus, AgentType } from "./types.js";
|
||||
|
||||
function generateId(): string {
|
||||
return randomBytes(4).toString("hex");
|
||||
@@ -106,7 +100,7 @@ function parseTask(content: string): Task {
|
||||
const notesSection = fileBody.match(/## Notes\n([\s\S]*?)$/);
|
||||
if (notesSection) {
|
||||
const noteMatches = notesSection[1].matchAll(
|
||||
/\*\*(\d{4}-\d{2}-\d{2}T[\d:.Z]+)\*\*\n\n([\s\S]*?)(?=\n\*\*\d{4}|$)/g
|
||||
/\*\*(\d{4}-\d{2}-\d{2}T[\d:.Z]+)\*\*\n\n([\s\S]*?)(?=\n\*\*\d{4}|$)/g,
|
||||
);
|
||||
for (const match of noteMatches) {
|
||||
notes.push({
|
||||
@@ -118,13 +112,9 @@ function parseTask(content: string): Task {
|
||||
|
||||
// Parse acceptance criteria
|
||||
const acceptanceCriteria: string[] = [];
|
||||
const criteriaSection = fileBody.match(
|
||||
/## Acceptance Criteria\n\n([\s\S]*?)(?=\n## Notes|$)/
|
||||
);
|
||||
const criteriaSection = fileBody.match(/## Acceptance Criteria\n\n([\s\S]*?)(?=\n## Notes|$)/);
|
||||
if (criteriaSection) {
|
||||
const criteriaMatches = criteriaSection[1].matchAll(
|
||||
/- \[[ x]\] (.+)$/gm
|
||||
);
|
||||
const criteriaMatches = criteriaSection[1].matchAll(/- \[[ x]\] (.+)$/gm);
|
||||
for (const match of criteriaMatches) {
|
||||
acceptanceCriteria.push(match[1].trim());
|
||||
}
|
||||
@@ -267,9 +257,7 @@ export class FileTaskStore implements TaskStore {
|
||||
|
||||
async getChildren(id: string): Promise<Task[]> {
|
||||
const allTasks = await this.list();
|
||||
return allTasks
|
||||
.filter((t) => t.parentId === id)
|
||||
.sort(sortByPriorityThenCreated);
|
||||
return allTasks.filter((t) => t.parentId === id).sort(sortByPriorityThenCreated);
|
||||
}
|
||||
|
||||
async getDescendants(id: string): Promise<Task[]> {
|
||||
@@ -397,10 +385,7 @@ export class FileTaskStore implements TaskStore {
|
||||
return saved!;
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
changes: Partial<Omit<Task, "id" | "created">>
|
||||
): Promise<Task> {
|
||||
async update(id: string, changes: Partial<Omit<Task, "id" | "created">>): Promise<Task> {
|
||||
const task = await this.get(id);
|
||||
if (!task) {
|
||||
throw new Error(`Task not found: ${id}`);
|
||||
|
||||
@@ -2,11 +2,7 @@ export type TaskStatus = "draft" | "open" | "in_progress" | "done" | "failed";
|
||||
|
||||
export type AgentType = "claude" | "codex";
|
||||
|
||||
export type ModelName =
|
||||
| "haiku"
|
||||
| "sonnet"
|
||||
| "opus"
|
||||
| `gpt-${string}`;
|
||||
export type ModelName = "haiku" | "sonnet" | "opus" | `gpt-${string}`;
|
||||
|
||||
export interface Note {
|
||||
timestamp: string; // ISO date
|
||||
|
||||
Reference in New Issue
Block a user