Evidence
Attribution picks a section; this is everything that happens to it afterwards. Four passes, in order, each one narrowing what a reader — or a model — ever sees.
Denoise
Cutting volume before anything is selected. Per line: strip ANSI
escapes, collapse
\r progress-bar rewrites to their final state, optionally drop
the runner's timestamp prefix, then drop configured noise patterns. Finally,
runs of identical lines fold into one with a (×47) count.
A line that looks like an error is never removed by a noise pattern — the escape hatch that stops an over-broad rule from deleting the one line that explains the failure.
Matcher, window, role
A matcher is a regex that recognises one tool's failure output. Where it fires, it anchors a window — the run of lines actually worth showing. Overlapping windows merge; the space between them is replaced by an explicit count.
# denoised log, blamed section
Compiling ledger v0.4.1
error[E0308]: mismatched types <- matcher anchors here
--> src/posting.rs:88:31 |
| | window: the lines kept
88 | let rate: Decimal = ... |
= note: expected `Decimal` <-
… [14 lines elided] … <- the gap is announced, never silent
error: could not compile `ledger`
Role decides who survives when the evidence exceeds the
budget: a tool's window outranks a wrapper's, which
outranks the fallback. npm ERR! always trails the
compiler errors that caused it, so it must lose to tsc. The full
catalogue — every shipped pack, what each one catches, and how to add your
own — is on Matchers.
Budget
The evidence has to fit the model's input limit. Two passes, in order: shed whole windows, lowest-ranked first, then trim whatever survived. Trimming keeps the tail, because the failure lives at the end of a log — and it announces what it dropped.
The order matters. Trimming alone would keep the tail of everything, and a tool that reports its cause before its own epilogue would lose the diagnosis and keep the complaint. Token counting is a four-characters-per-token estimate; no tokenizer is loaded, and nothing is downloaded to do it.
Redaction
Scrubbing secrets out of everything that leaves the process. A local
regex set (private keys, glpat- tokens, AWS keys, Authorization: headers, credentials embedded in URLs) plus the literal values of any environment
variable whose
name looks secret — a stand-in for the CI's masked-variable list,
so a token with no recognisable shape is still caught.
It runs twice: on the prompt before it is sent, and on
the finished report before it is printed or posted. Once is not enough —
a model can quote a secret straight out of the log it was shown. There
is no entropy scanning and no network call; replacements read [REDACTED:<kind>] so the report stays legible.
The evidence bundle
Everything the report step gets, and all it gets: the blamed phase, its denoised and budgeted lines, one non-causal note per warning-only phase, job metadata, the token estimate, and whether anything was truncated. Truncation is always surfaced — a silent cut would be a lie about the evidence.
… [N lines elided] … wherever they happen — between windows,
and again if the budget had to trim inside one. If you never see a marker,
nothing was dropped.
Watching this run is one flag: ci-doctor analyze -v prints a
denoise → extract → fit line with the surviving line count after
each pass, and whether the budget truncated. See the CLI reference.