githubnext/chopinPublic

Let's compose rich plans together

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>
CCMAMaggie Appleton and Claude Opus 5 (1M context) authored
0f59766Patch
Parente59e9c7
3 files changed+419-10
StackedSplit
apps/server/src/agent/arguments.test.ts+165 −0
apps/server/src/agent/arguments.ts+251 −0
apps/server/src/agent/tools.ts+3 −10