Matchers

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:

FormFieldsUse 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.

From a match to rendered evidence The denoised lines of the blamed section are fed to every matcher, which turns each hit into a window either from a pattern with before/after context or from a start/end block; a tail window is added separately. Overlapping windows merge, keeping the highest role they absorbed, whole windows are shed lowest-ranked-first until the budget fits, and what remains is rendered with elision markers. TAIL_LINES PATTERN ±N START…END IN Denoised lines of the blamed section MTCH Every matcher runs over them anchor packs · block packs TAIL Tail window ranked last WIN Window one hit + its context MRG Merge overlaps window keeps the highest role SHED Shed whole windows lowest role, earliest first, until it fits OUT Render … N lines elided … LEGEND Where ranking decides Pipeline step Data in & out
core/extract.py — matching, merging, shedding, rendering.

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.

start
^=+ FAILURES =+
end
^=+ short test summary
python_traceback anchor −1 / +25 lines · tool · runtime

An uncaught Python exception. The exception type and message are on the traceback's last line; the frames above show where it was raised.

pattern
^\s*Traceback \(most recent call last\):
python_lint anchor −0 / +3 lines · tool · lint

A file.py:12: error diagnostic from mypy, ruff, flake8 or pylint.

pattern
^\S+\.py:\d+:(\d+:)? (error|[EFWCN]\d+)

JavaScript / TypeScript

jest block start → end · tool · test

A jest test failure — the ● header names the test, and the diff below it shows expected against received.

start
^\s*●|^FAIL\s+\S+\.(test|spec)\.[jt]sx?\b
end
^Tests:
tsc anchor −1 / +3 lines · tool · build

A TypeScript compile error (error TS2345:) with the source excerpt around it.

pattern
error TS\d+:
eslint anchor −2 / +6 lines · tool · lint

ESLint's line:col error rule-name entries and its ✖ N problems footer.

pattern
^\s*\d+:\d+\s+error\s+|✖ \d+ problems?
pnpm anchor −1 / +10 lines · tool · deps

pnpm's ERR_PNPM_* codes — the resolver or lockfile refusing, which is the cause itself.

pattern
ERR_PNPM_\w+
yarn anchor −1 / +8 lines · tool · deps

Yarn Classic's error <Capital> lines and Berry's ➤ YN####: diagnostic codes.

pattern
^error [A-Z]|➤ YN\d{4}
bun anchor −2 / +12 lines · tool · test, deps

A failed bun test (the ✗ test and its assertion), or a bun install that found no matching version.

pattern
error: No version matching|^\s*✗ |^\s*\d+ fail\b
node_runtime anchor −12 / +6 lines · tool · runtime

An uncaught Node error. The message above the stack is the cause; an ESM/CJS resolution error means a missing or mis-declared module.

pattern
^\s*at .+\(node:internal|^Node\.js v\d+\.|ERR_MODULE_NOT_FOUND|ERR_REQUIRE_ESM|UnhandledPromiseRejection
playwright anchor −0 / +20 lines · tool · e2e

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.

start
^\* What went wrong:
end
^\* (Try|Get more help)
build_failure_banner anchor −3 / +15 lines · wrapper · build

The BUILD FAILURE / FAILURE: Build failed banner Maven and Gradle print once the build gave up.

pattern
FAILURE: Build failed|BUILD FAILURE

.NET

dotnet_test anchor −1 / +12 lines · tool · test

dotnet test: xunit/nunit [FAIL] markers and the Failed! summary line.

pattern
\[FAIL\]|^\s*Failed\s+\S+\s+\[\d|^Failed!\s+-\s+Failed:
dotnet_build anchor −1 / +3 lines · tool · build

MSBuild, Roslyn and NuGet error codes — error CS0103, error NU1101, error MSB3073.

pattern
error (CS|MSB|NU|AD)\d+

Ruby

rspec block start → end · tool · test

RSpec's Failures: block through to the N examples, N failures tally.

start
^Failures:$
end
^\d+ examples?, \d+ failures?
minitest anchor −0 / +10 lines · tool · test

Minitest's numbered 1) Failure: entries and the assertion beneath each one.

pattern
^\s*\d+\) (Failure|Error):
ruby_exception anchor −2 / +10 lines · tool · runtime

An unhandled Ruby exception — a backtrace frame naming the error class.

pattern
\.rb:\d+:in .+\(\w*(Error|Exception)\)
bundler anchor −1 / +6 lines · tool · deps

Bundler failing to resolve — a missing gem, or a version conflict it prints the tree for.

pattern
Could not find gem|Bundler::(GemNotFound|VersionConflict)

PHP

phpunit block start → end · tool · test

PHPUnit's There were N failures block through to its FAILURES/ERRORS/OK footer.

start
^There (was|were) \d+ (failure|error)
end
^(FAILURES|ERRORS|OK)
php_fatal anchor −1 / +8 lines · tool · runtime

A PHP fatal or parse error and the stack trace after it.

pattern
PHP (Fatal|Parse) error|^Fatal error:
composer anchor −1 / +12 lines · tool · deps

Composer's resolver refusing the requirements, with the explanation it prints underneath.

pattern
Your requirements could not be resolved

C / C++

cc_cpp anchor −3 / +8 lines · tool · build

C/C++ builds — CMake configure errors and linker undefined reference / collect2 failures.

pattern
CMake Error|undefined reference to|collect2: error

Language-agnostic

make anchor −3 / +8 lines · wrapper · build

make: *** — make reporting that a recipe it ran failed. The recipe's own output above it is the cause.

pattern
make(\[\d+\])?: \*\*\*
bazel anchor −2 / +10 lines · wrapper · build

Bazel reporting that an action or target failed. A wrapper — the compiler or test output it relays is the cause.

pattern
^ERROR: \S+:\d+:\d+: .*(failed|FAILED)|^FAILED: Build did NOT complete|^//\S+\s+FAILED in \d
oom anchor −5 / +2 lines · tool · infra

The job being killed — exit code 137, Killed, Out of memory. Nothing else in the log explains a SIGKILL.

pattern
exit code 137|Killed|Out of memory
generic_error anchor −5 / +20 lines · fallback

A line starting ERROR or FATAL from a tool with no pack of its own — the weakest signal; read it in context.

pattern
^(ERROR|FATAL)\b

Containers

docker_build anchor −3 / +8 lines · wrapper · build

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 packs
extraction:
  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.