magus v0.4.3 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
5 min read

ADR 0007: verbosity decides which parts of a message print

Decision accepted on .

Each item below carries its own state: done is in the tree, in progress sits on a branch, proposed is decided in outline and not built, and not built was weighed and declined.

Context

magus printed every reason it had, every time. Measured on this tree before this decision:

  • A raw-tool denial ran seven lines for a one-line problem: the bootstrap was chained to tail.
  • The read-navigation denial printed a map of up to sixty declarations.
  • One generate run printed MGS4007 sixteen times, once per step.
  • MGS7003 wrapping MGS3035 printed two see: links and split a sentence between them.
  • Lock and upstream waits logged every fifteen seconds.

Cutting the text for everybody costs the person at a terminal. Waits that stay silent for a minute read as a hang, and a diagnostic stripped of its reason sends a person to the docs page for every error. A run that asks for less, with -q or -s, has no use for either.

Decision

A message is typed parts, not one string

Done. Every message magus prints is built from four parts:

  • The verdict is one sentence naming the problem.
  • The next command, at most one, acts on it.
  • The why carries the rationale, as long as it needs to be.
  • The ref names where the full text is kept: magus query output <ref>, or a docs page.

Each producer carries the parts as fields, never as one joined string:

producer verdict and next why
diagnostics.Error Msg Why, set by WithWhy
guard ShellVerdict Deny, Next Why
slog record the message an attr.Why(...) attribute

A wait or heartbeat record carries attr.Elapsed(d), which is how the display tells progress from news.

The diagmsg analyzer judges the verdict alone: a length cap, one causal clause, one command and no leading tag. A why is exempt, so a reason never has to be cut to pass lint.

The verbosity flags pick the parts

Done. No new setting decides this. -q, -s and log.silent (MAGUS_LOG_SILENT) already ask for less, and quiet.Wrap wraps the display whenever one of them is on:

part default -q / -s
why a dim second line in the run log, reached through the ref
waits from the first beat, then at each doubling quiet until a minute, then at each doubling
console the link, how to open it signed in the one command that opens it
repeats folded into one footer line with a count the same
component the name before the message the same

-v brings why back under -q or -s. -o json and -o jsonl carry every part as fields, since a parser selects what it shows. The run log keeps everything.

Two commands are quiet without being asked, because no person reads their stderr: the guard when installed hook glue calls it, and the MCP server. The harness configuration magus describe harness prints sets MAGUS_LOG_SILENT=true where the host has a place for it, which is -s on every command run there.

What this branch already does

  • Done. Guard denials print the verdict, one command and the ref, and the rationale moves into the stored verdict. A search denial keeps the graph's answer inline. Advisories print one line of advice and a ref the same way.
  • Done. A nested diagnostic prints one see: link and names the inner code inline.
  • Done. A diagnostic repeated across steps prints once, then one footer line with the count.
  • Done. slog messages carry a component attribute instead of a name tag in their text, and the pretty display still prints the name before the message.
  • Done. Lock and upstream waits log at their own cadence, each note carrying attr.Elapsed; a quiet display holds back the notes under a minute.
  • Done. MGS3035, MGS4007 and MGS7003 keep their short verdicts and carry their reasons as why: dim under the cause by default, a why field in -o json, and in the run log under -q and -s.

Every stderr line but help is a record

Done. Output takes one of three channels. Results go to stdout through the output emitters. Help prints to stderr directly, and only when asked. Every other stderr line is a slog record the display renders, so the verbosity flags, -o jsonl, redaction and the component name reach it:

  • attr.Notice(label) marks a line addressed to a person. It prints as label: message with no level glyph, so a notice keeps its words.
  • attr.Next(cmd) carries the one command, printed as a next: block. Under -q an info record survives as that command alone; the console link after a job verb is one.
  • attr.Error(err) carries the error, never the message: the label names who is speaking and the error names where it failed, and a shared origin prints once.
  • interactive.Hint logs a hint record, which -q and -s keep.

The stderrprint linter reports a write to os.Stderr outside a usage function and the display, and errmsg's error-notice reports an error built into a notice's message. Each file still allowed to write stderr names why: it runs before the display exists, relays a child's bytes, speaks a hook protocol, or prompts a person who must answer.

Alternatives

  • A setting naming who reads the output. The verbosity flags already say how much a run wants, and a second knob would let the two disagree.
  • Rewriting prose at render time. Summarizing a message by model or by pattern is nondeterministic, adds latency to every line, and nothing can test it.
  • A branch at each call site. It spreads one policy across every message and drifts the first time a site forgets it.
  • A second catalog of short wording. Two texts for one message drift apart, and nothing reports which one went stale.
  • Dropping a repeated origin at render time. The display could strip a leading name: a message already carries, but a message is prose and the guess would cut text that only looks like a prefix. A typed error attribute says which part is the origin.

Consequences

  • -q and -s now hide every info notice, where a direct print used to show whatever flag was passed. A notice that must survive them carries attr.Next or is a hint.
  • A notice rendered from a record moves from stdout to stderr where a command printed it beside a result, such as the console line after a job verb.
  • A new stderr line has to be a record, help, or an allow entry with a reason, so the three channels stay apart without review catching each one.

Open questions

  • Whether an MCP tool result carries why as a separate field or only the ref.
adrdecisionoutputloggingdiagnosticsguardverbosity
Last updated (3269ac7f)
Earlier changes on this page (1)

Full history ↗ · Blame source ↗

Glossary

Op

A single tool-native command a target composes (long form: operation); the middle of the work hierarchy (Spell to Op to Target). Op in full.

CI

An ordinary target you compose yourself with ctx.needs; magus does not hardcode its stages. It is the anchor magus affected ci keys off, and ci strips the rw charm. CI in full.

Latency

How long an operation takes. magus records latency as OpenTelemetry histograms per family (target execution, cache op, pool wait, and graph query) and reports each as a count, sum, and percentiles. Latency in full.

Tool

An action a model can call through its host: a name, a description, a typed input and a result. magus serves its tools over MCP, each a read of the workspace model or a run of the CLI it already has. Tool in full.

Harness

The deterministic scaffolding around the work on a change, for a person or an agent: compiler errors, formatters, linters, checks, gates, leases and hooks, placed at points a person chose. Around an agent's loop, magus supplies one and keeps it outside. Harness in full.

Job

The unit of delegated work, and one row of the job store: what an orchestrating agent handed out, with its goal, the checkpoint it was cut against, the paths it may write or must not touch, and the one check it runs. A job's holder is either a session, for work an orchestrator handed out, or the daemon, for its own maintenance. The store records; the agent guard is what reads those facts back when grading a write. Job in full.

Run

One target executing under one magus invocation, such as magus run test web or magus affected ci. A run keeps its captured output behind an output reference. Every magus run is a run whether or not any job asked for it; see Job for how the two relate. Run in full.

Advisor

One read-only check from the advice suite: it reads the changeset through magus and writes one titled section of findings. The same advisors run as a pull request comment in CI and inside magus diff --impact locally. Advisor in full.

Conventions

Placeholders

Angle brackets mark a value you replace with your own; never type the brackets:

magus run <target>
magus completion <shell>    # e.g. bash, zsh, fish

<target>, <path>, <shell>, <name> and the like are stand-ins, not literal text.