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:
YeonGyu-Kim
2026-03-18 11:42:57 +09:00
parent 1d5652dfa9
commit 55ac653eaa
8 changed files with 96 additions and 0 deletions
+1
View File
@@ -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"
@@ -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).`
@@ -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
}
},
}
}
@@ -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")
})
})
})
})
@@ -0,0 +1 @@
export { createTodoDescriptionOverrideHook } from "./hook"