A matcher recognises one tool's failure output and pulls the lines
around it into the evidence. 38 ship by default, covering the test frameworks,
compilers, linters and package managers most pipelines run. They are data,
not code — every one is overridable from your own .ci-doctor.yml.
How a pack is shaped
There are two forms, and a pack uses one or the other:
Form
Fields
Use when
Anchor
pattern + before/after
The failure is one recognisable line, and the useful
context is a known number of lines around it.
Block
start + end
The tool prints a delimited region — === FAILURES === through to the summary line.
Both carry a role, which decides who survives when the
evidence exceeds the token budget: fallback windows are shed
first, then wrapper, then tool — and within a role
the earliest, because a job stops at its first failing command. A wrapper
reports that something it ran failed: npm ERR! always trails the
compiler errors that caused it, so it loses to tsc wherever it
sits. Every regex is RE2 — see
Configuration for the syntax.
Anchor on the cause, not the summary. A tool's final
2 tests failed line comes after the detail explaining
why, so a summary-only anchor windows straight past the evidence. Anchor on
something only your tool prints, too: a bare ^ERROR: .*failed also matches
ERROR: Job failed: exit code 1, the line that ends every
failed GitLab job.
What happens to a match
Matching is only the first step. Every hit becomes a window, windows
that touch are merged, and role and position decide which ones survive the
budget — all of it in
core/extract.py, before a model is ever involved.
Two details follow from this. Merged windows take the highest role
they absorbed, so a good pack never loses rank by sitting next to a weak one.
And shedding stops at one window: a single unbounded start/end block can exceed the budget alone, and cutting inside it is
core/budget.py's job, not the matcher's.
The shipped catalogue
Generated from config/defaults.yml, so this is the 38 packs
that actually ship, in the order they are declared. anchor packs list the context lines they keep either side of the hit; block packs run from their start to their end.
Python
pytest
block
start → end
· tool
· test
pytest's === FAILURES === block, from the first failing test through to the short summary.
A Playwright test failure — the 1) suite › test header names the test; its trace and diff follow.
pattern
^\s*\d+\) .+ ›
cypress
anchor
−3 / +12 lines
· tool
· e2e
A CypressError or a retry timeout, with the command log leading up to it.
pattern
CypressError|Timed out retrying after \d+ms
npm
anchor
−2 / +10 lines
· wrapper
· deps
npm ERR! lines. A wrapper: npm reports the failure of the script it ran, never the cause.
pattern
npm ERR!
js_script_exit
anchor
−1 / +4 lines
· wrapper
A JS package manager announcing that a lifecycle script exited non-zero — pnpm's ELIFECYCLE, bun's error: script "x" exited with code N.
pattern
^\s*ELIFECYCLE\b|error: script ".+" exited with code
Go
go_test
anchor
−2 / +15 lines
· tool
· test
go test's per-test --- FAIL marker, plus the assertion output printed under it.
pattern
^--- FAIL
go_panic
anchor
−2 / +20 lines
· tool
· runtime
A Go panic: or runtime fatal error: with the goroutine dump beneath it.
pattern
^panic: |^fatal error:
Rust
rust_test
block
start → end
· tool
· test
cargo test's failures: block through to the test result: line.
start
^failures:$
end
^test result:
rust_panic
anchor
−2 / +8 lines
· tool
· runtime
A Rust panic, with the message and the frames that follow it.
pattern
^thread '.*' panicked at
rust_compile
anchor
−0 / +8 lines
· tool
· build
rustc refusing to compile (error[E0308]). The first error is usually the cause; later ones often follow from it.
pattern
^error\[E\d+\]: |^error: (could not compile|aborting due to|linking with)
JVM (Maven / Gradle)
maven_gradle
anchor
−3 / +15 lines
· tool
· build
Maven's [ERROR] lines — the compiler and test diagnostics Maven relays.
pattern
\[ERROR\]
gradle
block
start → end
· wrapper
· build
Gradle's * What went wrong: block, stopping before the * Try: boilerplate advice. A wrapper — it names the failed task, the compiler output above it is the cause.
BuildKit giving up — failed to solve / executor failed running — around the Dockerfile step that failed. A wrapper: the RUN step's own output is the cause.
pattern
failed to solve|executor failed running
Infrastructure as code
terraform
anchor
−1 / +12 lines
· tool
Terraform's boxed │ Error: and the on main.tf line 12 it points at.
pattern
^\s*│\s*Error: |on .+\.tf line \d+
Adding your own
Matcher lists merge by id, which is the one exception
to "lists replace". A new id is added alongside the shipped packs:
# .ci-doctor.yml — a new id is *added* to the shipped packsextraction: matchers: - id: my_service_errors role: tool classification: [any] description: My service's own error lines. pattern: '^\[myservice\] (ERROR|PANIC)' before: 3 after: 12 examples: match: ['[myservice] ERROR db unreachable'] no_match: ['[myservice] WARN slow query']
The description is not only for this page: the LLM sees it beside
the lines your pack selected. The model knows public tools, not your in-house
ones — so say what the tool is and what its failure means. The
examples are checked by the test suite for shipped packs only;
in your own config they document intent but are not run yet.
Reusing a shipped id retunes that pack instead, field by field:
# A shipped id *overrides* that pack, field by field, and logs a warning.extraction: matchers: - id: pytest role: wrapper # start:/end: are inherited, not blanked
Only the fields you write are overridden — setting role on
pytest keeps its start/end,
because blanking them would leave a matcher that can never fire.
ci-doctor logs a warning naming any shipped pack you override, so an
accidental id collision is visible rather than silent.
Check it actually fires
The most common mistake is a pack that never matches. It fails silently: the tail window still returns output, so the report looks fine while
your matcher does nothing. Replay a real failing log and confirm your
lines are in the evidence:
# Prove it fires before you trust it — a matcher that never matches# fails silently, because the tail window still returns something.ci-doctor analyze path/to/failing-job.log
Contributing a pack upstream? It needs a fixture log for both providers
plus an expected verdict — see GUIDELINES.md §5.1. The concepts page explains where
extraction sits in the pipeline.