ADR 0016 — Names match gh/tea verbatim; no invented vocabulary
- Status: Accepted
- Date: 2026-07-01
- Tracking: legacy tracker 1225 (batch rename to parity), legacy tracker 1572 (stale CLAUDE.md vs deliberate bareword), legacy tracker 1593 (no-negative-steers), legacy tracker 1604, legacy tracker 1605 (param-name ergonomics)
Context
madtea exposes two surfaces — the Cobra CLI (madtea …) and the MCP server
(madt_<noun> tools) — and every operation appears on both. Without a
deliberate naming rule, each surface accumulates its own vocabulary as
contributors pick names that feel natural or internally consistent. Over time
this creates two problems:
- User relearning cost. Agents and humans arrive with muscle memory from
gh(the GitHub CLI) andtea(the Gitea CLI). Every command or flag renamed away from those tools requires relearning. Across a user base, this is a large aggregate cost; across model generations rediscovering the API, larger still. - Surface drift. If the CLI and MCP are named independently, the parity test must maintain an ever-growing alias table to reconcile them, and an agent that learned the CLI name cannot predict the MCP action name.
The legacy tracker 1225 batch rename brought CLI and MCP into mutual alignment after a period of divergent naming. The lessons from that sweep are codified here.
Decision
Every CLI subcommand, flag, and MCP action name matches gh (GitHub CLI)
first, then tea (Gitea CLI) where gh has no equivalent, verbatim. madtea
is a capability superset of those tools, never a rename of them. Exactly one
canonical name exists per operation on both surfaces; no aliases are added.
Lookup order (documented in docs/contributing/naming-conventions.md)
- Run
gh <upstream-noun> --help. Ifghhas the command, usegh’s exact name — including verb spelling (edit, notupdate) and hyphenation (delete-asset, notdeleteAsset). - If
ghdoes not have it, runtea <upstream-noun> --helpand usetea’s name. - If neither has it (madtea-unique commands:
finish,init,orchestrate,prune,secret-exec), pick a clear verb and note its absence indocs/COMPARISON.md.
The same rule applies to flags. --draft, --prerelease, --target —
whatever flag names gh uses. If gh does not have it, mirror tea. Never
introduce a flag named differently from both upstreams.
MCP action names mirror the CLI path
MCP action names are derived from the CLI subcommand path by converting kebab-case to snake_case:
| CLI path | MCP action |
|---|---|
release upload | upload |
release delete-asset | delete_asset |
release edit | edit |
release assets list | assets_list |
One tool per resource noun, named madt_<noun> (plural). No MCP aliases:
agents discover the action list fresh each session; there is no muscle memory to
preserve that would justify a second name.
The local/forge singular-plural split
madtea wraps some local git commands (branch, tag, checkout) that must
match git exactly — singular. When both a local wrapper and a forge resource
exist for the same noun, the local wrapper owns the singular name and the forge
resource takes the tea-canonical plural (branches, tags). This is the one
place forge commands are plural while their siblings (issue, pr, repo) are
singular — forced by the local/remote homonym collision, not by preference.
Invented -api-style suffixes (the previous workaround: branch-api,
tag-api) are banned.
No aliases on either surface
CLI aliases are forbidden. If an alias is tempting because of a rename, the
rename was in the wrong direction — pick the upstream name from the start.
The cliCanonicalAliases map in internal/parity/parity.go documents the
handful of existing documented exceptions (mostly mechanical shape differences
between surfaces: bare noun vs. -list suffix, CLI convenience shortcuts that
collapse onto a parameterized MCP action, a few cross-domain placements). New
entries in that map require justification; they are not the default.
Acceptable divergences
Three patterns legitimately diverge between surfaces and are documented in
docs/contributing/naming-conventions.md:
- CLI convenience shortcuts over a parameterized MCP action (e.g.
release by-tag <tag>vs. MCPget(tag=…)). - MCP-only plumbing (
madt_git_objects/refs,tree,blob,apply_diff) with no CLI surface — registered asIntentionalMCPOnlyininternal/parity/allowlist.go. - Narrow CLI-only operations where an agent has no real automation use case
(server-side markdown rendering, human mailbox subscriptions, bare Cobra
parents that only print help) — registered as
IntentionalCLIOnly. DevOps surface area (runners, secrets, webhooks, SSH keys) is NOT in this category.
Any other divergence must register in cliCanonicalAliases with a comment, or
be a gap tracked by a KnownGapsCLIOnly/KnownGapsMCPOnly entry pointing at
an open issue.
Enforcement
internal/parity/parity.go and internal/parity/parity_test.go enumerate the
live Cobra command tree and the live MCP tool+action registry at test time
(using go list-introspected data, not a static snapshot) and fail CI when any
operation appears on one surface but not the other without an allow-list entry.
The test is the final word on whether the convention is being followed.
Consequences
- Renaming is a breaking change with a known blast radius. A rename is not
done until every consumer is updated: executable scripts, hook strings,
integration tests, CI workflow steps, and the
docs/COMPARISON.mdcomparison matrix. The legacy tracker 1225 sweep established this touch-list. See thefeedback_cli_rename_touchlist.mdmemory entry for the full checklist. - Parameter names follow the same rule.
madt_finishusestitlebecausegh pr createuses--title;madt_searchusesqbecause that is the Forgejo/Gitea search API. Internal inconsistency is not a bug when the names map to two different upstream vocabularies. The description carries the cross-vocabulary nuance (ADR 0005). - The parity table stays small. When names align by construction, the alias table holds a few dozen entries for well-understood shape differences, not a growing list of invented synonyms.
- Relates to ADR 0005. When an agent reaches for a natural-but-wrong
parameter name (e.g.
commit_messageinstead oftitle), the fix is the description — surfacing the concept the agent reached for — not a new did-you-mean entry or alias. Names stay anchored togh/tea/API. - Relates to ADR 0008. The neutral
OperationSpecregistry (ininternal/opspec) is authored with gh/tea names as the anchor, so the parity convention and the help corpus reinforce each other.
Alternatives considered
- Invent clearer names. Rejected: “clearer by whose judgment?” Every developer has a different intuition about clarity. Without an external anchor the names accumulate personal preferences and diverge. The gh/tea vocabulary is an external fact, not a judgment call.
- Provide aliases for all old names. Rejected: two names per operation is ambiguity. An agent (or a human) that has learned an alias has no signal that a canonical form also exists; the parity test grows without bound; and the alias table itself becomes a liability when the upstream changes its name.
- Allow CLI and MCP to name independently. Rejected: this was the situation before legacy tracker 1225. The alias table was large and growing; agents that knew the CLI docs could not predict MCP action names; the parity test was a maintenance burden rather than a structural guarantee.
Amendment (2026-07-04, legacy tracker 1874) — view as THE one deliberate synonym for get
view is a working synonym for get across the entire tool surface.
Agents trained on gh — which uses view as the canonical verb for fetching
a single resource (gh issue view, gh pr view, gh repo view,
gh release view) — should not be penalised by a first-call failure when
they reach for view on a madtea surface that uses get.
Rationale. The no-aliases rule exists to prevent the alias table from growing without bound and to keep the parity test maintainable. One deliberate exception is the controlled alternative — a bounded carve-out with a hard rule (consistency-or-nothing; a half-applied alias is worse than none). The ergonomic cost of rejecting a gh-trained agent on every first call outweighs the cost of a single, well-bounded, CI-enforced synonym.
Scope. view ↔ get only. The one-canonical-name rule stands for
every other operation. show is specifically NOT an alias; it stays as a
SuggestFor hint on the four commands (issue, pr, repo, release) that carry
it, pointing agents toward the canonical get. Extending the synonym mechanism
to other pairs (e.g. update ↔ edit) requires a new amendment.
Implementation.
- MCP:
installViewActionNormalization(middleware ininternal/mcp/register.go) rewritesaction="view"toaction="get"BEFORE the SDK’s JSON-schema validator runs, for any consolidated tool whose manifest includes agetaction.NormalizeViewAction(exported frominternal/mcp/action_spec.go) is the pure function used by the parity test. - CLI:
addViewAliasToGetCommands(internal/cmd/root.go) walks the Cobra tree after all commands are registered and appends"view"to theAliasesfield of every command whose name is"get". One function, one walk, zero per-command edits.
Enforcement. Two CI locks, one per failure direction.
TestViewAliasOnBothSurfaces in internal/parity/view_alias_test.go fails
when any CLI get command is missing the view alias, when any tool
advertises view as a native action, or when the normalization over-applies
(a no-get tool, or show anywhere). TestViewAlias_EndToEndServerWiring
in internal/mcp/view_alias_e2e_test.go drives action="view" through the
PRODUCTION server construction and fails if the middleware is unwired from
NewServerWithRegistry (mutation-verified: removing the wiring makes it
fail). Consistency-or-nothing: a half-applied alias fails one of the two. The
cliCanonicalAliases table in parity.go requires no new entries — cobra
Aliases do not create separate command paths in the WalkCLI enumeration,
and the MCP normalization maps view onto the canonical get key before any
recording happens.
get remains the canonical name everywhere. view is accepted, never
advertised as the primary name in docs, help text, or examples. The schema
action enum, the tool description, the madt_help topic content, and all
reference docs continue to list get as the action name; view works silently
in the background.
Amendment (2026-07-05, legacy tracker 1958) — branches-remote deviates from tea’s branches for clarity
The remote-branch resource is branches-remote on both surfaces — CLI
madtea branches-remote and MCP madt_branches_remote — deliberately deviating
from tea’s branches verb. The local/forge singular-plural split (above) gave
the local git wrapper the singular branch and the forge resource the plural
branches to match tea branches. In practice branch (local) vs branches
(remote) reads as tea jargon: anyone not steeped in tea cannot tell from the
names which one hits the server API, and the near-identical spelling invites
using the wrong one. The -remote suffix names the distinction that actually
matters — local checkout vs. remote API — so a non-tea user reaches for the
right command the first time.
Scope. branches → branches-remote only, and it is a clean-break rename
(no branches alias; legacy tracker 1958: “rename clean break, no existing users”).
tags is unchanged: it keeps the tea-canonical plural (tea tags /
madt_tags), because there is no comparable confusion (tag vs tags is the
ordinary local/remote plural, and tea tags is the muscle-memory name). The
local-branch wrapper branch (and the MCP madt_branch_create /
madt_branch_delete) is unchanged. Extending an -remote suffix to any other
noun requires a new amendment.
Rationale. This is the same controlled-exception logic as the view↔get
amendment: the lookup-order rule (“match gh, then tea, verbatim”) exists to
keep names predictable and the parity table small, but it is a means to the end
of low user-relearning cost. Here that end is better served by a one-word
deviation than by inheriting a name that actively misleads the majority of users
who do not know tea. branches-remote has no gh/tea equivalent, so it is
noted as an intentional deviation in docs/COMPARISON.md (per lookup-order step
3), exactly as the other madtea-unique names (finish, init, …) are.
Enforcement. The rename is applied in lockstep on both surfaces, so the
internal/parity CLI↔MCP check passes by construction — CLI branches-remote/*
and MCP madt_branches_remote/* (tool name minus madt_) both canonicalize to
the branches-remote/* key space with no new cliCanonicalAliases entry. The
deviation is documented at its point of use in internal/parity/parity.go (the
comment above cliDomainAliases) and in docs/contributing/naming-conventions.md
so a future reader does not “correct” it back to the tea name.
branches-remote is the canonical name everywhere. There is no branches
synonym on either surface; the schema, tool description, madt_help topics, and
reference docs all name branches-remote / madt_branches_remote.
Canonical source: docs/adr/0016-gh-tea-vocabulary-parity.md in the madtea repo.