One Afternoon Instead of Two Months: Migrating the E24H Form to Next 16 with Matt Pocock's Skills
Published Oct 12, 2026
The application form behind Empresas en 24 Horas has a legacy name inside the team: VUF. It started as a module inside the big government portal, a large chunk of TypeScript carrying the versions it had grown up on. A teammate pulled it out into its own repository. That was the easy part. The hard part was what it was still running: Next 13, React 18, Node 20, Tailwind 3, yarn.
I wanted all of it current. A senior engineer doing this by hand, carefully, with QA between steps, would need about two months. That's my estimate, not a measurement. It took six and a half hours, from the moment I created the plan to the last merged pull request. It went smoothly, and this post is about why, and about the things that broke along the way.
What changed
| Before | After | |
|---|---|---|
| Next.js | 13 | 16 |
| React | 18 | 19 |
| Node (Dockerfile and CI) | 20 | 24 LTS |
| Package manager | yarn | pnpm |
| Tailwind CSS | 3 | 4 |
| Data fetching | react-query 3 | TanStack Query 5 |
| Headless UI | 1 | 2 |
| ESLint | 8 (next lint) | 9 (flat config) |
| PDF viewer | react-pdf 7 | react-pdf 11, with a download fallback |
| Browser tests | none | 13 Playwright tests, run in CI |
Five pull requests, about 80 files outside the lockfiles. Most of the dependency tree now sits on current, supported releases.
The method: plan first, in the open
I didn't start by asking an agent to "upgrade everything". That's how you end up with a 400-file diff nobody can review. Instead I used Matt Pocock's skills, mostly three of them.
/wayfinder is where it starts. You give it a foggy idea ("update our Next and related tooling") and instead of doing the work it draws a map: one GitHub issue holding the destination, the decisions made so far, and a section called Not yet specified for the fog you can see coming but can't phrase as a question yet. The map's children are decision tickets, and each one resolves a decision rather than a slice of code.
/grilling is how the destination and the tickets get sharper. It interviews you in rounds, and each question comes with a recommended answer worded so that "yes" accepts it. I come from Vue and Nuxt, and I told it. When it asked whether to move to the App Router as part of this, I had no idea what the tradeoffs were, so I said that, and it explained the options before asking again. At least once my whole answer was one word: recomendados.
/research handles the tickets that need facts. Each research ticket got its own agent, which wrote its findings to a throwaway branch and read primary sources instead of going from memory.
The map ended up with eight decision tickets: which Node LTS, what Next 16 breaks in a Pages Router app, a compatibility matrix for every dependency, whether Tailwind 4 was worth it, how to move to pnpm, what a minimal Playwright suite should cover, an alternative to a PDF viewer library with an absurdly high browser floor, and finally the order of the steps. Five were research tickets. They ran in parallel and finished in about ten minutes.
The decisions that mattered were all made before any code existed:
- Node 24, not 26. Node 24 is the current LTS, and the PDF library needed a recent one anyway.
- pnpm and Node move together, because the new pnpm needs a newer Node than the one we had. So the first pull request does both.
- Tests before upgrades. Playwright goes in second, on the old stack, so every later step has a safety net that already passed once.
- A jump straight to Next 16, not 13 to 14 to 15 to 16. One set of breaking changes instead of three.
- Tailwind 4 last and alone, because it changes how the app looks, which no type checker can see.
By the time the agents started executing, there was nothing left to argue about.
The execution: one ticket, one worktree, one builder
For execution I used a workflow I call /horde: one ticket, one git worktree, one builder agent. A separate orchestrator checks the work and opens the pull request. Terminal sessions run in herdr, which keeps each builder in its own pane.
The rule I care most about: a claim is not evidence. Every time a builder said "done", the orchestrator re-ran typecheck, lint, build and the Playwright suite itself, built the Docker image, started it, and checked that it answered. Then two reviewers read the diff, one for architecture and one for security. Anything blocking went back to the builder, up to three rounds. Only then did /create-pr open a pull request, ready for review, and I merged it.
What the process caught
These would have hurt in production.
- The container image broke the first time it was really built. Local builds passed; the first real image build did not. A copy step was dragging files from my machine into the image and overwriting the container's own.
- Next 16 changed a server default. The container started, but its health check could not connect. A reviewer predicted it from reading the code, and a real container confirmed it.
- CI used a different build mode than my machine. Next 13 shrugged at the difference. Next 16 crashes while prerendering the 404 page. Our local builds used the default, so nothing reproduced it. CI caught it.
- A new browser test failed now and then under load. It looked flaky. It was a real bug: Tailwind 4 removed the
transformclass, which in v3 quietly kept the modal panel above its dark backdrop. Without it, the backdrop could swallow clicks on the modal's own buttons. The fix was one word,relative. Before it the test failed 15 to 31 times in 80 runs under load; after it, 80 of 80 passed. - An error boundary that never caught anything. It looked right in the diff and passed its own tests. A reviewer noticed it kept no state, so in React 19 a render error would unmount the whole app instead of showing the fallback.
- Tailwind 4 changes defaults that compile fine. Reviewers measured the compiled CSS in a real browser and found date inputs two pixels shorter, a white focus ring where there used to be a blue one, buttons that lost their pointer cursor, and darker placeholders. A pixel diff of ten key screens against the old build showed zero difference in nine, and about 0.03 percent in the tenth.
- A dependency guard got bypassed. A builder skipped a safety check on brand-new releases to install a package published hours earlier. The review sent it back; an older version did the job.
Anything that wasn't part of the upgrade went to the issue tracker instead of getting fixed in passing. A migration pull request should be about the migration.
What it didn't do
- The agents weren't right the first time. Almost every ticket needed at least one round of review fixes. The process worked because it assumed that.
- The tests only cover the main flows. Thirteen Playwright tests with a fully mocked API check the redirect, the form, an upload, saving, the request detail page, the PDF viewer and the 404.
- The PDF viewer is worse for older browsers. The current version of its engine needs very new browser APIs, so a lot of people will get a "download the PDF" fallback instead of the viewer. We accepted that on purpose to stay on a maintained library. It needs a check on real devices.
- The expensive part is still mine: a full manual regression on real browsers and devices.
- It wasn't free. By the end of the day my weekly AI usage meter was at 96 percent.
What I'd steal from this
- Put the plan on your issue tracker and record each decision as you make it. It outlives the session, and a fresh agent can pick it up cold.
- Spend the first hour on questions, not code. Everything that went smoothly was decided there.
- Put the safety net in before the change it protects.
- Keep the thing that writes separate from the thing that verifies.
- Give the risky, visible change its own pull request. Tailwind got one because of what it does to pixels.
Two months became an afternoon. I still wouldn't ship any of it without a person checking it in a real browser.
Let's build something
Have a project in mind? I'm open to freelance work and full-time roles.