Skip to content

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, registered next-step hint. CLI and MCP pass results through withFailureRecoveryHint: a tailored registered next step passes unchanged; otherwise the boundary appends the failure-recovery entry only for an audited error family whose message or first diagnostic supplies the correction. serializeResult rejects 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 (failMessage in finish.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 eject has 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 status renderer 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 discern command 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 hints shape 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.
choose openEsc close