audit: sweep CLI flags, subcommands, and MCP arg/action names for name-vs-behavior mismatches #441

Open
opened 2026-08-17 22:07:53 +00:00 by hexajon · 2 comments
Owner

Problem

Some command surfaces carry a NAME that does not match what they DO. The name misleads a caller until it fails or behaves unexpectedly.

Seed example (found while landing #432): the --allow-ff-only flag on madtea repo edit is named for fast-forward-only merging, but it binds to allow_rebase_update - the Gitea field for "update a PR branch by rebase," a different setting. #432 corrected the flag's help text but kept the binding, and added the correctly named --allow-fast-forward-only for the real merge style. So the repo now has a flag whose NAME still implies ff-only while its behavior is rebase-update.

Ask

Sweep every command surface for the same class of defect - a name that does not describe its behavior:

  • CLI subcommands and flags (internal/cmd/).
  • MCP actions and argument names (internal/mcp/, internal/opspec/).

For each mismatch, record the name, what it actually does, and a proposed fix (rename to the accurate name, or correct the binding). Renames are governed by ADR 0016 (one canonical name per operation, enforced by the internal/parity test) and are compatibility decisions, so the deliverable is the audit plus proposals; each rename is then decided against the naming rules.

Acceptance

  • A list of every name-vs-behavior mismatch across the CLI and MCP surfaces, each with the accurate description and a proposed rename or rebind.
  • The list states the method used to enumerate the surfaces, so the next reader knows what was covered and what was not.
  • --allow-ff-only is the first entry.
  • #432 - surfaced the seed; corrected the help text, kept the binding.
  • #423 - renamed identifier args reject the old name (the aliasing / compatibility decision).
  • ADR 0016 - gh/tea vocabulary parity.
## Problem Some command surfaces carry a NAME that does not match what they DO. The name misleads a caller until it fails or behaves unexpectedly. Seed example (found while landing #432): the `--allow-ff-only` flag on `madtea repo edit` is named for fast-forward-only merging, but it binds to `allow_rebase_update` - the Gitea field for "update a PR branch by rebase," a different setting. #432 corrected the flag's help text but kept the binding, and added the correctly named `--allow-fast-forward-only` for the real merge style. So the repo now has a flag whose NAME still implies ff-only while its behavior is rebase-update. ## Ask Sweep every command surface for the same class of defect - a name that does not describe its behavior: - CLI subcommands and flags (`internal/cmd/`). - MCP actions and argument names (`internal/mcp/`, `internal/opspec/`). For each mismatch, record the name, what it actually does, and a proposed fix (rename to the accurate name, or correct the binding). Renames are governed by ADR 0016 (one canonical name per operation, enforced by the `internal/parity` test) and are compatibility decisions, so the deliverable is the audit plus proposals; each rename is then decided against the naming rules. ## Acceptance - A list of every name-vs-behavior mismatch across the CLI and MCP surfaces, each with the accurate description and a proposed rename or rebind. - The list states the method used to enumerate the surfaces, so the next reader knows what was covered and what was not. - `--allow-ff-only` is the first entry. ## Related - #432 - surfaced the seed; corrected the help text, kept the binding. - #423 - renamed identifier args reject the old name (the aliasing / compatibility decision). - ADR 0016 - gh/tea vocabulary parity.
Author
Owner

Ran the sweep. Result: the seed (--allow-ff-only) is already fixed, one true live name-vs-binding mismatch, plus two parity defects that will trip callers. Total actionable: 3, plus one cross-surface decision.

Method

Two surfaces enumerated:

  • CLI flags: every flag registration (Flags().StringVar/BoolVar/IntVar/...) across internal/cmd/ (518 registrations, read in full). Compared each NAME against its description and bound variable, then traced the high-risk mutation surfaces (repo edit/create, pr merge, branch protection, release, push-mirror) through internal/service/ and internal/gitea/client_*.go to the actual API JSON field.
  • MCP actions/args: internal/opspec/paramdocs.go in full (the source of record for MCP param docs, ADR 0008) plus the json:"..." tags and Required/Optional lists in internal/mcp/tools_*.go.
  • Bindings traced to ground truth against docs/api-specs/forgejo-15.0.json / gitea-1.26.1.json. Governing docs read first: ADR 0016 and docs/contributing/naming-conventions.md.

Not fully covered: I did not trace all 518 flags individually to their API field, only the high-risk mutation surfaces plus a name/description eyeball of the tail. Positionals were spot-checked, not swept. Raw-passthrough params (data, json_fields) bypass name-checking by design.

Mismatches

1. repo edit --allow-ff-only -> allow_rebase_update (SEED, ALREADY FIXED). Resolved in b8a03828: the current tree registers --allow-rebase-update -> allow_rebase_update (internal/cmd/repo/update.go:175) and the correctly-named --allow-fast-forward-only -> allow_fast_forward_only_merge (:176). The founding example no longer exists.

2. MCP madt_repos push_mirror_id binds to the mirror NAME, not an ID (TRUE mismatch, live). PushMirrorID string json:"push_mirror_id" (internal/mcp/tools_repos.go:122, Required at :391/:401) is passed straight into GET/DELETE /push_mirrors/{name} (internal/gitea/client_pushmirror.go:25-31). The API keys a push mirror by remote name; the CLI already models it as a <name> positional (internal/cmd/repo/pushmirror.go:77-101); the param's own description says "mirror name" (internal/opspec/paramdocs.go:635). A caller who trusts the name and passes a number gets a 404. Proposed rebind: rename the MCP arg to push_mirror_name (plain name is taken by the repo-name field, so keep the prefix). This aligns MCP with both the API path segment and the CLI positional.

3. release --prerelease vs --pre-release (CLI inconsistency, live). release create --prerelease (no hyphen, matches gh; release_create.go:152) but release list --pre-release (release_list.go:112) and release edit --pre-release (release_update.go:135) are hyphenated. MCP is consistent (prerelease everywhere). A caller who learned --prerelease on create gets "unknown flag" on list. gh uses --prerelease on every subcommand. Proposed: rename --pre-release -> --prerelease on list and edit.

4. PR-merge param names diverge CLI vs MCP (cross-surface, a decision). CLI pr merge --admin/--auto/--match-head-commit (gh-canonical; internal/cmd/pr/merge.go:133-135) vs MCP force_merge/auto_merge/head_commit_id (internal/mcp/tools_prs.go:69-70, paramdocs.go:489). None misleads on its own; the defect is cross-surface inconsistency (ADR 0016: one canonical name per operation on both surfaces). This is the one I want to settle deliberately: align MCP to the gh/CLI canon (admin/auto/match_head_commit), or record these as deliberate MCP-clarity deviations the way the view/branches-remote amendments do. I lean align for admin/auto (gh muscle-memory is the whole point of ADR 0016), but it is a compatibility call. The parity test does not currently catch param-name divergence (it enforces action-name parity) - that mechanical backstop is #444.

Sanctioned short forms (NOT mismatches)

--allow-merge -> allow_merge_commits, --allow-squash -> allow_squash_merge, --allow-rebase* (exact), --allow-fast-forward-only -> allow_fast_forward_only_merge, pr merge --ff-only -> strategy "fast-forward-only" (the correct use of ff-only, and the contrast that proves the seed was specific to repo edit). Plus the ADR-sanctioned surface conventions (view/get, branches-remote, forge-plural nouns, q).

Coverage gaps / borderline (flagged, not promoted)

  • Untraced flag tail: name/description eyeballed all 518, traced only high-risk surfaces to their API field.
  • madt_orchestrate max_failures: same name, different default across surfaces (CLI 0 = unlimited; MCP 0 = default 2). Documented, not deceptive, but a real gotcha.
  • repo search --topic is a boolean (mirrors the Gitea topic field) but reads like it wants a value; --starred-by is an int user ID. Both API-anchored, defensible, but plausible caller traps.
  • repo edit --no-wiki/--no-packages/... are disable-only (cannot re-enable via CLI; only MCP has_wiki enables) - a gh-parity + capability gap, not a name-vs-behavior mismatch.

Decision

These are public-surface renames, so they are compatibility changes, and they sit on top of #423 (converge-and-reject vs keep a deprecation alias). Which do you want to land, and under which #423 policy: (2) push_mirror_id -> push_mirror_name, (3) --pre-release -> --prerelease, and the (4) PR-merge align-or-record call? Say the set and I will land them (or wire #444 to enforce it mechanically).

Ran the sweep. Result: the seed (`--allow-ff-only`) is already fixed, one true live name-vs-binding mismatch, plus two parity defects that will trip callers. Total actionable: 3, plus one cross-surface decision. ## Method Two surfaces enumerated: - CLI flags: every flag registration (`Flags().StringVar/BoolVar/IntVar/...`) across `internal/cmd/` (518 registrations, read in full). Compared each NAME against its description and bound variable, then traced the high-risk mutation surfaces (repo edit/create, pr merge, branch protection, release, push-mirror) through `internal/service/` and `internal/gitea/client_*.go` to the actual API JSON field. - MCP actions/args: `internal/opspec/paramdocs.go` in full (the source of record for MCP param docs, ADR 0008) plus the `json:"..."` tags and Required/Optional lists in `internal/mcp/tools_*.go`. - Bindings traced to ground truth against `docs/api-specs/forgejo-15.0.json` / `gitea-1.26.1.json`. Governing docs read first: ADR 0016 and `docs/contributing/naming-conventions.md`. Not fully covered: I did not trace all 518 flags individually to their API field, only the high-risk mutation surfaces plus a name/description eyeball of the tail. Positionals were spot-checked, not swept. Raw-passthrough params (`data`, `json_fields`) bypass name-checking by design. ## Mismatches **1. `repo edit --allow-ff-only` -> `allow_rebase_update` (SEED, ALREADY FIXED).** Resolved in `b8a03828`: the current tree registers `--allow-rebase-update` -> `allow_rebase_update` (`internal/cmd/repo/update.go:175`) and the correctly-named `--allow-fast-forward-only` -> `allow_fast_forward_only_merge` (`:176`). The founding example no longer exists. **2. MCP `madt_repos push_mirror_id` binds to the mirror NAME, not an ID (TRUE mismatch, live).** `PushMirrorID string json:"push_mirror_id"` (`internal/mcp/tools_repos.go:122`, Required at `:391`/`:401`) is passed straight into `GET/DELETE /push_mirrors/{name}` (`internal/gitea/client_pushmirror.go:25-31`). The API keys a push mirror by remote name; the CLI already models it as a `<name>` positional (`internal/cmd/repo/pushmirror.go:77-101`); the param's own description says "mirror name" (`internal/opspec/paramdocs.go:635`). A caller who trusts the name and passes a number gets a 404. Proposed rebind: rename the MCP arg to `push_mirror_name` (plain `name` is taken by the repo-name field, so keep the prefix). This aligns MCP with both the API path segment and the CLI positional. **3. `release --prerelease` vs `--pre-release` (CLI inconsistency, live).** `release create --prerelease` (no hyphen, matches gh; `release_create.go:152`) but `release list --pre-release` (`release_list.go:112`) and `release edit --pre-release` (`release_update.go:135`) are hyphenated. MCP is consistent (`prerelease` everywhere). A caller who learned `--prerelease` on create gets "unknown flag" on list. gh uses `--prerelease` on every subcommand. Proposed: rename `--pre-release` -> `--prerelease` on list and edit. **4. PR-merge param names diverge CLI vs MCP (cross-surface, a decision).** CLI `pr merge --admin`/`--auto`/`--match-head-commit` (gh-canonical; `internal/cmd/pr/merge.go:133-135`) vs MCP `force_merge`/`auto_merge`/`head_commit_id` (`internal/mcp/tools_prs.go:69-70`, `paramdocs.go:489`). None misleads on its own; the defect is cross-surface inconsistency (ADR 0016: one canonical name per operation on both surfaces). This is the one I want to settle deliberately: align MCP to the gh/CLI canon (`admin`/`auto`/`match_head_commit`), or record these as deliberate MCP-clarity deviations the way the `view`/`branches-remote` amendments do. I lean align for `admin`/`auto` (gh muscle-memory is the whole point of ADR 0016), but it is a compatibility call. The parity test does not currently catch param-name divergence (it enforces action-name parity) - that mechanical backstop is #444. ## Sanctioned short forms (NOT mismatches) `--allow-merge` -> `allow_merge_commits`, `--allow-squash` -> `allow_squash_merge`, `--allow-rebase*` (exact), `--allow-fast-forward-only` -> `allow_fast_forward_only_merge`, `pr merge --ff-only` -> strategy "fast-forward-only" (the correct use of ff-only, and the contrast that proves the seed was specific to repo edit). Plus the ADR-sanctioned surface conventions (`view`/`get`, `branches-remote`, forge-plural nouns, `q`). ## Coverage gaps / borderline (flagged, not promoted) - Untraced flag tail: name/description eyeballed all 518, traced only high-risk surfaces to their API field. - `madt_orchestrate max_failures`: same name, different default across surfaces (CLI 0 = unlimited; MCP 0 = default 2). Documented, not deceptive, but a real gotcha. - `repo search --topic` is a boolean (mirrors the Gitea `topic` field) but reads like it wants a value; `--starred-by` is an int user ID. Both API-anchored, defensible, but plausible caller traps. - `repo edit --no-wiki`/`--no-packages`/... are disable-only (cannot re-enable via CLI; only MCP `has_wiki` enables) - a gh-parity + capability gap, not a name-vs-behavior mismatch. ## Decision These are public-surface renames, so they are compatibility changes, and they sit on top of #423 (converge-and-reject vs keep a deprecation alias). Which do you want to land, and under which #423 policy: (2) `push_mirror_id` -> `push_mirror_name`, (3) `--pre-release` -> `--prerelease`, and the (4) PR-merge align-or-record call? Say the set and I will land them (or wire #444 to enforce it mechanically).
Author
Owner

Decided. Ground truth was checked against the Forgejo API spec, gh, and the CURRENT tea CLI (gitea.com/gitea/tea, HEAD 2026-09-08). Note: the GitHub go-gitea/tea mirror is frozen at 2019 and was unusable - the live source had to be read directly.

Landing:

  • Item 2 (live bug): rename the MCP arg push_mirror_id -> push_mirror_name. It binds to the mirror NAME (GET/DELETE /push_mirrors/{name}, internal/gitea/client_pushmirror.go:25-31); the current name invites a 404. Converge-rename per the #423 policy (the old name rejects with a teaching redirect).
  • Item 3 (live inconsistency): rename CLI --pre-release -> --prerelease on release list and release edit, matching release create and gh. Converge-rename.
  • Item 4 (cross-surface): align the MCP pr-merge params to the gh canon that madtea's CLI already uses - force_merge -> admin, auto_merge -> auto, head_commit_id -> match_head_commit. One canonical name across both surfaces (ADR 0016). tea offers no precedent (it exposes only --style/--title/--message and omits these three), so gh is the only established CLI vocabulary. Old MCP names reject with a teaching redirect; changelog-flag the break (0.x, ADR 0028).

Item 1 (the --allow-ff-only seed) was already fixed (b8a03828).

The mechanical parity backstop for param-name divergence (the parity test only enforces action names today) is #444.

Clearing needs-decision.

Decided. Ground truth was checked against the Forgejo API spec, gh, and the CURRENT tea CLI (`gitea.com/gitea/tea`, HEAD 2026-09-08). Note: the GitHub `go-gitea/tea` mirror is frozen at 2019 and was unusable - the live source had to be read directly. Landing: - Item 2 (live bug): rename the MCP arg `push_mirror_id` -> `push_mirror_name`. It binds to the mirror NAME (`GET/DELETE /push_mirrors/{name}`, `internal/gitea/client_pushmirror.go:25-31`); the current name invites a 404. Converge-rename per the #423 policy (the old name rejects with a teaching redirect). - Item 3 (live inconsistency): rename CLI `--pre-release` -> `--prerelease` on `release list` and `release edit`, matching `release create` and gh. Converge-rename. - Item 4 (cross-surface): align the MCP pr-merge params to the gh canon that madtea's CLI already uses - `force_merge` -> `admin`, `auto_merge` -> `auto`, `head_commit_id` -> `match_head_commit`. One canonical name across both surfaces (ADR 0016). tea offers no precedent (it exposes only `--style`/`--title`/`--message` and omits these three), so gh is the only established CLI vocabulary. Old MCP names reject with a teaching redirect; changelog-flag the break (0.x, ADR 0028). Item 1 (the `--allow-ff-only` seed) was already fixed (`b8a03828`). The mechanical parity backstop for param-name divergence (the parity test only enforces action names today) is #444. Clearing needs-decision.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
sixfold-space/madtea#441
No description provided.