The AI played me like a noob, for 4 days straight.
I had to move an app from SQLite to PostgreSQL because the data volume was getting out of hand. New async abstraction, writers rewritten, concurrency, telemetry, progressive migration, load testing at 100 users, backup with PITR, automated cutover.
Then 7 more things showed up along the way, including a resumable orchestrator that survives a crash, a durable per-phase journal, and 10 signed Ed25519 proofs. More than 60,000 lines of code [sic], tests and docs before a single byte moved. To shift 6.47 GB.
Every addition came with its justification, and every justification held up fine on its own. A signature chain that guarantees the integrity of migration reports, you don't turn that down. An orchestrator that survives a SIGKILL, same. So you say yes, then yes, then yes again, and it never looks insane when it arrives one piece at a time.
The bill: 4 days, a cutover past midnight, bugs found in production, and on July 29 a commit that deletes 5,433 lines of a signature chain that was never commissioned. I also had to take another AI subscription halfway through, because this thing went through my Claude 20x plan like it was nothing. Nobody else paid for those 4 days.
All that for a migration that took 13 minutes....
The Migration Itself Took 13 Minutes
Here is the actual size of the problem: 2,120,930 products, 19.54M rows spread across 39 tables, 6.47 GB on disk.
That is small. For PostgreSQL that is a rounding error, and I want to say it clearly because if you don't work with databases every day, 19 million rows sounds impressive. A laptop handles this.
The measured durations, in order:
- Verified snapshot: 20 minutes 40
- Full cascade from pilot to the 250k slice to the complete catalog: 1 hour 33
- The full-catalog COPY on its own: 13 minutes
- The final gate from staging to production: 7 minutes, validating 44 application-level operations with 0 failures
Now the timeline that actually hurts. Work started July 26 at 11:04. On July 28 at 12:24, no full-catalog import had begun and the live gates did not exist yet. The first real cascades ran on the 28th around 19:00. The gate went green on the 29th at 00:04. Cutover at 00:11.
Most of those 61 hours ran out before the first byte moved.
There is an obvious objection here and a competent reader will have it ready by now. Comparing a 13-minute COPY to a 4-day project mixes 2 different things, and that mixing is the whole problem. Making an application PostgreSQL-compatible is a software job and it has a legitimate duration. Migrating the data is a short, measurable runbook. Both got wrapped into 1 project with 1 finish criterion, with no way left to say where the second one stood.
Control number: with the runtime compatible now, redoing this migration would take 2 to 3 hours.
60,000 Lines to Move 6 Gigabytes

