Smoke-6 found the agent calling note(scope='decision', context=null,
chosen=null, rationale=null) eight times in a row. Root cause split
across two surfaces:
1. The MCP tool schema declared these fields as `str | None = None`,
producing a JSON schema of `anyOf [string, null]`. minimax-m2.7 read
that and decided null was a valid value — passed it on every retry.
2. The remediate text used `<placeholder>` syntax for the example
without telling the agent "don't pass null" explicitly.
Fixes:
- roboco/api/schemas/v2/do.py NoteRequest: context, chosen, rationale,
what_done, what_learned, what_struggled now typed `str = ""` (no None).
Pydantic on the route rejects literal null with 422 BEFORE the gateway
sees it. Empty string still counts as missing at the gate.
- roboco/mcp/do_server.py note(): matching signature changes so the
MCP tool schema declares the fields as `string` not `anyOf[string,null]`.
- roboco/services/gateway/content_actions.py: remediate text now opens
with "DO NOT pass null" and the example uses concrete values (redis vs
postgres) instead of <angle bracket> placeholders. Reflect remediate
also gets the don't-pass-null intro and concrete values.
8 new tests pin "schema rejects null for each of the 6 string fields"
plus "empty defaults work for unscoped notes".