feat(hooks): add todo-description-override hook to enforce atomic todo format
Override TodoWrite description via tool.definition hook to require WHERE/WHY/HOW/RESULT in each todo title and enforce 1-3 tool call granularity.
This commit is contained in:
@@ -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<typeof HookNameSchema>
|
||||
|
||||
@@ -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"
|
||||
|
||||
28
src/hooks/todo-description-override/description.ts
Normal file
28
src/hooks/todo-description-override/description.ts
Normal file
@@ -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).`
|
||||
14
src/hooks/todo-description-override/hook.ts
Normal file
14
src/hooks/todo-description-override/hook.ts
Normal file
@@ -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
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
40
src/hooks/todo-description-override/index.test.ts
Normal file
40
src/hooks/todo-description-override/index.test.ts
Normal file
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
1
src/hooks/todo-description-override/index.ts
Normal file
1
src/hooks/todo-description-override/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { createTodoDescriptionOverrideHook } from "./hook"
|
||||
@@ -71,5 +71,9 @@ export function createPluginInterface(args: {
|
||||
ctx,
|
||||
hooks,
|
||||
}),
|
||||
|
||||
"tool.definition": async (input, output) => {
|
||||
await hooks.todoDescriptionOverride?.["tool.definition"]?.(input, output)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
createHashlineReadEnhancerHook,
|
||||
createReadImageResizerHook,
|
||||
createJsonErrorRecoveryHook,
|
||||
createTodoDescriptionOverrideHook,
|
||||
} from "../../hooks"
|
||||
import {
|
||||
getOpenCodeVersion,
|
||||
@@ -35,6 +36,7 @@ export type ToolGuardHooks = {
|
||||
hashlineReadEnhancer: ReturnType<typeof createHashlineReadEnhancerHook> | null
|
||||
jsonErrorRecovery: ReturnType<typeof createJsonErrorRecoveryHook> | null
|
||||
readImageResizer: ReturnType<typeof createReadImageResizerHook> | null
|
||||
todoDescriptionOverride: ReturnType<typeof createTodoDescriptionOverrideHook> | 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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user