ADR 0007: verbosity decides which parts of a message print
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
generaterun 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
componentattribute 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, awhyfield in-o json, and in the run log under-qand-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 aslabel: messagewith no level glyph, so a notice keeps its words.attr.Next(cmd)carries the one command, printed as anext:block. Under-qan 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.Hintlogs a hint record, which-qand-skeep.
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
-qand-snow hide every info notice, where a direct print used to show whatever flag was passed. A notice that must survive them carriesattr.Nextor 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
whyas a separate field or only the ref.