ADR 0008 — The OperationSpec registry is the single source of record for all operation help and metadata

  • Status: Accepted
  • Date: 2026-06-28
  • Tracking: legacy tracker 1693

Context

madtea ships as both a CLI binary and an MCP server, and today maintains two independently-authored help corpora:

  • CLI help — Cobra Short/Long/flag-usage strings, hand-written per command in internal/cmd.
  • MCP helpActionSpec.Summary and jsonschema param tags, authored in internal/mcp.

The parity test (internal/parity) keeps names aligned across surfaces but never description text, so the two corpora drift freely. Concrete divergences today: pr merge exposes four boolean flags (--squash, --rebase, --rebase-merge, --ff-only) on the CLI versus a single strategy enum on MCP; the CLI flag --admin appears as force_merge in MCP; issues create is described as “Create a new issue.” on the CLI and “Create an issue.” in MCP. MCP summaries are also deliberately richer — a drift guard mandates that footgun keywords appear inline — but that policy runs against MCP alone and cannot protect the CLI description.

MCP discovery is also noun-keyed: madt_help(topic="<noun>") plus the tool schema. A help lookup by verb (import, finish, rank, develop) dead-ends (legacy tracker 1690) — there is no verb topic. action= and op= values are documented as prose inside the tool description, not as JSON-Schema enums with per-value descriptions, so the model never sees structured per-verb docs when selecting an action.

Current MCP best practice (per the MCP specification and Claude Code docs) calls for: reference docs needed to call a tool to live in the schema itself (enums with per-value descriptions, surfaced on every call); deeper reference docs to be delivered via MCP Resources/Prompts; and tool errors to return the valid option list. madtea predates these conventions. This ADR adopts them now that they are settled practice.

Decision

1. A surface-neutral OperationSpec registry

Establish a single neutral OperationSpec registry by lifting ActionSpec out of internal/mcp into a new surface-neutral package imported by both internal/cmd and internal/mcp. This package is the source of record for every operation’s help and metadata. Per operation (noun × verb) it carries:

  • Tiered description fields — headline, dense summary, verbose long, params (name, description, required, …), examples, gotchas.
  • Surface/exposure metadatasurfaces[] (cli/mcp), gatedDomain, safety flags (fail-closed guard, no-force-delete, etc.).

