Check what the model actually sent before believing the schema
`edit_plan` took the model's arguments as `unknown` and cast them to the shape its JSON schema advertises. `as` is erased at runtime, so nothing checked. A model sent `operations` as a JSON-encoded string, which reached `edit.apply` and died three lines in on `operations.some is not a function`. The two guards it passed on the way are the interesting part. `length === 0` and `length > 1` were counting characters, not operations, so a batch of one sailed through a check meant to catch a batch of many, and the type lie surfaced as a `TypeError` several frames from where it entered. Worse than the crash: `answer` caught it and handed the model back "operations.some is not a function", which names nothing it could change. A model that cannot read its own error retries the same call. So the schema is now checked rather than advertised. Every rule in it — the required fields, the `op` enum, the item limits, the digest pattern — is enforced at the boundary by hand, next to a comment saying which line of the schema it mirrors, because the two disagreeing silently is the whole failure mode being fixed. What the schema cannot express is enforced here too: one object shape covers every operation, so "index is required unless the op is insert_root" lives only in a description string, and now also in code. Nothing is coerced. A JSON string could be parsed back into an array and the turn would succeed, but the model would go on sending strings and the next tool to trust its input would find out the hard way. It is told what shape was wanted instead, which is the contract `ask` has had all along through `@chopin/question`'s `normalize` — this is the tool that skipped it. `anchor_plan` had the identical cast and is fixed with it. It runs immediately after every successful edit, so it is no less reachable; a string there would have iterated character by character and failed differently, just as uselessly. `edit.apply` keeps its `Operation[]` signature. Validation belongs where the input arrives, and the type it always claimed is now true. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Parente59e9c7
3 files changed+419-10
StackedSplit