diff --git a/src/config/schema/hooks.ts b/src/config/schema/hooks.ts index 4acc37584..f2e84853b 100644 --- a/src/config/schema/hooks.ts +++ b/src/config/schema/hooks.ts @@ -51,6 +51,7 @@ export const HookNameSchema = z.enum([ "anthropic-effort", "hashline-read-enhancer", "read-image-resizer", + "todo-description-override", ]) export type HookName = z.infer diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 77b2c3c13..73fbb652d 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -52,3 +52,4 @@ export { createWriteExistingFileGuardHook } from "./write-existing-file-guard"; export { createHashlineReadEnhancerHook } from "./hashline-read-enhancer"; export { createJsonErrorRecoveryHook, JSON_ERROR_TOOL_EXCLUDE_LIST, JSON_ERROR_PATTERNS, JSON_ERROR_REMINDER } from "./json-error-recovery"; export { createReadImageResizerHook } from "./read-image-resizer" +export { createTodoDescriptionOverrideHook } from "./todo-description-override" diff --git a/src/hooks/todo-description-override/description.ts b/src/hooks/todo-description-override/description.ts new file mode 100644 index 000000000..dc85fc7bf --- /dev/null +++ b/src/hooks/todo-description-override/description.ts @@ -0,0 +1,28 @@ +export const TODOWRITE_DESCRIPTION = `Use this tool to create and manage a structured task list for tracking progress on multi-step work. + +## Todo Format (MANDATORY) + +Each todo title MUST encode four elements: WHERE, WHY, HOW, and EXPECTED RESULT. + +Format: "[WHERE] [HOW] to [WHY] — expect [RESULT]" + +GOOD: +- "src/utils/validation.ts: Add validateEmail() for input sanitization — returns boolean" +- "UserService.create(): Call validateEmail() before DB insert — rejects invalid emails with 400" +- "validation.test.ts: Add test for missing @ sign — expect validateEmail('foo') to return false" + +BAD: +- "Implement email validation" (where? how? what result?) +- "Add dark mode" (this is a feature, not a todo) +- "Fix auth" (what file? what changes? what's expected?) + +## Granularity Rules + +Each todo MUST be a single atomic action completable in 1-3 tool calls. If it needs more, split it. + +**Size test**: Can you complete this todo by editing one file or running one command? If not, it's too big. + +## Task Management +- One in_progress at a time. Complete it before starting the next. +- Mark completed immediately after finishing each item. +- Skip this tool for single trivial tasks (one-step, obvious action).` diff --git a/src/hooks/todo-description-override/hook.ts b/src/hooks/todo-description-override/hook.ts new file mode 100644 index 000000000..5b850d741 --- /dev/null +++ b/src/hooks/todo-description-override/hook.ts @@ -0,0 +1,14 @@ +import { TODOWRITE_DESCRIPTION } from "./description" + +export function createTodoDescriptionOverrideHook() { + return { + "tool.definition": async ( + input: { toolID: string }, + output: { description: string; parameters: unknown }, + ) => { + if (input.toolID === "todowrite") { + output.description = TODOWRITE_DESCRIPTION + } + }, + } +} diff --git a/src/hooks/todo-description-override/index.test.ts b/src/hooks/todo-description-override/index.test.ts new file mode 100644 index 000000000..374b13a98 --- /dev/null +++ b/src/hooks/todo-description-override/index.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from "bun:test" +import { createTodoDescriptionOverrideHook } from "./hook" +import { TODOWRITE_DESCRIPTION } from "./description" + +describe("createTodoDescriptionOverrideHook", () => { + describe("#given hook is created", () => { + describe("#when tool.definition is called with todowrite", () => { + it("#then should override the description", async () => { + const hook = createTodoDescriptionOverrideHook() + const output = { description: "original description", parameters: {} } + + await hook["tool.definition"]({ toolID: "todowrite" }, output) + + expect(output.description).toBe(TODOWRITE_DESCRIPTION) + }) + }) + + describe("#when tool.definition is called with non-todowrite tool", () => { + it("#then should not modify the description", async () => { + const hook = createTodoDescriptionOverrideHook() + const output = { description: "original description", parameters: {} } + + await hook["tool.definition"]({ toolID: "bash" }, output) + + expect(output.description).toBe("original description") + }) + }) + + describe("#when tool.definition is called with TodoWrite (case-insensitive)", () => { + it("#then should not override for different casing since OpenCode sends lowercase", async () => { + const hook = createTodoDescriptionOverrideHook() + const output = { description: "original description", parameters: {} } + + await hook["tool.definition"]({ toolID: "TodoWrite" }, output) + + expect(output.description).toBe("original description") + }) + }) + }) +}) diff --git a/src/hooks/todo-description-override/index.ts b/src/hooks/todo-description-override/index.ts new file mode 100644 index 000000000..9a8ac2796 --- /dev/null +++ b/src/hooks/todo-description-override/index.ts @@ -0,0 +1 @@ +export { createTodoDescriptionOverrideHook } from "./hook" diff --git a/src/plugin-interface.ts b/src/plugin-interface.ts index 0cff2ee7c..b37c9d417 100644 --- a/src/plugin-interface.ts +++ b/src/plugin-interface.ts @@ -71,5 +71,9 @@ export function createPluginInterface(args: { ctx, hooks, }), + + "tool.definition": async (input, output) => { + await hooks.todoDescriptionOverride?.["tool.definition"]?.(input, output) + }, } } diff --git a/src/plugin/hooks/create-tool-guard-hooks.ts b/src/plugin/hooks/create-tool-guard-hooks.ts index 3e909e785..1c79f6949 100644 --- a/src/plugin/hooks/create-tool-guard-hooks.ts +++ b/src/plugin/hooks/create-tool-guard-hooks.ts @@ -14,6 +14,7 @@ import { createHashlineReadEnhancerHook, createReadImageResizerHook, createJsonErrorRecoveryHook, + createTodoDescriptionOverrideHook, } from "../../hooks" import { getOpenCodeVersion, @@ -35,6 +36,7 @@ export type ToolGuardHooks = { hashlineReadEnhancer: ReturnType | null jsonErrorRecovery: ReturnType | null readImageResizer: ReturnType | null + todoDescriptionOverride: ReturnType | null } export function createToolGuardHooks(args: { @@ -111,6 +113,10 @@ export function createToolGuardHooks(args: { ? safeHook("read-image-resizer", () => createReadImageResizerHook(ctx)) : null + const todoDescriptionOverride = isHookEnabled("todo-description-override") + ? safeHook("todo-description-override", () => createTodoDescriptionOverrideHook()) + : null + return { commentChecker, toolOutputTruncator, @@ -123,5 +129,6 @@ export function createToolGuardHooks(args: { hashlineReadEnhancer, jsonErrorRecovery, readImageResizer, + todoDescriptionOverride, } }