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 ininternal/cmd. - MCP help —
ActionSpec.Summaryand jsonschema param tags, authored ininternal/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 metadata —
surfaces[](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/opparameters 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_helpis 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/openums no longer carry per-value descriptions — they became bare registry-derived value sets, and the toolDescriptionis 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) —setupcannot prompt over MCP, andstashis governed by the never-drop-a-stash rule. (Note:pruneis genuinely dual-surface —madt_pruneis 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_helponce 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.