ADR 0005 — Parameter-name ergonomics: proactive schema clarity, not reactive matching
- Status: Accepted
- Date: 2026-06-24
- Tracking: legacy tracker 1604, legacy tracker 1605. Builds on legacy tracker 1577 (the bounded did-you-mean) and legacy tracker 1593 (no negative steers).
Context
Agents reach for a “natural” argument name that is not the tool’s canonical parameter, the JSON unmarshaler silently drops it, and a wasted round-trip follows. Recurring, real instances:
madt_finishis keyed bytitle; an agent passedcommit_message(andmessage) — “the commit message” is the reflex word for a commit subject.madt_searchis keyed byq; an agent passedquery.
Each time one of these surfaces, the reflex is to reach for the reactive
side — extend the unknown-argument did-you-mean (legacy tracker 1577) with another synonym, a
compound/_-token splitter, or a broad alias table — until “no guess ever
fails.” That instinct keeps getting re-explored from scratch, and it pulls
toward exactly the failure mode we want to avoid: a never-fail matcher that is
confidently wrong (user_message → body?, review_id → id). This ADR records
the decision so a future “wrong parameter name” bug resolves by reference.
Three facts shape the decision:
The documented best practice is proactive. Anthropic’s tool-design guidance
(Define tools) states that “extremely detailed descriptions … [are] by far
the most important factor in tool performance,” including “what each parameter
means.” Consistent naming across a tool suite is called out by the MCP tool
guidance as load-bearing — inconsistent names “cause the LLM to make mistakes.”
Reactive “did you mean X?” error-correction is nowhere endorsed; a
wrong-guess-then-retry is a failure the schema should prevent, not a feature to
build. (Tool-use examples, the secondary proactive lever cited in Advanced
tool use, are an Anthropic Messages-API feature with no field on the MCP
mcp.Tool type — so via the MCP server the descriptions ARE the lever.)
Our canonical names are deliberately the gh/tea/API names, which are the
agent training-data anchor. madt_finish uses title because that is
gh pr create --title; madt_search uses q because that is the Forgejo/Gitea
search API. The apparent internal inconsistency — “the commit message” is title
on finish but message on the five commit-creating tools (files, wiki, tags,
gitobjects, prs-merge) — is not arbitrary: it mirrors the git-commit vs gh-PR
vocabularies those tools wrap. Renaming to make the internal surface uniform
would walk away from the external vocabulary agents are trained on, and break
the gh/tea parity guard (docs/contributing/naming-conventions.md). So the
consistency that matters is consistency with gh/tea/API, not internal
uniformity.
Fuzzy matching is the precedent we are warned off. The existing did-you-mean
(internal/mcp/arg_suggest.go) is deliberately bounded — Levenshtein ceiling
2, snake_case token containment only on an exactly one hit, identifier synonym
classes kept tight and resolved ambiguity-aware. Every prior fix in this lineage
(legacy tracker 881 FlexIDs, legacy tracker 883 enrichment, legacy tracker 884 scalar coercion, legacy tracker 1327 FlexStrings, legacy tracker 1577
rejection) taught one discrete, conservative lesson and added an escape hatch
rather than a broad heuristic. Coercion never rewrites keys; suggestion never
fires when ambiguous. Broadening that into compound/token matching is the
“never-fail-but-buggy” pattern those bounds exist to prevent.
Decision
A wrong parameter-name guess is fixed PROACTIVELY, at the schema, not REACTIVELY, in the matcher. When an agent passes a natural-but-wrong argument name, the fix is the parameter’s description, and — if the canonical name has drifted from gh/tea/API — the name. It is not a new did-you-mean entry, a fuzzy/compound matcher, an alias, or a rename toward internal uniformity.
The recipe (apply this when a “wrong param name” bug surfaces)
- Confirm the canonical name matches gh/tea/API. If it does not, that is the bug — align the name to the external vocabulary (and update the parity matrix). If it does, do not rename it to chase internal uniformity.
- Fix the description, positively. Make the parameter’s
jsonschemadescription surface the concept the agent reached for, with an inline example — e.g. finishtitle: “Commit message subject and PR title — the one-line conventional-commit summary applied to both the commit and the PR (e.g. ‘feat: add auth’).” An agent scanning the schema for “commit message” now lands ontitlewithout guessing. - No negative steers (per legacy tracker 1593). State what the field is, never “pass as X, not Y/Z” — an enumerated forbidden set is brittle, never complete, and reads as nagging.
- Do not extend the reactive matcher for this. No new synonym for the
compound, no
_/-token-splitting, no broad alias table.
The reactive did-you-mean stays a bounded backstop
installUnknownArgumentRejection (legacy tracker 1577) remains valuable: an unknown key is
rejected before the handler runs, named, with the valid-argument list and a
confident suggestion when one exists. But it is a backstop, not the primary
fix, and its bounds are deliberate and not to be loosened:
- Levenshtein ceiling stays 2 (typos only —
numbr→number, notmessage→title, distance 6). - Token containment fires only on an exactly one hit.
- Synonym classes stay tight (interchangeable peers only) and resolve ambiguity-aware (no suggestion when a tool declares two members of one class).
Tighten, never broaden. A semantic synonym that is not a typo (message,
query, commit_message) is the schema description’s job, not the matcher’s.
Names stay anchored to gh/tea/API
No rename “fixes” an internal inconsistency that is really two external
vocabularies. title (gh PR), q (search API), and message (git commit) each
match what they wrap. The parity test in internal/parity/ is the guard; the
description carries any cross-vocabulary nuance.
Consequences
- Fixing a param-name bug is a one-line description edit, gated by the
normal
docs generate --verify. No new matcher surface, no growing alias table, no behavior change to the bounded suggester. legacy tracker 1604 and legacy tracker 1605 both reduced to exactly this. - The did-you-mean does not creep. Its bounds are now a recorded decision, not a thing to be re-argued each time a non-typo guess appears.
- The “adding a command” guidance gains a pointer: a new parameter’s description must positively name the concept (and example) an agent reaches for, in the gh/tea/API vocabulary — the proactive lever, applied at authoring time rather than after a bug.
Alternatives considered
- Extend the did-you-mean with the synonym / compound guess (the first
reflex, prototyped for legacy tracker 1604). Rejected: the docs do not endorse reactive
correction;
message/queryare not typos so they fall outside the bounded tiers by design; and the natural next step (token-splittingcommit_message) is the over-firing matcher (user_message → body?) the bounds exist to prevent. A suggestion is also only a confident retry, not a first-call land — the description is what makes the first call land. - An explicit enumerated alias table (
message→title,query→q, …) as a deterministic suggester. Rejected as the primary fix: it is still reactive (post-failure), it is a table that grows with every observed miss, and it competes with the one place that should carry the meaning — the description. - Accept the wrong name as an alias (silently treat
messageastitleon finish). Rejected: forks the one-canonical-name rule (docs/contributing/naming-conventions.md); the agent never learns the real name. - Rename to internal consistency (e.g. finish
title→message). Rejected: walks away from the gh/tea vocabulary agents are trained on, and breaks the parity guard. The split is two external vocabularies, not a defect.
Canonical source: docs/adr/0005-parameter-name-ergonomics.md in the madtea repo.