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:

  1. User relearning cost. Agents and humans arrive with muscle memory from gh (the GitHub CLI) and tea (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.
  2. 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)

  1. Run gh <upstream-noun> --help. If gh has the command, use gh’s exact name — including verb spelling (edit, not update) and hyphenation (delete-asset, not deleteAsset).
  2. If gh does not have it, run tea <upstream-noun> --help and use tea’s name.
  3. If neither has it (madtea-unique commands: finish, init, orchestrate, prune, secret-exec), pick a clear verb and note its absence in docs/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 pathMCP action
release uploadupload
release delete-assetdelete_asset
release editedit
release assets listassets_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:

  1. CLI convenience shortcuts over a parameterized MCP action (e.g. release by-tag <tag> vs. MCP get(tag=…)).
  2. MCP-only plumbing (madt_git_objects/refs, tree, blob, apply_diff) with no CLI surface — registered as IntentionalMCPOnly in internal/parity/allowlist.go.
  3. 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.md comparison matrix. The legacy tracker 1225 sweep established this touch-list. See the feedback_cli_rename_touchlist.md memory entry for the full checklist.
  • Parameter names follow the same rule. madt_finish uses title because gh pr create uses --title; madt_search uses q because 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_message instead of title), the fix is the description — surfacing the concept the agent reached for — not a new did-you-mean entry or alias. Names stay anchored to gh/tea/API.
  • Relates to ADR 0008. The neutral OperationSpec registry (in internal/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. viewget 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. updateedit) requires a new amendment.

Implementation.

  • MCP: installViewActionNormalization (middleware in internal/mcp/register.go) rewrites action="view" to action="get" BEFORE the SDK’s JSON-schema validator runs, for any consolidated tool whose manifest includes a get action. NormalizeViewAction (exported from internal/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 the Aliases field 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. branchesbranches-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 viewget 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.