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. The early version had ten steps, in this order:

  1. Bump the template.
  2. Upgrade every app.
  3. Run the automated tester.
  4. Convert bugs to features.
  5. Review.
  6. Remediate.
  7. Run the full end-to-end test.
  8. Back-propagate improvements to the template.
  9. Commit.
  10. Sync.
The ten-step fleet upgrade cycle: bump the template, upgrade every app, run the tester, convert bugs to features, review, remediate, run the end-to-end test, back-propagate, commit, sync, and repeat 1 2 3 4 5 10 9 8 7 6 bump the template upgrade every app run the tester convert bugs to features review remediate end-to-end test back- propagate commit sync repeat
The early ten-step dance. Fixes found in an app went back into the template at step 8 and reached the other apps on the next pass.

The current workflow adds a pre-flight check and checkpoints for resuming a stopped pass. It checks the template, each app’s actual base version, and the working trees before applying an upgrade. The list above describes how the process began, not today’s exact command sequence.

At step 4 of that early loop, I turned bugs into tracked work items in the same format as features. That let 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.

Check for a shared cause

If the same bug shows up in three or more apps, I check the template before fixing the apps individually. When that is where the bug came from, the fix belongs there too.

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, their Python-centered Jinja or Cookiecutter setup would have added another template toolchain beside the Bun and PowerShell workflow I already had. My .templateoverrides file records why each path is frozen and reports which changes a target release would withhold. Agents cherry-pick narrow template commits and review each resulting diff. Adopting either tool would have meant replacing those controls, so I kept my workflow and used the same three-way comparison against a recorded base.

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.

Check the applications, not only the template diff

Two dances in one week, eleven apps to one template release on Monday and seven of them to the next on Wednesday, showed that most of the failures were already sitting in the applications.

The first pass found seven branded Dockerfiles missing a COPY line the template had added. The second exposed eight apps with the same sixty-second readiness timeout. From outside the container they just looked slow: the port accepted connections and nothing answered. The backend had rejected a security key that still held its placeholder value, supervisord gave up after four attempts, and the front-end proxy stayed up serving a closed backend socket. Generating real keys in the eight apps still carrying the placeholder fixed that startup failure. 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.

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 applications had drifted far enough that describing the domain again against a current baseline made more sense than carrying the old copies forward. Three had just been upgraded, so this was not a separate set of six additional apps. I re-created them from fresh template scaffolds 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 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 separated reviewed feature blueprints from implementation rather than pushing straight through by default. Five stopped at that stage; the sixth began its domain implementation the same day. At the blueprint 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, and I did not try to repair them automatically to keep the fleet uniform. They stayed on their previous versions while those blockers were reviewed. One unused theme pack had no route into the product, and its setting had disappeared from the template six releases earlier. I deleted it and amended the feature specification in the same commit.

Freezing the template

Eventually most template work was polish, and I wanted to spend more time on the applications. I tried freezing the template for patch-only maintenance. 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. Five minor releases followed the LTS tag, one explicitly flagged as an exception. On June 30 I removed the baseline snapshots, the lockfile guard, and the surface-diff policy entirely. The guard rails did what they were built to do. When the template genuinely needed to move, they produced hard failures I then overrode, and the policy was costing more attention than the drift it was preventing.

There is no LTS tag in the current repository, and patch-only did not remain the default. The template has continued to ship minor releases. The current dance chooses a patch or minor bump from the changes being shipped.

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.

Where this came from