ADR 0172: Hints compile from a hint registry, and the envelope owns all advisory prose
Public-failure amendment (2026-07-28; ADR 0208, classified by ADR 0266): The
hints: string[]wire shape still stands, but every failed public result must now carry at least one fired, registerednext-stephint. CLI and MCP pass results throughwithFailureRecoveryHint: a tailored registered next step passes unchanged; otherwise the boundary appends thefailure-recoveryentry only for an audited error family whose message or first diagnostic supplies the correction.serializeResultrejects a failed envelope that lacks registered actionable recovery or gives a tailored family the generic fallback; plain strings, notices, and guardrails do not satisfy the invariant. The prepared envelope is also the one recorded by the logbook.
Status: accepted
Context
§Every verb returns one result envelope, and its hints[] channel carries advisory next steps to the caller. That channel grew organically: roughly sixty emission sites across the engine and installer, concentrated in status but spread over the gate, standards, coupling, the logbook, the worktree lifecycle, setup, and the command router. A few sites matured into named constants or helper functions (FLEET_OWNERSHIP_HINT, setupUnfinishedHint, the router's did-you-mean pair in the vocabulary module); most remain inline string literals.
A July 2026 audit of the full hint surface, verified against the engine, found the costs of that growth:
- Remedy prose exists that the primary surface never receives. The gate's failed-stage remedy table (
failMessageinfinish.ts) attaches only on the human render path; a JSON or MCP caller — the surface this project declares primary — gets the failure without the stage's remedy sentence.skills ejecthas the same defect in miniature: its human renderer prints an advisory line that is in no envelope. ADR 0028's rule is that every surface renders the same result object; advisory prose that exists only renderer-side violates it. - Tests pin hint wording. Around thirty-five test files assert on fragments of hint prose. Any rewording is a cross-suite find-and-fix, so wording calcifies — the instance-pinned coverage pattern the suite-audit discipline exists to cure.
- Renderers filter by string reconstruction. The human
statusrenderer deliberately drops agent-facing hints, which is legitimate — but it identifies them by reconstructing the exact string and comparing for equality. A one-word edit silently breaks the filter. - Families drift. One underlying fact is worded several ways: three restart-your-session variants, three setup-unfinished variants, six generated-file-drift variants. Nothing marks them as one family, so they diverge independently.
- Nothing validates embedded command references. Hint strings quote real commands (
discern update,discern refresh). The map's fenced examples are validated against the live verb registry on every gate run; hint strings are not, so a verb rename leaves stale advice with no failing check. The launch rename train made this a demonstrated risk, not a hypothetical one.
The glossary work proved the shape that fixes this class: a closed registry as the single source, generated artifacts derived from it, and forcing-function guards that keep live usage honest. Hints are the last large advisory surface outside that discipline, and the logbook is ready to record which hints fire — impossible while a hint is an anonymous string.
Decision
§Every hint is an entry in one closed registry of typed templates, the envelope owns all advisory prose, and fired hints are identified evidence.
The channel contract
§The envelope's three prose-bearing fields have exclusive roles. message states what happened, and on a refusal or failure it carries the remedy. data carries machine-shaped facts. hints[] carries advisory guidance — next steps, guardrails, notices — and never decides success, never confirms an outcome message already states, and never substitutes for data as a data channel.
Renderers render the envelope. A renderer may drop envelope hints that do not suit its surface — the interactive status view dropping agent-facing guardrails stays correct — but it may not add advisory prose the envelope does not carry. The gate's failed-stage remedies therefore join the envelope on every failed run, and the skills eject advisory line moves into its result.
The registry
§One module, src/shared/hints.ts, defines every hint the engine and installer can emit. An entry carries a stable id, a category (next-step, guardrail, or notice), an audience (all, or agent for hints the interactive renderers drop), an optional family name grouping variants of one underlying fact, and a typed template function from named parameters to the rendered string. Entry documentation carries provenance and rationale; rendered text stays self-contained, because shipped strings cite no internal decision numbers.
In process, a fired hint is an id/text pair, so renderers filter by id and the logbook records ids. On the wire nothing changes: hints remains string[], and no public contract or schema moves.
The guards
§Four forcing functions hold the set closed:
- Closed set. An architectural test forbids hint prose from reaching
hints[]except through the registry. Adding a hint means adding an entry; an inline literal fails the gate. - Command references. A test renders every template with placeholder parameters, extracts each
discerncommand it quotes, and validates verb and subcommand against the live verb registry — the same check the map's fenced examples already pass. A rename now fails the hint corpus mechanically. - Renderer parity. Renderers add no advisory prose absent from the envelope; the failed-stage remedy table is enrolled in the envelope and covered so the class cannot silently return.
- Tests assert through the registry. A test asserts a hint by rendering its entry with the case's parameters, never by hand-pinned prose fragments, so rewording flows through the suite without edits.
Telemetry and inventory
§Each logbook verb event records the ids of the hints that run fired — evidence recorded from day one even before a reader consumes it, per the logbook's vocabulary discipline. Pattern detectors over hint follow-through become possible without schema archaeology.
A generated inventory page under the map's _internal tier enumerates every registry entry — id, category, family, audience, trigger, and rendered example — replacing the hand-compiled audit that produced this decision. The audit becomes a regenerated artifact instead of a document that staled on its first edit.
What this deliberately does not do
§There is no public documentation page of hints: they are situational engine advice, and their reference audience is this repository's maintainers and auditors. There is no new verb and no wire-format change. Data-shaped prose currently rendered into hints[] (coupling's per-partner rows, update's pagination lines) is not forced through templates as-is; the wording pass reclassifies it against the channel contract, keeping the advisory framing and leaving the rows to data. No count ceiling or corpus-size standard accompanies the registry — the closed set plus its guards is the enforcement.
Consequences
§- A verb rename or vocabulary change now fails hint prose mechanically, the same way it already fails the map. Stale advice cannot ship silently.
- Rewording becomes cheap: no test pins prose, so the voice pass and every future wording change touch the registry alone.
- The logbook gains hint-fire evidence at near-zero cost, opening follow-through detectors (a hint that fires repeatedly while its advice is never taken is a measurable loop, not an anecdote).
- The interactive renderers' drop-list keys on ids, removing the string-reconstruction equality filter.
- The audit inventory is reproducible on demand and can never drift from the code.
- Adding a hint costs two touches — the registry entry and the call site — instead of one inline string. That is the price of the closed set, and it is deliberate: the entry is where category, audience, and documentation are decided.
- The registry module is large by design; it is the one place to read the whole advisory voice.
- During the staged migration the old inline strings and the registry coexist on the programme branch; the closed-set guard lands only after the last site moves, so the branch, not the trunk, absorbs the transition.
Alternatives considered
§- Keep hints scattered and fix the audit findings individually. Rejected: each finding is an instance of a class (renderer-only prose, pinned wording, unchecked references, family drift), and instance fixes leave every class open. The forcing-function discipline exists precisely for this shape.
- Extend the glossary term registry to hold hints. Rejected: terms are display vocabulary with matching rules; hints are parameterized behavioral templates. The two share guard machinery but not shape, and one registry serving both would strain both schemas.
- Carry hint ids on the wire. Rejected: no external consumer needs ids, and changing the envelope's
hintsshape would churn every published contract for an internal benefit. Ids stay in-process where their consumers (renderers, logbook, tests) live. - A prose file format for hint templates outside TypeScript. Rejected: typed parameters are the point. A template with named, compiler-checked slots cannot silently lose a parameter; a text DSL can.