Inspiration
On January 31 2017, GitLab.com lost production data. Backups had been failing for a while, and the emails that reported it were rejected, so nobody knew (GitLab's postmortem). The monitoring existed. It had never been tested against the failure that mattered.
Code has mutation testing: break the code on purpose and check that a test notices. Alerting rarely gets the same treatment. Rules and routing are edited by hand, syntax-checked at best, and their first real test is a real outage at 3 AM. We wanted every merge request to answer one question before it merges: if this change breaks something, does the right person hear about it?
What it does
On each merge request, the page-check job starts the real app (Cedar Court, a small dues API) and breaks its one dependency, a holiday calendar, in eight ways while steady user traffic runs: it hangs, returns 503s, slows 1 request in 4 by 700 ms, returns truncated JSON, refuses connections, returns 200 with an error inside, answers 1 in 2 with 503 and retry-after: 0 (inviting a retry storm), and fails 1 call in 100. Plus a quiet day with a healthy calendar.
- Record in real time. 60 s quiet, then 6 minutes of fault at 10 user requests per second. The app's own
/metricsis scraped every 5 s; a recording whose scrapes drift more than 500 ms is rejected. The load generator records what users actually got. - Evaluate the repo's own rules, unmodified, with Prometheus' engine.
promtool test rulesreplays every recording againstmonitoring/rules.yml, sorate(),for:and burn-rate windows behave exactly as in Prometheus. - Route every alert that fired through
monitoring/alertmanager.ymlwithamtool. - Score. If users were hurt, a page must reach
gitlab-pagerwithin 5 minutes. If the app's fallback held, a ticket must reachgitlab-tickets, and a page is a false page. The quiet day must fire nothing. Every miss is named as a code, rule or routing gap, and the job fails on any miss or false page.
route-check checks that every alert the rules define reaches the receiver its severity needs. page-live sends every alert that fired through a real Alertmanager with the repo's config into a webhook sink inside the job.
Two modeled inputs, both printed on every scorecard: the 1 h and 6 h rule windows need history a 7-minute recording can't have, so each replay starts with 6 hours of modeled quiet traffic at the rate measured in the same run, with zero errors; and the slow-burn row is computed on a modeled 6-hour continuation at the error ratio measured in its recording. Everything else is recorded from the real app.
When a pipeline fails, a GitLab Duo Agent Platform flow with four agents wakes up (trigger: Pipeline events, Failed):
- Gate reads the pipeline event and hands the flow to the pager agent only when
page-checkfailed on a merge request pipeline. A red unit test never wakes the pager agent, and neither does a failed pipeline on the flow's ownpms-repair-branch. - Pager reads
scorecard.json, the diff, the rules and the routing, points each missed outage at the line behind it, commits one bounded repair on a new branch, opens a Draft merge request, and comments on the original merge request. If the fix is out of bounds (a harness change, a new dependency, a product decision), it files aMonitoring debt:issue instead. - Skeptic reviews only the Draft's diff, with no pipeline tools, against fixed rules: no alert deleted or disabled, no trivially low threshold, no
for:lengthened to dodge, no edits to the harness or pipeline, no page route withoutseverity="page", and the outbound call and its metrics kept. It postsSkeptic: APPROVEorSkeptic: REJECT. - Pages is built to turn each page
page-livedelivered into a GitLab incident (not run live yet).
The Draft's own pipeline reruns the same mutants with the same harness. That pipeline is the proof, not the agents' word. The flow never merges, approves or enables auto-merge; a person decides.
The live run on GitLab.com (Oct 25 2026, Mountain Time)
Everything here is on public pages of the project. Nothing has been merged.
- MR !1 (
calendar-liveintomain) switches the next-due calculation to a live holiday calendar. It is a demo merge request: we seeded its monitoring with four gaps on purpose, each marked with a "Seeded gap" comment in the files, and its new calendar client trusts any 200 response. Its pipeline 2988040064 failed at 11:01:13 PM:page-check,route-checkandpage-livered. The page-check job scored it 0 of 8 outages caught: 4 routing gaps, 3 rule gaps, 1 code gap. Pages did fire for four of the outages, but the pager route matchescedar_courtwhile the alerts carrycedar-court, so all of them landed inblackhole; the page-live job replayed 5 pages and delivered 0. - The flow woke up. At 11:01:15 PM, two seconds later, workload pipeline 2988067872 started as the flow's service account and ran for 3 minutes.
- The repair. The flow created branch
pms-repair-1and pushed commit 932a96f2, authored by the service account: the pager route matcher, the latency rule's route selector, a dependency ticket rule'sfor:cut from 15m to 2m, a new slow burn-rate tier, and a calendar client with a timeout, a body check, a snapshot fallback and a metric on every attempt. It opened Draft MR !2 intocalendar-liveand posted a table on !1 with each missed outage, the line behind it and the fix. GitLab lists lume grid (lumegridai) as the author of !2; its commit and pipelines are by the service account. - The skeptic. On !2, the flow posted
Skeptic: APPROVEwith all six checks (R1 to R6) passing, read from the diff only. - The proof. !2's pipeline 2988078421 passed every job. Its page-check job scored 8 of 8 caught (2 paged, 5 ticketed, 1 absorbed with nobody woken), 0 false pages, with the same harness hash as !1's run (
a1cc7900494b, taken from the target branch). Its page-live job delivered 2 pages togitlab-pagerand 6 tickets togitlab-ticketsthrough a real Alertmanager, 0 to nobody.
The flow is public in the AI Catalog: https://gitlab.com/explore/ai-catalog/flows/1019767. Its YAML is also in the repo at .gitlab/duo/flows/pager-mutation-score.yaml.
How we built it
- GitLab Duo Agent Platform: one custom flow, four agent components (gate, pager, skeptic, pages) joined by a router on the gate's one-word answer. Trigger: Pipeline events (Failed) plus Mention. Tools come from GitLab's tool registry only; the skeptic gets no pipeline tools.
scripts/validate-flow.pychecks the YAML against GitLab's ownflow_v2.jsonschema, then checks every router target, prompt input and tool name. - GitLab CI/CD:
test,page-check,route-check,page-live, GitLab's SAST and Secret Detection templates, and areleasejob that publishes the scorecard on a tag (built, not yet run on a tag). Scorecards (.svg,.md,.json) andpages.jsonare exposed as merge request artifacts. - Prometheus 3.15.1 and Alertmanager 0.34.1 from the official release tarballs, checked against pinned SHA-256 sums in the job.
promtoolevaluates the rules;amtoolroutes; a real Alertmanager delivers inpage-live. - Node.js 20+,
node:test, no runtime dependencies beyondacorn. The pipeline needs no paid services, no CI variables and no alert integration. - A hash-locked harness. On a merge request,
page-checkruns the target branch's harness (mutants, expectations, recorder, scorer) against the merge request's app and monitoring, and fails if the merge request changed the harness. CODEOWNERS guards the pipeline itself once code owner approval is switched on.
GitLab lifecycle stages that ran live on this project: Create (the repair on Draft !2), Verify (page-check and route-check red on !1, green on !2), Configure (alert rules and routing as code, evaluated by Prometheus' own tools), Monitor (page-live delivering through a real Alertmanager, green on !2) and Govern (the hash-locked harness, SAST, Secret Detection, and the skeptic's review). Built but not yet run live: Plan (Monitoring debt: issues), Release (scorecard on a tag) and incidents from the pages agent.
Challenges we ran into
- Honest time. Burn-rate rules with
for:clauses only mean something on real timing, so the recordings run in real time andpromtoolreplays them at the same 5 s cadence. When a busy machine stalled twice for 13 to 20 s during a local run, every recording had a scrape too far from its slot, andpage-checkrefused to score all 36 instead of guessing. - History we can't record. 1 h and 6 h windows need hours of traffic. We model a quiet lead-in from the same run's measured request rate, with zero errors, and print that on every scorecard rather than hide it.
- The trigger's Instructions field. With instructions set, the flow receives them plus a short context block instead of the pipeline event, so the gate has nothing to read. Leaving Instructions empty hands the flow the raw pipeline event.
- The flow schema. The flow editor validates against a stricter schema than the prose docs: no top-level name or description, no
max_cycleson agents. Each prompt ends with its own stop rule, and we validate locally before pasting. - Developer access only. We couldn't add an alert integration or masked variables, so
page-liveruns a real Alertmanager into a webhook sink inside the job, guarded by a Bearer key generated in that job. It never claims GitLab received a page.
Accomplishments that we're proud of
- A real merge request on GitLab.com went red for the right reason; two seconds later a flow woke up and wrote a repair that a second agent reviewed. !1's pipeline scored 0 of 8; the repair's own pipeline scored 8 of 8 with the same harness. Nothing was merged.
- Every alert time and route in the scorecard comes from
promtoolandamtool, every first-alert time is asserted back topromtool, and the saved test files replay withpromtool test rulesalone. - The scorecard names each miss as a code, rule or routing gap, and the pager agent pointed each one at its line (the four monitoring gaps are labeled "Seeded gap" in the files; the client gap is not): a pager route typo, a rule on a label the app never emits, a
for:that outlives the deadline, a missing burn-rate tier, and a client that trusts a 200 with an error inside.
What we learned
Alerting fails quietly and in layers. In the bad merge request, alerts did fire for four of the outages, and the pages still reached nobody because of one character in a route. Rule tests alone would have passed. route-check catches that typo in seconds, but only the full path, from real metrics to a delivered page, shows which outages users would have felt with nobody paged.
What's next
- Run the pages agent and
Monitoring debt:issues live, and publish the scorecard as a release on a tag. - More dependencies and mutant shapes (databases, queues), and a forward to a real alert endpoint by default.
- Score a project's existing alerting against its real traffic shape instead of a modeled quiet lead-in.
Prior art
promtool unit tests use hand-written series; chaos tools can check a monitor during an experiment. Pager Mutation Score records series from the real app under real faults, scores them on every merge request, and pairs an agent that repairs the gap with a second agent that checks the repair.
Try it
Node 20+, git, curl and sha256sum. One command scores the demo merge request and its repair, in parallel, in about 8 minutes:
npm ci --ignore-scripts && npm run demo
To use it on your own GitLab project: push the repository, add the flow (AI > Flows > New flow, or from the AI Catalog), then add a flow trigger with Pipeline events (Failed) and Mention, leaving Instructions empty. Steps are in the README.
Built With
- alertmanager
- amtool
- gitlab-ci
- gitlab-duo-agent-platform
- javascript
- node.js
- prometheus
- promtool
- python
Log in or sign up for Devpost to join the conversation.