2. All surfaces render from this one record

  • CLI renders verbose-by-default (long text + examples + full flag help), derived from the spec. Hand-written Cobra help strings are retired.
  • MCP tool schema renders the dense summary inline and promotes action/ op parameters to JSON-Schema enums with per-value descriptions — making the tool schema the self-sufficient Tier-1 reference for every verb.
  • MCP Resources/Prompts are promoted from optional (legacy tracker 1324) to a first-class per-noun (and per-verb) reference delivery path (madtea://reference/{noun}[/{action}]), backed by the same spec content.
  • madt_help is demoted to a thin, verb-aware compatibility surface that resolves and suggests the owning noun. It is not deleted.

3. Authoring stays MCP-canonical

The neutral package is the physical home of every spec entry, not a licence to author CLI-first. Per ADR 0001’s invariant: MCP semantics remain the source of truth; the spec is authored with MCP needs as the primary lens.

4. Render tier, not corpus tier

“Verbose for humans” versus “dense for agents” is a render choice applied to the tiered fields in the single spec — not a second, independently authored corpus. Agents receive dense-inline content plus depth-on-demand (via resources and madt_help); humans receive verbose-inline content. The facts are the same; the presentation is not.

Consequences

Fork of ADR 0001 §Tiers — stated explicitly (following ADR 0006’s precedent of naming the reconciliation):

ADR 0001 names madt_help(topic) as canonical Tier-1 surface 3 and MCP Resources/Prompts as merely optional (legacy tracker 1324). ADR 0008 changes the taxonomy: the canonical reference is the neutral OperationSpec; its Tier-1 self-sufficient projection is the tool/action schema (now carrying enums with per-value descriptions); Resources/Prompts are promoted to first-class reference delivery; madt_help is demoted to a thin, verb-aware surface. ADR 0001’s tier wording is updated in the ADR itself under legacy tracker 1697.

Updated by legacy tracker 2057 (2026-07-08): the action/op enums no longer carry per-value descriptions — they became bare registry-derived value sets, and the tool Description is the sole carrier of every action/op summary (schema self-sufficiency unchanged). See the ADR 0015 legacy tracker 2057 amendment for the authoritative record.

Clean-room self-sufficiency (legacy tracker 1326) is preserved. Tier 1 must remain self-sufficient without Resources. Because the enum-borne schema carries the reference inline, self-sufficiency holds even after madt_help is demoted. The ordering constraint follows: action/op enum self-descriptions (legacy tracker 1698/legacy tracker 1695) must land before any madt_help demotion ships, and madt_help is kept thin — not deleted.

Safety surface exposure is unchanged (hard non-goal). This ADR unifies description text, never the exposed surface. The CLI↔MCP exposure divergence is preserved and made explicit via per-op surfaces[], gatedDomain, and safety metadata fields. The following boundaries are unchanged:

  • CLI-only, safety-withheld operations (setup, stash) — setup cannot prompt over MCP, and stash is governed by the never-drop-a-stash rule. (Note: prune is genuinely dual-surface — madt_prune is registered always-on and is a safe merged-branch cleanup with no force-delete path; it is not withheld.)
  • The 11 madt_enable-gated domains
  • MCP-only plumbing (git-objects)
  • MCP-only fail-closed guards (repoguard, no-force-delete, default-branch/apply-diff)

An anti-widening CI test (sibling to the parity test) fails if any operation is exposed on a surface its surfaces[] metadata excludes. Forking ADR 0001’s surface taxonomy must not weaken the agent-exposure boundary.

Drift guards relocate. The ADR 0005 param-description floor, the ADR 0001 footgun-gotcha requirement, and the substance floor (legacy tracker 1689) all move to run against the neutral OperationSpec, which protects both surfaces simultaneously.

Migration is incremental. Work proceeds noun-by-noun behind the unchanged parity keyspace, gated by docs generate --verify plus a new content-parity test. The two primary risks are the cmd→mcp layering inversion (extract the neutral package first) and structural param mismatches (e.g. pr merge’s four CLI boolean flags vs. one MCP enum parameter).

Alternatives considered

  • CLI-authored, derive MCP (author rich human text CLI-side, auto-truncate to a terse MCP summary). Rejected: inverts ADR 0001 (MCP is the source of truth) and risks dropping mandated MCP footgun keywords during truncation.
  • Keep two corpora; add a content-parity CI test. Rejected: cheapest path with no refactor, but it enshrines the two-systems-of-record this ADR exists to eliminate — a parity test over two independent sources is maintenance of two independent sources.
  • Delete madt_help once Resources exist. Rejected: breaks the clean-room self-sufficiency invariant (legacy tracker 1326) unless and until the schema fully carries all reference inline. Kept thin instead.

Amendment (2026-07-10) — prune gains a content-verified force-delete (legacy tracker 2109)

Issue legacy tracker 2109 (Decided 2026-07-10) adds a prune-scoped, content-verified force-delete: under confirm=true/--yes, prune deletes the findSquashMerged result set with git branch -D, re-verifying each branch’s content-in-default-branch proof at delete time (and, per the same decision, deleting the squash-merged branch’s remote twin under --remote with the same proof). The “Safety surface exposure is unchanged” parenthetical above describing madt_prune as “a safe merged-branch cleanup with no force-delete path” is superseded for prune only — the general madt_branch_delete no-force-delete guard is unchanged and still accurate.

Canonical source: docs/adr/0008-operation-registry-source-of-record.md in the madtea repo.