Summary
todo_write registers validateRawTodoArguments as its rawArgumentValidation hook, but every rejection returns { outcome: "reject" } without a correction code. validateToolArguments (packages/ai/src/utils/validation.ts) then throws the generic:
Validation failed for tool "todo_write": raw arguments rejected before coercion
No offending key, op, or expected shape is named, so the model has no way to self-correct and repeats the same failed call. Observed in a live session: four consecutive failed todo_write calls across turns, each with the identical opaque message.
Reproduction
All of these are rejected with the same uninformative message:
{"ops":[{"op":"note","note":"..."}]}
(unsupported note key — correct key is text, and note requires task)
{"ops":[{"op":"done","items":["some task"]}]}
(items is accepted by the key allowlist but done/drop require task or phase; items is append-only)
By contrast, an invalid op name reaches Zod validation and produces an actionable message:
- ops/0/op: Invalid option: expected one of "init"|"start"|"done"|"rm"|"drop"|"append"|"note"
So the pre-coercion hook is strictly less helpful than the schema layer it front-runs. Notably, TodoWriteTool.execute already handles the missing done/drop target case with a friendly Missing task or phase for done operation summary — the raw hook rejects the same shape earlier with zero detail, shadowing the better error.
Where
packages/coding-agent/src/tools/todo-write.ts — validateRawTodoArguments returns { outcome: "reject" } with no code in all four branches (root unknown key, op-entry unknown key, done/drop missing target, init-entry unknown key).
packages/ai/src/utils/validation.ts — RAW_ARGUMENT_REJECTION_MESSAGES only contains the three ask-* deep-interview codes, so any todo rejection falls to the generic byte-for-byte message.
Expected
Raw-argument rejections from todo_write should carry bounded, authority-controlled correction codes (the mechanism added for the ask-tool codes) so the surfaced error names the violated rule, e.g.:
- unknown root/op/init-entry key → list the accepted keys
done/drop without task/phase → state that a target is required and that items is append-only
Summary
todo_writeregistersvalidateRawTodoArgumentsas itsrawArgumentValidationhook, but every rejection returns{ outcome: "reject" }without a correction code.validateToolArguments(packages/ai/src/utils/validation.ts) then throws the generic:No offending key, op, or expected shape is named, so the model has no way to self-correct and repeats the same failed call. Observed in a live session: four consecutive failed
todo_writecalls across turns, each with the identical opaque message.Reproduction
All of these are rejected with the same uninformative message:
{"ops":[{"op":"note","note":"..."}]}(unsupported
notekey — correct key istext, andnoterequirestask){"ops":[{"op":"done","items":["some task"]}]}(
itemsis accepted by the key allowlist butdone/droprequiretaskorphase;itemsisappend-only)By contrast, an invalid op name reaches Zod validation and produces an actionable message:
So the pre-coercion hook is strictly less helpful than the schema layer it front-runs. Notably,
TodoWriteTool.executealready handles the missing done/drop target case with a friendlyMissing task or phase for done operationsummary — the raw hook rejects the same shape earlier with zero detail, shadowing the better error.Where
packages/coding-agent/src/tools/todo-write.ts—validateRawTodoArgumentsreturns{ outcome: "reject" }with nocodein all four branches (root unknown key, op-entry unknown key, done/drop missing target, init-entry unknown key).packages/ai/src/utils/validation.ts—RAW_ARGUMENT_REJECTION_MESSAGESonly contains the threeask-*deep-interview codes, so any todo rejection falls to the generic byte-for-byte message.Expected
Raw-argument rejections from
todo_writeshould carry bounded, authority-controlled correction codes (the mechanism added for the ask-tool codes) so the surfaced error names the violated rule, e.g.:done/dropwithouttask/phase→ state that a target is required and thatitemsis append-only