For most of 2026 I have been maintaining a fleet of my own applications with coding agents. At its largest there were thirteen apps, eleven of them inheriting from one template. Keeping a fix moving from the template into each app has taken more work than I expected, particularly when an app has changed the same file for its own reasons.
I’ve tried automating that judgment and lost code doing it. This is the upgrade process I use now.
The loop
I call one full pass a dance. It has ten steps, in this order:
- Bump the template.
- Upgrade every app.
- Run the automated tester.
- Convert bugs to features.
- Review.
- Remediate.
- Run the full end-to-end test.
- Back-propagate improvements to the template.
- Commit.
- Sync.
At step 4, I turn bugs into tracked work items in the same format as features. That lets the agents pick them up in the next run. In the early months a sweep across six apps in parallel routinely surfaced around thirty bugs a session, and one afternoon five parallel agents cleared a backlog of nearly seventy audit findings in about twenty minutes.
Three apps means it is a template bug
If the same bug shows up in three or more apps, fix it in the template.
That is my cue to look for a shared cause before spending more time fixing the apps individually.
It works in the small too. One authentication bug traced to a single function that could not distinguish “I had a session and it expired” from “I never had a session at all,” so the URL showed ?expired=1 on a genuinely first visit. Which makes an app look broken on first impression. Three-line fix, and because the same file existed byte-for-byte in six of seven repositories, it applied everywhere at once.
Keeping shared files byte-identical makes this much easier. Each customized copy needs its own review during an upgrade, so I try to keep those changes limited to what the app needs.
Reviewing template changes
I wrote a script to automate step 2. Six apps, roughly three minutes each when handled serially: eighteen minutes for a fleet pass. The script mostly worked.
When it did not work, it decided that code an app had added on purpose was “drift” from the template and quietly overwrote it. On April 10 it stripped vital domain code out of an application and left a trail of typecheck errors behind. The next day I spent thirteen hours on what should have been a ninety-minute workflow: one app had a custom helper stripped, another lost app-specific fields from its data store, a third ended up with a broken login flow.
All three files were “safe” according to the classifier.
I did not fix the classifier. I deleted the script from the template and all six apps, stripped it from the docs, simplified the slash command that used to offer “auto or manual” down to just manual, and left a note for future-me saying not to bring it back. The replacement is manual cherry-picking through six parallel subagents. The per-app work still takes about three minutes; overlapping those reviews brings the wall-clock fleet pass to about nine. The gain comes from parallelism while every diff remains one I can actually read.
My own notes had already warned me about the flag. I ran it anyway to save time.
I no longer let that classifier decide which changes to keep. A wrong decision can remove app behavior, and I may not notice until much later. I still automate fetching, applying reviewed changes, and running gates in parallel. A human or reviewing agent reads each diff.
There is a related mechanical point about how you produce that diff. When an app has fallen behind, a two-way comparison between the app and the current template cannot distinguish “the template moved forward” from “the app deliberately changed this.” Both look like differences.
Upgrade against the app’s real base version instead, the template release it was actually derived from, so you can compare the app’s changes and the template’s changes against the same starting point. Files the template never touched get left alone entirely. When all six of my active derived apps had fallen six releases behind, the three-way comparison is what made catching them up a one-day job.
This is also the argument for tracking, per app, exactly which template version it came from and which it currently claims. Guess the base and you are back to two-way diffs.
This model has prior art. Copier regenerates a project from its recorded template version, calculates the project’s own changes, updates from the newer template, and reapplies those changes. Cruft records the template commit and variables in .cruft.json, then presents an update for review; it can also exclude paths from later updates. Both get the provenance right.
Neither was a drop-in for this fleet. On Windows, adopting their Python-centered Jinja or Cookiecutter lifecycle would have added a second template toolchain beside the Bun and PowerShell workflow already in use. My .templateoverrides file also gives each frozen path a recorded reason and reports what a target release would withhold, while the upgrade flow is built around agents cherry-picking narrow template commits and reviewing every resulting diff. Retrofitting either tool would have replaced those controls. I kept the part they proved: record the real base and make the update a three-point comparison.
Reviewing overrides
My template supports freezing a file per app so local behavior survives upgrades. I found a serious problem in copies that had been frozen this way.
Five applications allowed sensitive database tables (API keys, users, the token blacklist) to be edited through the generic database-admin screen. The generic editor guards row mutations with a mutability check, and those apps’ copies consulted only the append-only table set, never the mutation-denied set. The template had an assertion for exactly this. Each affected app had frozen or disabled it during earlier template work.
The interfaces around a frozen file keep changing. In these apps, the frozen copies no longer met the template’s expectations, and the assertions that would have caught that had also been disabled.
If you support overrides, they need three properties:
- An inventory. One command that lists every override across the fleet with its reason. If you cannot produce that list, you do not know what your gates are actually checking.
- An expiry or review date. Without a date, I’m likely to forget to revisit it.
- A recorded reason at the point of suppression, not in a chat log.
Most of what breaks is already in the apps
Two dances in one week, eleven apps to one template release on Monday and seven of them to the next release on Wednesday, taught me the thing I least expected. I went in assuming the template diff would cause the trouble. Nearly all of it was already sitting in the applications, waiting.
Seven apps failed the production Docker check with the same sixty-second readiness timeout. From outside the container they just looked slow: the port accepted connections and nothing answered. The cause was in docker logs. The backend rejected a security key that still held its placeholder value, supervisord gave up after four attempts, and the front-end proxy stayed up cheerfully serving a closed backend socket. Generating real keys in the eight apps still carrying the placeholder fixed it, and because those config files are gitignored there was nothing to commit.
Then image builds failed for an unrelated reason: the branded Dockerfiles were missing a COPY line the template had added when a preinstall guard changed. The branded copies had drifted and never picked it up. Most of the apps also carried stale template workspace entries in their committed lockfiles, so a frozen install failed, after the reset step had already deleted node_modules. A row of application folders with no dependencies and several different errors scrolling at once.
I now check the backend logs when a readiness check times out, even if the proxy is accepting connections. Startup validation should reject placeholder keys, and the readiness check needs to expose that failure. I also check committed lockfiles before a reset removes dependencies. Stale workspace entries can prevent the reinstall even when the source diff looks fine.
Rebuild, and let apps stay behind
Six other applications, dormant for longer, had drifted far enough that a three-way merge was taking longer than describing the domain again against a current baseline. So I re-created them from a fresh template scaffold with brand-new git history. The old repositories stay on disk as read-only evidence, and one rebuilt app’s specification names its .old sibling at a specific commit so there is no ambiguity about which behavior to reproduce.
So far I have needed the old behavior as a reference, but haven’t had to trace the commits that produced it. I’m keeping those repositories in case that changes.
I also stopped the rebuild at reviewed feature blueprints rather than pushing straight through to implementation. At that stage I can still change the domain description without reworking an implementation.
On the Wednesday dance, seven apps moved and four stayed. Each of the four had its own blocker: a schema assertion plus a bundle-budget failure plus a pile of unused exports, or missing named domain constraints, or an unused wrapper plus an import the dead-code checker could not resolve, or file-size limits crossed in numerous places alongside a group of unused theme files. I did not try to repair them automatically to keep the fleet uniform.
The four apps stayed on their previous versions while those blockers were reviewed.
One of those blockers resolved itself pleasantly. The unused theme files turned out to be an old terminal-aesthetic experiment with no route into the product, whose setting had disappeared from the template six releases earlier. I considered wiring it back in, then deleted it and amended the feature specification in the same commit. I updated the specification too, so it no longer promised a feature I had removed.
Freezing the template
Eventually most template work was polish, and I wanted to spend more time on the applications. I tagged a long-term-support release and made patch-only the default. I know how easily I get pulled back into this, so I added hard failures on dependency drift, lockfile changes, and manifest staleness, using baseline snapshots, a lockfile guard, and a surface-diff policy.
It did not hold. Three exception releases followed, and on July 1 I removed the baseline snapshots, the lockfile guard, and the surface-diff policy entirely. The guard rails did what they were built to do. Each time the template genuinely needed to move, they produced a hard failure I then overrode, and after the third one the policy was costing more attention than the drift it was preventing.
I kept the tag and the patch-only default.
I use semantic versioning and a changelog to record what each template release changes. They help me estimate the upgrade work and identify the right base when an app has fallen behind.
Between April 17 and 19 I ran five full dance cycles, two of them in a single day. The early passes surfaced more than fifty findings and the fifth found none. I look at the findings across successive runs, along with what the tests actually covered, before deciding the upgrade is finished.
Changelog
- August 27, 2026: Corrected the fourth held-back app’s blocker, clarified which six apps were rebuilt versus upgraded, and named the removed guard rails consistently.
- August 25, 2026: Recorded that the long-term-support freeze was reversed on July 1 and that the baseline-snapshot, lockfile, and surface-diff guard rails were removed.
- August 5, 2026: Added Copier and Cruft as prior art, corrected the rebuild status and fleet count, clarified the serial-versus-parallel upgrade timing, and retitled the guide around the eleven inheriting apps.
- July 21, 2026: First published.
Where this came from
- Undoing a Shortcut - the day the auto-apply script was deleted, in full.
- The Dance: Running a Six-App Fleet with Agents - the ten-step loop and the first two months of running it.
- The Freeze That Didn’t Hold - freezing the template, the guard rails built to enforce it, and the later reversal.
- Five of Six Apps Had Turned Off the Same Security Gate - the three-way upgrade, the five-app security hole, and six apps started over.
- Two Dances, Six Rebuilds - the Docker morning, the apps left behind, and what the rebuilds became.
- Green Is Not Evidence - why the assertion that should have caught the security hole was not running.