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",
|
"anthropic-effort",
|
||||||
"hashline-read-enhancer",
|
"hashline-read-enhancer",
|
||||||
"read-image-resizer",
|
"read-image-resizer",
|
||||||
|
"todo-description-override",
|
||||||
])
|
])
|
||||||
|
|
||||||
export type HookName = z.infer<typeof HookNameSchema>
|
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 { createHashlineReadEnhancerHook } from "./hashline-read-enhancer";
|
||||||
export { createJsonErrorRecoveryHook, JSON_ERROR_TOOL_EXCLUDE_LIST, JSON_ERROR_PATTERNS, JSON_ERROR_REMINDER } from "./json-error-recovery";
|
export { createJsonErrorRecoveryHook, JSON_ERROR_TOOL_EXCLUDE_LIST, JSON_ERROR_PATTERNS, JSON_ERROR_REMINDER } from "./json-error-recovery";
|
||||||
export { createReadImageResizerHook } from "./read-image-resizer"
|
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"
|
||||||
@@ -71,5 +71,9 @@ export function createPluginInterface(args: {
|
|||||||
ctx,
|
ctx,
|
||||||
hooks,
|
hooks,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
"tool.definition": async (input, output) => {
|
||||||
|
await hooks.todoDescriptionOverride?.["tool.definition"]?.(input, output)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
createHashlineReadEnhancerHook,
|
createHashlineReadEnhancerHook,
|
||||||
createReadImageResizerHook,
|
createReadImageResizerHook,
|
||||||
createJsonErrorRecoveryHook,
|
createJsonErrorRecoveryHook,
|
||||||
|
createTodoDescriptionOverrideHook,
|
||||||
} from "../../hooks"
|
} from "../../hooks"
|
||||||
import {
|
import {
|
||||||
getOpenCodeVersion,
|
getOpenCodeVersion,
|
||||||
@@ -35,6 +36,7 @@ export type ToolGuardHooks = {
|
|||||||
hashlineReadEnhancer: ReturnType<typeof createHashlineReadEnhancerHook> | null
|
hashlineReadEnhancer: ReturnType<typeof createHashlineReadEnhancerHook> | null
|
||||||
jsonErrorRecovery: ReturnType<typeof createJsonErrorRecoveryHook> | null
|
jsonErrorRecovery: ReturnType<typeof createJsonErrorRecoveryHook> | null
|
||||||
readImageResizer: ReturnType<typeof createReadImageResizerHook> | null
|
readImageResizer: ReturnType<typeof createReadImageResizerHook> | null
|
||||||
|
todoDescriptionOverride: ReturnType<typeof createTodoDescriptionOverrideHook> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createToolGuardHooks(args: {
|
export function createToolGuardHooks(args: {
|
||||||
@@ -111,6 +113,10 @@ export function createToolGuardHooks(args: {
|
|||||||
? safeHook("read-image-resizer", () => createReadImageResizerHook(ctx))
|
? safeHook("read-image-resizer", () => createReadImageResizerHook(ctx))
|
||||||
: null
|
: null
|
||||||
|
|
||||||
|
const todoDescriptionOverride = isHookEnabled("todo-description-override")
|
||||||
|
? safeHook("todo-description-override", () => createTodoDescriptionOverrideHook())
|
||||||
|
: null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commentChecker,
|
commentChecker,
|
||||||
toolOutputTruncator,
|
toolOutputTruncator,
|
||||||
@@ -123,5 +129,6 @@ export function createToolGuardHooks(args: {
|
|||||||
hashlineReadEnhancer,
|
hashlineReadEnhancer,
|
||||||
jsonErrorRecovery,
|
jsonErrorRecovery,
|
||||||
readImageResizer,
|
readImageResizer,
|
||||||
|
todoDescriptionOverride,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user