The Agent Never Decided Scope
Two days, about eight hours of my attention and less than $100 in GitHub Copilot token costs took an end-of-life Drupal 7 site to a static Astro site with the same look, the same URLs and the same content. I’d have planned a month of development for it, and the eight hours were spread across those two days while I worked on other things. The speed came from one rule the agent couldn’t break: it proposes, a person accepts, and only then does it commit and start the next step. By the last step the log held 36 decisions, each tied to a commit. None of them was the agent’s.
The second thing that made it work was distrusting the database. On one product page, 0 of 13 overview fields that the database returned appeared on the live page. The migration copied what visitors saw.
This was a migration at work, so the site stays unnamed. The method is the part worth stealing.
Six goals, written before any code
The first file was a PLAN.md with six goals, and every later decision was checked against them:
- Look the same. New Tailwind and Astro code, same rendered page.
- Keep all the content. Every published page, image and document in the site’s language, and nothing else.
- Keep every URL. Every alias resolves at the same path, and every redirect still works.
- Be secure. No PHP, database or admin screen at request time. Static files and headers only.
- Hand interactivity to third parties. Forms post to the existing marketing-automation service. No custom backend.
- Keep site search.
Drupal 7 reached end of life on January 5, 2025, and this build had collected the usual risks: field-level translation, Panels, Views, Paragraphs, content in other languages left over from a shared codebase, and a text format that still ran PHP. The site was small, about 330 pages, but it carried search ranking and lead forms the business depended on. Small and load-bearing is the worst combination to leave on an unsupported CMS.
The plan held a progress table, one row per step, and a health line with a 🟢 or 🟡 for each goal. A separate LOG.md recorded every decision and acceptance in the product owner’s own words.
The agent proposes; a person accepts
The work was twelve steps with explicit dependencies, from a site audit to a final parity check. Each one ended the same way: evidence, a list of numbered decisions, and a stop.
Gaps came back as lettered options. The answer went into the log word for word: “A. Fix it to be at parity… C. Leave that as is, it is an improvement.” When the product owner was away, the agent kept working and logged each choice it made as “not accepted,” so nothing slipped into the record as settled.
This is the same rule I’m building into the delivery pipeline: agents prepare the work, people make the decisions. On a migration it does one more job. A legacy CMS is full of things nobody remembers deciding, and an agent that is allowed to decide will make the site match its own idea of a site. Scope belongs to the person who has to answer for the result.
A borrowed playbook works as a checklist, not a pipeline
The agent didn’t learn Drupal’s internals from scratch. It started from unDrupal, an MIT-licensed project by saitanay that isn’t a module or a CLI. It’s a set of markdown skills (site audit, content model, paragraphs, menus, aliases and redirects, multilingual, rich-text cleanup, validation) that teach an AI assistant how to get content out of Drupal, plus helper SQL and Drush scripts. Drupal’s own Migrate API only moves content in. unDrupal is written for the way out.
We cloned it read-only at a pinned commit, pointed Copilot at its AGENTS.md, and loaded one skill per step. Its Drupal 7 support needed work. The audit script took four fixes before it ran under Drush 8. The node query used the English source title and didn’t filter aliases by language. The paragraphs and config exports only cover Drupal 8 and later, and nothing handled the translation setup this site used. The adapted copies live in our repository, and upstream stays untouched.
It earned its place anyway. On a migration the expensive bug is the thing you forgot existed, and the skills kept asking “what did we forget?” at each step, which is the one question an agent working fast doesn’t ask itself.
The database lies about what the page shows
The audit found that the database and the page disagreed. The English values were cruft from a project more than five years old, still sitting in the fields. Drupal’s translation layer falls back to them when a field has no translation, so the API returns them, but the rendered page often doesn’t show them. Migrating from the database would have brought back English text that no visitor had seen in years. Every long-lived CMS has a layer like this: data that’s still stored, still returned by queries, and no longer anyone’s content.
So the extraction method treated the rendered HTML as the source of truth and used the database only for structure: content types, paragraphs, menus and aliases. A deterministic builder, a Python script with 67 unit tests, turned the export into typed JSON that Astro validates on every build. Deterministic matters here. Rerunning it after a redirect fix produced a six-entry diff, and that diff became its own commit, small enough to read.
URLs are the contract
Every alias, redirect and file path went into one map, and every row got a class: same path, 301 to a new target, 301 to a file, or excluded with a reason. No row points somewhere Drupal didn’t. A URL map is a pipeline’s acceptance test for search engines: it either resolves the old address or it says, in writing, why not.
“Looks the same” needed a ruler and a person
The agent used Chrome DevTools over MCP to measure the live site instead of guessing: gutters, sidebar widths, grid columns, font sizes and button heights, at 1280 and 500 pixels wide. It wrote CSS against those numbers.
Then I made the mistake of trusting a percentage. A script took side-by-side screenshots of 12 templates and scored the pixel mismatch. The scores were useless: one heading a line taller shifts everything below it, and a near-perfect page scores like a broken one. The gate became a person looking at the pairs, with an explicit gap list. That loop caught what code review wouldn’t have: duplicated gallery slides, a misaligned hero, and a mobile gallery in the wrong order.
Where parity and accessibility disagreed, accessibility won, and the log says so. The live link blue measures 4.15:1 against white text, under the 4.5:1 that WCAG AA requires, so the new site uses a darker blue.
What the tokens bought
The two days produced 73 commits in the Astro repository, plus the plan and log commits on the Drupal side. Three habits kept the token bill that low:
- One source of truth.
PLAN.md,LOG.mdand session checkpoints let each new session resume from the progress table instead of rediscovering the project. - Scripts over agent effort. Export, cleanup, asset hashing, URL mapping, SEO checks and the screenshot pass are all scripts with tests. The agent wrote each one once, and reruns cost nothing.
- Small commits. Review stays fast and rollback stays boring.
The result is plain static files, with Pagefind for search. A post-build image pass with sharp took built images from 182 MB to 50 MB while the source files stayed byte-identical to Drupal’s, so the hash checks still pass. Lighthouse puts mobile performance at 82 to 90 on the sampled pages, held down by marketing tags we kept on purpose. The live Drupal homepage scores 28. npm audit reports zero vulnerabilities, which is a sentence nobody has written about a Drupal 7 build in years.
The case against
Two days says as much about the site as the method. It was small, the theme was stable, and the person accepting each step knew the old build. A site ten times the size, or one with real application logic, gets the same rule and a much longer calendar.
The gate also costs time, and it’s only as good as the person at it; verification is the bottleneck here too. Thirty-six acceptances is thirty-six chances to approve something without reading it, and a reviewer who is multitasking is exactly the one who will. The “not accepted” label kept work moving while the owner was away, and it also let a backlog of unreviewed choices build up quietly behind the work.
And the site isn’t live. A demo runs on a static host, and the last step is waiting on acceptance. Hosting and regional compliance need confirming, five documents over 25 MiB need object storage, and a short editorial list (untitled video embeds, skipped heading levels, 14 legacy links nobody can resolve) needs an owner.
Editing is the open problem
Removing Drupal removed the admin screen, which was the point and is now the largest gap. The plan is Sveltia CMS, a browser-only CMS that reads the Decap/Netlify CMS configuration format, edits the JSON in the repository, and commits to GitHub so the static build redeploys. It adds no server. What’s left is mapping the content schemas to Sveltia fields, GitHub sign-in for editors, and deciding which entries people edit and which stay generated from the export.
The gate is the deliverable
The static site was never the hard part. Astro, Tailwind and a CDN will render anything you hand them. What shrank the calendar was a written record of every choice, made by the person who’d have to defend it, with the agent doing everything up to that line and nothing past it. That record is what I’d hand the next team, before the code.
Resources
- Drupal, Drupal 7 end of life, January 5, 2025.
- unDrupal by saitanay (MIT), AI agent skills for extracting content, configuration and structure from Drupal 7 through 11.
- Astro, Pagefind, and sharp.
- Sveltia CMS.
- W3C, WCAG 2.2 contrast minimum (1.4.3).
- Counts, scores, costs and quotes: the migration’s
PLAN.md,LOG.mdand Lighthouse runs, October 2026 (private repository). Contrast ratios computed from the two link colors.
About the Author
Kevin P. Davison has over 20 years of experience building websites and figuring out how to make large-scale web projects actually work. He writes about technology, AI, leadership lessons learned the hard way, and whatever else catches his attention—travel stories, weekend adventures in the Pacific Northwest like snorkeling in Puget Sound, or the occasional rabbit hole he couldn't resist.