Inspiration
I opened the "add monitor" menu of Uptime Kuma, the 93k-star self-hosted monitoring tool, and counted 27 monitor types. Our demo instance used a handful of them. The gRPC monitor had zero users, yet it kept 21 packages installed, and those packages carried 15 known vulnerabilities, one of them critical.
Code that gives users nothing isn't free. It keeps costing you:
- Maintenance: dependency bumps, patch releases and CI minutes, all for a feature nobody uses.
- Attack surface: every install ships its vulnerabilities, used or not.
- Onboarding: a new developer has to read it, understand it and be careful around it before they can change anything nearby.
- LLM context: coding agents read it too. Dead code fills their context window and gets copied as if it were a pattern to follow.
- Value per line: the codebase gets bigger while what it does for users stays the same, so every line you keep is worth a little less.
Code generation made this worse. We now add code faster than ever, and every CI pipeline automates adding: review, test, scan, ship. Nothing automates taking it away. Nobody deletes the dead feature either, because you can't be sure nobody uses it, and nobody wants to be the one who broke it.
Undertaker is for platform teams who maintain a fork of a big open-source tool, and for any team with an old codebase. It retires dead features the way a hospital declares death: with evidence, an observation period, a signature, and a way back.
Code generation is solved. Code deletion isn't.
What it does
Undertaker runs a feature's whole funeral in GitLab. Humans make three decisions. Everything else is a GitLab Duo flow or deterministic CI.
- Inquest (CI): a scheduled pipeline scores every optional feature from evidence: live usage from the running app's
/metrics, git history, tests, code size, and the vulnerabilities and size of the packages only that feature keeps installed. - Death certificate (CI): each feature above the threshold gets an issue with the evidence: "0 of 12 monitors use it, yet it keeps 21 packages (5.2 MB) installed with 15 known vulnerabilities (1 critical, 8 high, 5 medium, 1 low). Burial score 68/100."
- 1 · Sign (human): you assign the certificate to the Undertaker service account. That starts the Duo flow.
- Life support off (Duo + CI): the flow's first agent opens merge request 1, which adds the feature to
life-support.yml. A CI gate merges it only if it touches that one file and its pipeline passed. A sync job turns on GitLab Feature Flags, and within seconds the option disappears from the menu in the live app, with no deploy and no reload. Every attempt to use it is blocked and counted. - Burial, prepared (Duo): in the same run, a second agent opens a draft merge request 2 that deletes the feature's code, UI, translations, tests and the dependencies only it used. It checks its own work with grep, ESLint and the backend tests.
- Observation (CI): if anyone tries to use the feature, it is revived: the flags go off, it works again for everyone, and the draft burial is closed. If nobody misses it for the whole window, the burial is marked ready.
- 2 · Accept the burial (human): you review and merge merge request 2. Its pipeline scans and builds the smaller app.
- Grave and obituary (CI): a
grave/<feature>tag keeps the deleted code, and a release lists what was removed, measured, not estimated. - 3 · Ship (human): staging deploys to Google Cloud Run on its own, and production ships when a human runs the manual production deploy.
Results from our run on Uptime Kuma 2.5.7: three death certificates. MongoDB was revived because someone tried to add a MongoDB monitor during observation. gRPC and SQL Server were buried, and the smaller app is live in production:
| What the burials removed | gRPC | SQL Server | Total |
|---|---|---|---|
| Lines of code (net) | 604 | 912 | 1,516 |
| Production packages | 21 | 46 | 67 (652 → 585) |
| Known vulnerabilities in production dependencies | 15 | 1 | 16 (79 → 63) |
How we built it
| Piece | What it does |
|---|---|
undertaker.yml |
Feature map (monitor types → files → dependencies only they use) and burial policy. Read from the default branch, so a merge request can't loosen its own rules |
undertaker/inquest.mjs |
Deterministic evidence and score; opens and refreshes death certificates |
.gitlab/duo/flows/undertaker.yml |
The Undertaker Duo flow: two AgentComponents (life support off, burial) with separate toolsets, triggered by Assign |
undertaker/gate.mjs |
Merges a life-support merge request only if it changes nothing but life-support.yml and its pipeline passed |
server/undertaker-life-support.js |
Inside Uptime Kuma: polls GitLab Feature Flags (Unleash) every 15 s, hides and blocks switched-off types, pushes the change to open browsers, exports undertaker_* metrics |
undertaker/observe.mjs |
Revival, or "observation passed" |
undertaker/obituary.mjs |
Grave tag and release with measured removals |
.gitlab-ci.yml |
build → test → inquest → package → deploy, with SAST, secret detection, dependency scanning, Container Registry, staging and production environments on Cloud Run |
The burial score is plain code, not a model:
$$\text{score} = 100 \cdot \text{lowValue} \cdot (0.5 + 0.5 \cdot \text{risk})$$
$$\text{lowValue} = 0.7 \cdot [\text{no live monitors}] + 0.3 \cdot \text{staleness}$$
Risk is a weighted sum: vulnerabilities 0.4, removable install size 0.3, no tests 0.2, code size 0.1. Value decides whether a feature is a candidate; risk decides how urgent it is. For gRPC that is 100 × 0.94 × 0.72 = 68.
Where Duo is used: the flow does the parts that need judgement about code, such as wiring the switch-off and doing the surgery across code, UI, tests and package.json. It never decides what is dead, never merges its own deletion, and never deploys. Each agent only gets the tools its job needs. The life-support agent can't edit files, and only the burial agent can run commands.
All 9 post-code stages are covered: plan, create, verify, package, secure, release, configure, monitor, govern. The automation took about 30 commits on top of Uptime Kuma's full history, and the 186 existing backend tests run in CI.
Challenges we ran into
- The gate cancelled itself. When the gate merged merge request 1, the merge started a new pipeline on
main, and auto-cancel killed the gate job right after its merge. Fix: the gate, observation and obituary jobs are marked non-interruptible. - Buried features came back in production. The obituary deleted the feature flags as soon as the burial merged, but production still ran the old code until its deploy, so the feature reappeared there. Fix: flag cleanup now runs only after
deploy:production. - Two numbers for the same packages. The certificate said 15 vulnerabilities and the obituary said 2, because
npm audit's total counts vulnerable packages, not advisories. Fix: both count advisories, so before and after agree. - Flow triggers need a human. A CI job can't start a Duo flow. So the original two-flow design (switch off, then bury later) needed a second human trigger in the middle. Fix: one flow does both in a single run, leaves the burial as a draft, and CI observation decides whether it becomes ready. That turned out to be the cleaner Supervised design: humans sign, accept and ship.
- Cloud Run forgets everything. Each revision starts with an empty SQLite database, so the inquest saw zero monitors. And on a fresh database the server sent its
setupevent in the same burst as the connection, so our seed script missed it. Fix: seed jobs run after every deploy and listen before connecting, and/metricsis read with stable admin credentials. - "Gone" had to mean gone right away. Hiding a type on the server didn't change menus that were already open. Fix: when the set of switched-off types changes, the server re-sends its info to every connected socket.
Accomplishments that we're proud of
- The safety net fired for real. A user tried to add a MongoDB monitor during observation. Undertaker revived it in one pipeline, with no deploy, and closed its burial with a comment saying why.
- The live app changes in front of you. A merge request merges and the option vanishes from an open browser seconds later.
- The LLM is fenced in by design, not by prompt. Scoring is deterministic, the rules are read from
main, the gate checks the diff scope, and the deletion needs a human merge. - Every cost from the list went down at once. Two burials removed 1,516 lines that nobody has to maintain, onboard onto or load into an agent's context. They also removed 67 packages that need no more patching, and 16 known vulnerabilities that no install ships anymore.
- It's subtraction. Most agentic tools add code. This one made a real 93k-star project smaller, and each deleted feature is still one
git revertaway.
What we learned
- Let deterministic code decide and let agents do the work. Judges, reviewers and on-call engineers can trust a score they can read. They can't trust a model's opinion that something is "unused".
- "Disable, observe, delete" is what makes AI deletion safe. The flag step turns an irreversible action into a reversible one, so people are willing to say yes.
- Count the same thing everywhere. If two reports give two different numbers for the same packages, nobody believes either one.
- Design around your platform's limits. "Only humans can trigger a flow" looked like a blocker. It became the governance model.
What's next for Undertaker
- Real observation windows: the default is already 14 days (
UNDERTAKER_OBSERVATION_HOURS=336). The demo used 9 minutes, and says so. - The rest of the monitor types: six more features are mapped in
undertaker.yml(GameDig, Steam, Oracle, SNMP, SFTP, Playwright) and go through the same inquest every day. - Notification providers next: Uptime Kuma ships 110 of them, which is the same problem with more candidates.
- Drafting the feature map: an agent proposes
undertaker.ymlfor any codebase, and a human reviews it once.
Built With
- docker
- eslint
- express.js
- gitlab
- gitlab-ci
- gitlab-container-registry
- gitlab-dependency-scanning
- gitlab-duo-agent-platform
- gitlab-feature-flags
- gitlab-releases
- gitlab-sast
- gitlab-secret-detection
- google-cloud
- google-cloud-run
- javascript
- knex
- node.js
- prometheus
- socket.io
- sqlite
- unleash
- vue.js
- yaml

Log in or sign up for Devpost to join the conversation.