The Factory Files a Ticket Against Itself: Two Repos, One Session, Three Gaps
9 min read
On September 1 I admitted my two healthcare-domain repos, fi-fhir and edilint, to Mills, the autonomous delivery system that runs on my cluster. Over the next two days it merged a fixture family and a FHIR CapabilityStatement I did not write, for a few dollars total. That part I expected. The part worth writing down is what it found: a test gate that had been passing on the wrong tree for weeks, a CI watcher that refused the only pipeline the repo produced, and a bookkeeping bug that left 269 escalation records unable to close. Every one of those fixes is a ticket the factory picked up against itself.
TL;DR
- Admitting a repo to Mills is two GitOps edits, and the first run failed in nine seconds and zero dollars because the second edit was missing. Cheap failure was the design working.
- Mills merged four items unattended across the two repos, including a
GET /metadataCapabilityStatement derived from fi-fhir's real mapper surface with a reflection-based drift guard. Each merge is graded keep or regret afterward. - It exposed three gaps in itself: a CI watcher that only accepted push pipelines, a tests stage that ran against the sandbox's copy of
maininstead of the pushed commit, and an escalation record that froze the wrong project name. All three were filed as backlog and fixed through the same pipeline. - The next day the freshly pinned tests stage caught that an existing test on
mainstill encoded an old rule, and Mills escalated instead of editing the test. That is the division of labor the whole thing exists for. - Gates only help if you audit what they actually checked. The run records now carry
tested_shafor exactly that reason.
Context: how a config-sync tool became a factory
Mills did not start as a factory. Each layer exists because the layer under it got boring enough to trust.
- Config sync. Loom began as a way to keep Model Context Protocol (MCP) servers in sync across Claude Code, Codex, and Gemini on more than one machine. One registry, generated configs per platform. It removed a daily annoyance and nothing more.
- Own the servers. Node and Python MCP servers kept breaking on dependency drift, so I rewrote the ones I use as single-binary Go servers. Boring is the precondition for stacking anything on top.
- Move the compute. The devbox server put agent sandboxes on the k3s cluster instead of my laptop: build the project image once, run tests and quality gates in a pod, give agents a real workspace with real limits.
- Close the loop. With sync, servers, and sandboxes in place, the missing piece was a process. A council plans from roadmap intent, a pipeline implements, tests, opens the merge request, watches CI, and merges, and gates (scope, protected paths, secret scan, diff size, docs) decide what may merge without a human. That is Mills. It runs as a Kubernetes operator with a policy ConfigMap in GitOps, a kill switch, budgets per repo, and an evidence trail per run. I have written about the orchestration model and about pointing it at its own SDK.
The two domain repos are the same idea applied to healthcare integration. fi-fhir is an integration engine that maps HL7v2, X12, CDA, and CSV into semantic events and out to FHIR R4, so integration logic reads in workflow terms instead of PID.3.1. edilint is a pre-send linter for X12, HL7v2, and EDIFACT files: a static binary with stable rule IDs and SARIF output, built to be a deterministic gate that CI, and systems like Mills, can trust without a model in the loop. Putting both through Mills was the point. The domain tools and the process tools are supposed to compose.
Setup: two edits, and the cheap failure
Admitting a repo to Mills turned out to be two changes, not one: the operator's policy (demand sources, per-repo protected paths, budget caps) and the HUD spawn allowlist plus a path map in loom-core. I made the first and forgot the second. The first fi-fhir run found the gap in nine seconds:
project "libs/fi-fhir" is not configured for spawning
Zero dollars spent. The gate sits in front of the spend, which is the design, and it is the least dramatic and most important thing in this post.
edilint needed a canonical repo first. It lived only on GitHub; Mills works on GitLab. I created the GitLab project, pushed every branch and the v0.1.0 tag, added CI mirroring make ci, and a mirror job so GitHub stays the public face. The first two pipelines went red for an unrelated reason worth telling: the shared CI template keeps the Go module cache inside the checkout, and two repo-walking checks (a British-spelling test and gofmt -l .) walked into it. Two one-line fixes, both landed through merge requests.
What merged unattended
edilint. A roadmap status refresh as the smoke test, then a synthetic 837P claim-batch fixture family with single-rule failing variants and a table-driven test. Mid-run the agent caught its own scope slip and removed a changelog fragment that does not belong in that repo. About $0.60 each.
fi-fhir. A README correction first: the mapper exposes 26 Map* methods, not the four the README claimed, and tracing is scaffolded, not shipped. Then a GET /metadata CapabilityStatement derived from the real mapper surface, with a reflection-based drift guard so the statement cannot quietly fall behind the code. That one merged on September 2 after two spec-side test fixes from me and one instructive detour. To stay inside the declared file scope, Mills removed the one line that wires the build version into the server, then caught that in its own review pass and restored it once the scope gate admitted the composition-root touch. About $3.30 across attempts.
Every merged item is graded keep or regret afterward, so the evaluation loop has something to learn from besides "CI was green."
What it exposed in the target repos
A fresh pipeline is an audit, whether or not you asked for one.
- fi-fhir's benchmark job was manual and blocking on every Go-touching merge-request pipeline. That is why its Sprint 5 merge requests had sat unmergeable since August. It is now non-blocking on merge requests and still blocking on
main. - Two Go CVEs (x/crypto, grpc) surfaced the moment a pipeline ran, plus an OpenSSL HIGH in the UI image and an npm advisory. Fixed in one consolidated merge request.
- Docker-in-Docker image builds hung on Alpine package fetches four times in a row. A same-slot success proved it was the per-build bridge network, and
--network=hoston the image builds fixed it first try.
What it exposed in itself
This is the interesting part. A process that cannot find its own gaps is not a process.
Gap 1: the CI watcher only accepted push pipelines
fi-fhir builds only merge-request pipelines. So every fi-fhir run escalated at the ci_watch stage even while the real pipeline was green, and the error's own diagnostic printed the pipeline it was refusing to use. I wrote the spec against Mills with file and line anchors and four test cases. Mills claimed it within minutes and implemented it. Review found two defects: a strict identity check that broke merge recovery, and a changed log-line shape. I fixed both on its branch, and Mills merged its own fix through its own pipeline. The operator picked up the new image, and the next fi-fhir run followed the merge-request pipeline as designed.
Gap 2: the tests stage was not testing the change
Two runs passed the in-sandbox tests gate and then failed CI on the same commit. That should be impossible if the gate is doing its job, so I read the gate. It pins the sandbox to the pushed commit only when it can resolve the SHA from a local worktree. Spawn-driven runs have no local worktree, and an explicit fallback ran the gate against the sandbox's copy of main, where the new tests do not exist.
Sampling the last 21 completed runs: every run with a tests stage was unpinned, and five of nine ran no test command at all (formatting only). Two items now fix Mills: pin the head through the GitLab API or fail closed, and always test the Go packages the change touched. The P1 was claimed three minutes after posting, merged the same afternoon, and the next fi-fhir run tested the pushed commit. Run records now carry tested_sha.
Gap 3: the fix's own ticket could not close
The pin item's run had failed the scope gate, so Mills opened a rescue merge request and recorded its number. After the merge, the sweep that should have closed the ticket failed every pass with "durable project unavailable" and backed off exponentially. The escalation record freezes the target project, and for the home repo that value is the empty string, which the reader treats as unknown. Every home-repo item that escalates before the merge-request stage has the same shape: 269 records since August. I found it by reading the operator's event table directly, not its logs.
While in there: the state database was 451 MB with no retention at all, two hundred thousand KPI snapshot rows, and the timeouts that caused had quietly taken out the KPI, report, and learning sweeps. Two more items filed against Mills, with the SQL and byte counts attached as evidence.
The loop closed the next day
Mills implemented the escalation-record fix exactly to spec. The tests stage that had been patched the day before, now pinned to the pushed commit, caught that an existing test on main still encoded the old rule. Mills escalated instead of editing the test. The human call was deciding which was right (the spec), fixing one test case on Mills' branch, and letting the pipeline merge it.
That is the division of labor the whole thing is built for. The factory does the work and refuses to paper over a contradiction. A person resolves the contradiction.
What I did, then
The things the tools cannot: decide what is worth building, write the specs with acceptance criteria, review the diffs (two real defects in Mills' own fix), diagnose the failures that crossed system boundaries (the Docker-in-Docker network, the unpinned gate, the frozen project name), and decide when a human merges by hand versus lets the pipeline own it. That last one I learned the expensive way: requeueing an item after a hand-merge makes the factory re-implement it.
What keeps this safe
Budgets per repo. Protected paths that force human review on auth and deploy surfaces. Secret scanning. A scope gate that fails a merge request touching files outside its declared slice. A policy kill switch that hot-reloads from GitOps. And the honest counterexample: the tests-stage gap shows that gates only help if you audit what they actually checked. Green is a claim, not a fact, until you know which commit it was green against.
Takeaways
- Put the gate in front of the spend. A nine-second, zero-dollar failure on a missing allowlist entry is the cheapest bug you will ever fix.
- A fresh pipeline is an audit. Blocking benchmark jobs, unpatched CVEs, and a flaky build network all surfaced because a new consumer ran the pipeline with fresh eyes.
- Read the gate, not the badge. Two green runs that failed CI on the same commit were the only signal that the gate was testing the wrong tree. Record what was tested, not just that something was.
- File factory gaps as factory backlog. The CI watcher, the pin, and the escalation record were all fixed by Mills against itself, through the same review and the same pipeline as any other item.
- Let the factory refuse. The most valuable behavior in the whole session was an escalation: the pipeline found a contradiction between a spec and an existing test and stopped, instead of making one of them agree.
The repos are public on GitHub. The merge requests and run records live on my GitLab, which is not public, so the numbers in this post are the evidence I can show: four unattended merges, three gaps found and fixed, a few dollars of tokens, and one escalation I was glad to receive.
Related Articles
Comments
Join the discussion. Be respectful.