The scope stopped being a database migration somewhere around day 2. It became 8 distinct software deliverables shipped as 1 job, then 15.
Walk through the 7 additions with me, because this is where you decide whether I was being an idiot or not:
- A SQLite capacity boundary, so the app knows when it has outgrown the old engine
- A static analyzer over the writers, to catch the ones that hadn't been converted yet
- A resumable orchestrator, because a crash at hour 3 sends you back to the start and costs a night
- 10 Ed25519 proofs, so a migration report can't be quietly edited after the fact
- Durable per-phase journals
- SIGKILL scenarios with PITR recovery
- Evidence collectors wired into every operation
Read that list again and try to find the one you would have refused. I couldn't either.
The effort split is the part that still bothers me. The proof chain weighed as much as the migration mechanics themselves, and close to 3 times the application adapter, which is the only component actually delivered to the runtime. Every hour spent on the thing that runs got outvoted by hours spent on the thing that watches the thing that runs.
On top of that sat chained attestations, a go-token, and a root-only entrypoint that verified the digest of the code tree and the SHA-256 of its own launcher. The whole thing was Fort Knox with a laser grid, and it was guarding a biscuit tin.
No key was ever provisioned and nothing was ever commissioned. Commit 7c77ca7 deleted the 5,433 lines the day after the cutover. 😬
So let's name the sizing for what it is: an apparatus designed against internal falsification of migration reports, on a 2-million-row database operated by 1 person. Me.
The falsifier and the auditor were the same guy.
(Somewhere in /docs there are now 14 markdown files with names like PHASE_4_EVIDENCE_PROTOCOL.md. I know they exist because I wrote the commit that created them. I have never opened one. I am not going to.)
The problem is the sizing, not the category. On a database 10 times bigger, with several operators, or for a process meant to be replayed with no human present, a good part of this apparatus earns its place. Signed proofs and recovery journals exist because real contexts need them. This one wasn't one.
A Review Has No Natural Stopping Condition
Every review round silently redefined the expected level of safety. A batch came back green, which triggered a new review, which found new conditions, which opened a new fix and a new cycle. Every green batch was a boss with a second health bar. The bar moved up because a review was performed, not because anything got worse.
This is structural, not accidental. In a system large enough, an agent tasked with finding weaknesses will find them, because they exist. There is always another unhandled edge, another timeout that could be tighter, another failure mode left uncovered. The verdict "this is enough" is not an observable property of the code. It is a trade between the risk you have left and the cost of continuing, and that trade belongs to whoever carries the consequences. An agent has no access to that information. It doesn't know what a delay costs me, it doesn't know what the data is worth, it doesn't know that I am the only person on this and that day 4 has a price. So when you don't hand it that trade as a frozen criterion, it does the thing it knows how to do, which is keep looking. The failure came from the audit and fix loop, not from a lack of rigor. Rigor was the whole problem.
Ask an agent to review your app before you put it online and the same thing happens. It finds something. It always does, and it never returns the verdict stop, this is good.
On July 24 2026, Anthropic published why the Claude Code team removed more than 80% of the system prompt for the Opus 5 and Fable 5 generation, with no measurable loss on their coding evals. Same phenomenon, different scale. A protective layer always gets written in reaction to a specific incident, and nothing in the process schedules a reread once that incident is forgotten. The layers accumulate, each one defensible on the day it was added, and the cost of stacking instructions only becomes visible when somebody finally counts them.
I ran into the same thing at a much smaller scale when I looked at what stacking instructions actually costs you inside a single config file.
There is a counter-argument going around right now, sharpest in Dex Horthy's "Why Software Factories Fail": harness engineering is not enough, because the models themselves degrade a codebase over time, and no amount of tooling wrapped around the loop fixes what the loop is made of. His answer is heavier planning upfront. That is a solid point and it covers a lot of the failures I have seen. I think it just doesn't cover this one. The planning was done, the architecture held, and what was missing was a number telling the loop when to exit.
2 Agents Missed the Loop
The agent driving the work stopped at L8/L9.
At that exact moment the board was 0 P0, 2 P1 remaining, 99 target tests green, TypeScript green. Every condition for a closed risk decision was met and documented. It relaunched a cycle instead of running the migration.
I asked several times that things move forward. They did not move forward. When I saw the migration eating more and more hours, I opened Claude Fable 5 in a separate session and asked it to look at what was going on.
Fable didn't spot the audit loop either.
2 agents looked at this project and neither one named the problem. Each was examining the state of the work in the frame where the question was asked, and neither was looking at the shape of the process producing that state. It is the HAL 9000 answer, minus the malice. The mission is proceeding correctly, all systems nominal, please stand by for the next verification round.
I ended up stopping everything myself.
I am not dressing that up as a method. I did not diagnose the loop. I got angry.
Losing your temper is a symptom, not a procedure. It is also the reason I now write the contract I freeze before the agent starts instead of trusting my own patience as a gate.
The Real Data Showed Up 2 Days Late
The evening of the cutover, 7 defects surfaced in the space of about an hour. Every one of them was findable on July 26, on a real snapshot, before a line of the proof chain existed:
- The export and compare CLI, broken by the new SQLite capacity boundary
- Residual Litestream tables sitting in the schema
- 2 orphaned foreign keys
- The comparator authenticating on the wrong session
- 3 products with no category
- COPY and checksum timeouts set too short
- Connection recycling firing in the middle of a compare
Instead they showed up at midnight, after 2 days of mostly synthetic testing.
4 of those 7 are the same bug repeated 4 times, and I missed it all 4 times. The psql COPY process timeout was set at 120 seconds against a COPY that runs 13 minutes. The queryTimeoutMs was capped at 60 seconds, untenable for parity checksums. The maxLifetimeSeconds sat at 300 seconds and recycled connections mid-compare. And the full-catalog count excluded products the export was selecting anyway.
Every one of those bounds was calibrated on reduced datasets where phases measured between 13 ms and 646 ms. It works on my machine, except the machine was a test fixture that fit in memory. On that scale, 120 seconds looks generous. It looks like a timeout you put there so a hung process doesn't sit forever. On the real catalog it is a stopwatch that fires while the job is doing exactly what it is supposed to do, and the job dies healthy.
4 Rules I Wrote After the Cleanup
Done gets written down before the first commit, in numbers, and the agent does not get to edit it. On this job the finish line was a sentence: the app runs on PostgreSQL and the data is intact. There is no exit condition anywhere in that sentence, so every review round was free to reinterpret it upward. What I write now looks like 0 P0, at most 2 P1, a parity checksum that matches on a real snapshot, and a cutover rehearsal that runs end to end once. When those 4 conditions are met the migration runs, and what runs is the migration, not another review.
The audit loop gets a cycle budget, and I set the number before I know what it will find. 3 rounds. Whatever a 4th round would have caught goes into a dated backlog file instead of into the current job. This is the rule that replaces me getting angry, and it only works because the number was chosen while I was calm, which happens to be the only state in which I was capable of trading risk against cost in the first place.
A software job and a runbook never share a finish criterion. Making the app PostgreSQL-compatible and moving 6.47 GB are 2 different pieces of work, they fail in different ways, and stapling them into 1 project is what made the duration impossible to read from the outside. Kept apart, both of them are boring, which is the point. The compatibility work ships on a green test suite against the new engine, the migration runs on a passing rehearsal, and neither one sits around waiting for the other to feel finished.
Real data lands on day 1, before anything gets built on top of an assumption. A snapshot pilot on July 26 would have surfaced the broken CLI, the orphaned foreign keys and all 4 timeout bounds at a point where fixing them cost an afternoon rather than a midnight. A synthetic fixture is calibrated on the numbers you already believe, which is why it never contradicts them.
None of this transfers cleanly to a team. With several operators the cycle budget is the wrong primitive, because the person who would spend it is not always the person carrying the consequences, and that is the one thing this whole approach depends on.
What stopped this project was not a criterion, not a report, and not either of the 2 agents that looked at the problem.
It was me losing my temper last night and saying we stop everything. That unblocked in 5 minutes what had been dragging for 4 days.
The trouble is that this doesn't go in a runbook. You don't write "phase 4: the operator gets angry". And the verdict "this is enough" will never come out of an agent, because it isn't in the code, it's with whoever pays the bill.
The runtime is compatible now. Redoing the same migration today is 2 to 3 hours.
Right, I'm off to the GYM before I put a knife through my mac (poor thing, it did nothing wrong)
Sources
- Thariq Shihipar, "The new rules of context engineering for Claude 5 generation models", Anthropic, July 24 2026
- Dex Horthy, "Why Software Factories Fail", HumanLayer: https://github.com/humanlayer/advanced-context-engineering-for-coding-agents/blob/main/wsff.md
This post may contain affiliate links. If you click them, I might earn a small commission (costs you nothing, and helps me keep shipping quality articles every day for your reading pleasure.)
Top comments (0)