feat(task-system): add experimental task system with Claude Code spec alignment (#1415)
* feat(hooks): add tasks-todowrite-disabler hook to block TodoRead/TodoWrite Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(task-tools): add parallel execution guidance to descriptions Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * refactor(index): migrate task system to experimental.task_system flag Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * docs: update AGENTS.md for experimental task system Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(task-tests): align test field names with Claude Code spec (subject, blockedBy, addBlockedBy) * fix: address Cubic review feedback Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix: add optional chaining for tasksTodowriteDisabler null check --------- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -37,3 +37,4 @@ export { createStopContinuationGuardHook, type StopContinuationGuard } from "./s
|
||||
export { createCompactionContextInjector, type SummarizeContext } from "./compaction-context-injector";
|
||||
export { createUnstableAgentBabysitterHook } from "./unstable-agent-babysitter";
|
||||
export { createPreemptiveCompactionHook } from "./preemptive-compaction";
|
||||
export { createTasksTodowriteDisablerHook } from "./tasks-todowrite-disabler";
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const HOOK_NAME = "tasks-todowrite-disabler"
|
||||
export const BLOCKED_TOOLS = ["TodoWrite", "TodoRead"]
|
||||
export const REPLACEMENT_MESSAGE = `TodoRead/TodoWrite are disabled because experimental.task_system is enabled.
|
||||
Use the new task tools instead:
|
||||
- TaskCreate: Create new tasks with auto-generated IDs
|
||||
- TaskUpdate: Update task status, add dependencies
|
||||
- TaskList: List active tasks with dependency info
|
||||
- TaskGet: Get full task details
|
||||
|
||||
IMPORTANT: 1 task = 1 delegate_task. Maximize parallel execution by running independent tasks concurrently.`
|
||||
@@ -0,0 +1,137 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
|
||||
const { createTasksTodowriteDisablerHook } = await import("./index")
|
||||
|
||||
describe("tasks-todowrite-disabler", () => {
|
||||
describe("when experimental.task_system is enabled", () => {
|
||||
test("should block TodoWrite tool", async () => {
|
||||
// given
|
||||
const hook = createTasksTodowriteDisablerHook({ experimental: { task_system: true } })
|
||||
const input = {
|
||||
tool: "TodoWrite",
|
||||
sessionID: "test-session",
|
||||
callID: "call-1",
|
||||
}
|
||||
const output = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
// when / then
|
||||
await expect(
|
||||
hook["tool.execute.before"](input, output)
|
||||
).rejects.toThrow("TodoRead/TodoWrite are disabled")
|
||||
})
|
||||
|
||||
test("should block TodoRead tool", async () => {
|
||||
// given
|
||||
const hook = createTasksTodowriteDisablerHook({ experimental: { task_system: true } })
|
||||
const input = {
|
||||
tool: "TodoRead",
|
||||
sessionID: "test-session",
|
||||
callID: "call-1",
|
||||
}
|
||||
const output = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
// when / then
|
||||
await expect(
|
||||
hook["tool.execute.before"](input, output)
|
||||
).rejects.toThrow("TodoRead/TodoWrite are disabled")
|
||||
})
|
||||
|
||||
test("should not block other tools", async () => {
|
||||
// given
|
||||
const hook = createTasksTodowriteDisablerHook({ experimental: { task_system: true } })
|
||||
const input = {
|
||||
tool: "Read",
|
||||
sessionID: "test-session",
|
||||
callID: "call-1",
|
||||
}
|
||||
const output = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
// when / then
|
||||
await expect(
|
||||
hook["tool.execute.before"](input, output)
|
||||
).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("when experimental.task_system is disabled or undefined", () => {
|
||||
test("should not block TodoWrite when flag is false", async () => {
|
||||
// given
|
||||
const hook = createTasksTodowriteDisablerHook({ experimental: { task_system: false } })
|
||||
const input = {
|
||||
tool: "TodoWrite",
|
||||
sessionID: "test-session",
|
||||
callID: "call-1",
|
||||
}
|
||||
const output = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
// when / then
|
||||
await expect(
|
||||
hook["tool.execute.before"](input, output)
|
||||
).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
test("should not block TodoWrite when experimental is undefined", async () => {
|
||||
// given
|
||||
const hook = createTasksTodowriteDisablerHook({})
|
||||
const input = {
|
||||
tool: "TodoWrite",
|
||||
sessionID: "test-session",
|
||||
callID: "call-1",
|
||||
}
|
||||
const output = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
// when / then
|
||||
await expect(
|
||||
hook["tool.execute.before"](input, output)
|
||||
).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
test("should not block TodoRead when flag is false", async () => {
|
||||
// given
|
||||
const hook = createTasksTodowriteDisablerHook({ experimental: { task_system: false } })
|
||||
const input = {
|
||||
tool: "TodoRead",
|
||||
sessionID: "test-session",
|
||||
callID: "call-1",
|
||||
}
|
||||
const output = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
// when / then
|
||||
await expect(
|
||||
hook["tool.execute.before"](input, output)
|
||||
).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("error message content", () => {
|
||||
test("should include replacement message with task tools info", async () => {
|
||||
// given
|
||||
const hook = createTasksTodowriteDisablerHook({ experimental: { task_system: true } })
|
||||
const input = {
|
||||
tool: "TodoWrite",
|
||||
sessionID: "test-session",
|
||||
callID: "call-1",
|
||||
}
|
||||
const output = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
// when / then
|
||||
await expect(
|
||||
hook["tool.execute.before"](input, output)
|
||||
).rejects.toThrow(/TaskCreate|TaskUpdate|TaskList|TaskGet/)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import { BLOCKED_TOOLS, REPLACEMENT_MESSAGE } from "./constants";
|
||||
|
||||
export interface TasksTodowriteDisablerConfig {
|
||||
experimental?: {
|
||||
task_system?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export function createTasksTodowriteDisablerHook(
|
||||
config: TasksTodowriteDisablerConfig,
|
||||
) {
|
||||
const isTaskSystemEnabled = config.experimental?.task_system ?? false;
|
||||
|
||||
return {
|
||||
"tool.execute.before": async (
|
||||
input: { tool: string; sessionID: string; callID: string },
|
||||
output: { args: Record<string, unknown> },
|
||||
) => {
|
||||
if (!isTaskSystemEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolName = input.tool as string;
|
||||
if (BLOCKED_TOOLS.some((blocked) => blocked.toLowerCase() === toolName.toLowerCase())) {
|
||||
throw new Error(REPLACEMENT_MESSAGE);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user