ADR 0217: Envelope command references render per delivery surface
Status: accepted; builds on the one-envelope seam of ADR 0028, the hint registry of ADR 0172, the verb→tool parity of ADR 0041/ADR 0045, and the channel model of ADR 0192; applies the declared-canonical-set discipline of ADR 0214 to command spellings.
Context
§discern is driven primarily over the Model Context Protocol (MCP), with the CLI as the documented fallback. But every envelope hint was authored in CLI spelling and rode both surfaces verbatim: an agent that had just called discern_prepare was told to "run discern done" — an instruction that contradicts the prefer-the-tools stance the same server serves, and that nudges the agent into a shell round-trip. Each bypass costs the structured result the tool would have returned and muddies per-surface attribution in the logbook.
The inconsistency was systemic, not editorial. The registry held dozens of CLI spellings across templates, shared helper strings, and hint parameters built at call sites (the gotchas doc fetch, the await retry command, setup's landing command), and every future hint inherited the problem. Meanwhile some spellings were correct because of the split: commands the agent relays for its human owner (discern status --verbose), and commands whose verbs deliberately have no tool. Nothing distinguished the deliberate cases from the accidental ones.
Decision
§A runnable discern command in a registered hint is a typed reference, and each delivery surface renders it in its own spelling.
- The reference form. Templates and CommandRef-typed hint parameters build references with validating constructors (
discernCommand,ownerDiscernCommand, plusflag/positional/endOfFlagsin CLI order). A reference serializes as a closed, delimited token inside the authored string; only the constructors produce the grammar, and the first word must be a live verb. The branded parameter type means a call site cannot pass free prose. - Per-surface rendering, at the delivery boundary.
fireresolves references to the CLI spelling immediately, soFiredHint.textstays the canonical wire text for the CLI's--jsonand human renderings, and keeps the authored token form beside it. The MCP completion boundary re-renders each hint from that retained representation before the envelope is observed, recorded, and rendered: a tool-backed verb becomes the tool spelling with flags and positional arguments as its parameters (`discern_start` (name: "…"), hyphens underscored, the CLI-only--jsondropped); a verb with no tool becomes the explicit shell instruction (`discern desk` (in a shell)) — the documented fallback posture, stated where it applies; an owner-executed reference keeps the CLI spelling everywhere, because the executor is a human at a terminal, never the connected agent. - The shell-only half is declared beside the verb→tool source.
MCP_SHELL_ONLY_VERBSlives next to theTOOLStable with a reason per member (promoted from a test-held array, per the declared-source rule). The parity guard reconciles tools + shell-only against the whole verb vocabulary, so a new verb must decide its MCP story the day it is born. The renderer takes the verb→tool mapping fromTOOLSitself — never a second table. - The guards close the class. The command guard validates every reference's CLI rendering against the live CLI model, refuses any runnable command still written as prose, and statically sweeps constructor literals across authored source for branches no example renders. A both-surfaces render pass asserts the MCP rendering never carries a bare CLI spelling for a tool-backed verb (owner relays excepted), ties every tool-less reference to the shell-only declaration, checks each rendered MCP parameter against the target tool's input schema, and keeps
discern_*names out of CLI renderings except for entries declaringdelivery: "mcp". Each rule has a seeded negative. - Recorded hint identity is the registry id — the pre-render reference, not the post-render text. Both boundaries re-associate the in-process identity onto the resolved array before observing and recording, so delivered-hint capture and the follow-through families match identically across surfaces; the logbook lifts ids, timings, and diagnostic classes, never rendered hint prose. The serialization boundary refuses an envelope whose hints still carry an unresolved token.
- The boundary: envelopes are in scope; documents are not. Map pages, fenced examples, and the guidance templates stay CLI-spelled — they are canonical documents, gate-validated as such, read by agents on every surface who then act through whatever surface they are on. Tool descriptions and the MCP instructions block keep their own authoring (ADR 0214). Result
datapayloads carrying command strings (the wire-contract-frozen shapes) are unchanged.
Consequences
§- An MCP-driven agent is never told by discern's own output to leave MCP when a tool exists; an agent on the CLI is never told to call a tool it cannot reach. The residual CLI spellings over MCP are each a declared decision: an owner relay or a shell-only verb.
- A renamed verb, dropped flag, or misspelled tomorrow-hint fails the gate — in the guard, not in a user's session. The drift class is closed rather than patched.
- Hint wording is unchanged: the CLI rendering is byte-identical to the previous prose (the regenerated hint inventory did not change), so nothing shifted for CLI users or existing tests beyond the assert helpers resolving the same way the boundaries do.
- Tests asserting MCP envelopes must use the MCP-rendered assert pair; asserting the CLI spelling against an MCP result is now a failure by design.
- Templates read slightly heavier (constructor calls instead of prose), and hints that fire with references pay a small re-render at the MCP boundary. Both are the price of one representation with two spellings.
- A hint text that loses its in-process identity (an envelope path copying the hints array) would fall back to the CLI spelling over MCP — the documented fallback posture, not a broken instruction; the render-pass guard still holds the registry contract.
Alternatives considered
§- Rewrite prose at the MCP boundary. A regex pass mapping
`discern <verb>`to tool names inside outgoing results. Rejected: multi-word verbs (worktree prune,setup begin), flags that become tool parameters rather than words, and verbs with no tool make prose rewriting fragile — and nothing can guard a rewrite pass except more regex. The house pattern fits instead: one declared representation, per-surface rendering, forcing-function guards. - Structured segments instead of tokens. Templates returning arrays of prose-and-reference parts. Rejected: it rebuilds every template's authoring model and the
FiredHint/wire shapes for the same outcome the token form reaches with template literals intact. - Record the post-render text as hint identity. Rejected: the same firing would record differently per surface, splitting follow-through episodes (ADR 0207) across renderings of one instruction. The registry id is surface-invariant by construction.