AI-assisted maintenance of a legacy PHP codebase

Mature codebases have a particular shape: layers of conventions, a few load-bearing oddities, and a test suite that mostly covers the parts that broke once. Dropping an AI coding agent into that environment is a very different experience from using one on a greenfield project.

Most of my AI-assisted work happens on Zen Cart, a PHP e-commerce platform with more than twenty years of history and a large ecosystem of third-party plugins. The agent is fast and capable. It is also confident about things it has guessed. The workflow below is how I get the speed without the surprises.

Start with a plan, not with code

The single most useful habit I’ve built is never letting an agent start by editing files. The first step is always a written plan.

I describe the change I want and ask the agent to investigate before proposing anything. What files are involved? Which hooks, notifiers or globals does this code rely on? Which plugins might be touching the same area? Then it writes a plan: the files it expects to change, the order it will change them in, how it will test each step, and what it is unsure about.

That last part matters most. A plan that lists its own open questions is far more useful than one that sounds certain. In a legacy codebase, the open questions are usually where the bugs are.

Iterate on the plan until it’s boring

The first plan is rarely right. It will miss a code path, propose a “cleaner” approach that breaks backwards compatibility, or change a function signature that half the plugin ecosystem depends on.

This is the cheapest point to catch those mistakes, so I treat the plan as a draft and push back on it:

  • Challenge the scope. Does this need to touch twelve files, or can it be done in three?
  • Ask about compatibility. Who else calls this function? What happens to a store running an old plugin?
  • Ask what’s missing. What would a long-time contributor say is wrong with this plan?
  • Split it up. If the plan has more than a handful of steps, break it into separate changes that can each be reviewed and shipped on their own.

A few rounds of this usually turn an ambitious plan into a boring one. Boring is the goal. A boring plan is predictable, reviewable, and easy to undo.

I keep the final plan as a Markdown file in the repository while the work is in progress. The agent works from it, ticks off steps as it goes, and records anything it discovers along the way. If a session ends or the context gets too long, the next session picks up from the plan instead of from memory.

Get tests in place before changing anything

In a greenfield project you write tests for new behaviour. In a legacy codebase, the first job is to pin down the behaviour that already exists.

Before any change, I ask the agent to check what test coverage there is for the code it’s about to touch. Where there isn’t enough, the first step of the plan is to write characterisation tests. These don’t assert what the code should do. They record what it currently does, including the odd parts, so that any change in behaviour shows up as a failing test instead of a support ticket.

AI agents are genuinely good at this. Characterisation tests are repetitive, detailed and tedious to write by hand: exactly the kind of work that’s easy to skip when you’re doing it yourself.

The rules I follow:

  1. Tests go green before the change starts. If the new tests don’t pass against the existing code, they’re wrong, not the code.
  2. Run the full suite after every step, not just the tests for the file that changed. Legacy code has side effects in places you wouldn’t expect.
  3. Never let the agent edit a test to make it pass. If a test fails after a change, that is information. The agent has to explain why, and I decide whether the behaviour change is intended.
  4. Keep the tests when you’re done. The coverage you built for one change protects every change after it.

Give the agent the project’s knowledge

Agents infer conventions from the code they can see. In a large, old codebase that inference is often wrong. Plugin hooks, notifier patterns, legacy globals and the “we do it this way because of 2009” rules all need to be spelled out.

I do this in two ways. First, a project instructions file that the agent reads at the start of every session. It covers coding standards, how to run the tests, which directories are core and which belong to plugins, and things it must never do, such as changing a public function signature without asking.

Second, I built zencart-ai-assist so that agents can search the Zen Cart documentation and code directly through a local MCP server. Instead of guessing how the notifier system works, the agent can look it up, the same way a new human contributor would.

Keep changes small and reviewable

Every step in the plan should produce a diff that a person can review properly in one sitting. If a step produces a diff I have to skim, the step was too big and gets split.

Small changes also make it easy to throw work away. When an agent heads in the wrong direction, I would rather discard twenty minutes of work than try to untangle a large change. With a written plan and good tests, restarting a step is cheap.

Where it helps most

  • Reading before writing. Asking the agent to explain a module before changing it surfaces assumptions I stopped noticing years ago.
  • Mechanical refactors. Renames, deprecation clean-ups and PHP version upgrades across hundreds of files, with tests confirming nothing changed.
  • Test coverage. Characterisation tests around code nobody wants to touch.
  • Documentation. Turning a completed plan into release notes or upgrade guidance for store owners.

Where it still needs a human

  • Compatibility judgement. The agent can find every caller in the repository. It can’t see the thousands of stores running plugins that were never published.
  • Security-sensitive code. Payments, authentication and anything handling customer data get a careful line-by-line human review, whatever the tests say.
  • Deciding what “correct” means. When a characterisation test reveals odd behaviour, whether it’s a bug or something a store depends on is a product decision, not a technical one.

The workflow in short

  1. Ask the agent to investigate, then write a plan with its open questions.
  2. Challenge and iterate on the plan until it is small and boring.
  3. Add characterisation tests and get them green before changing anything.
  4. Work through the plan one small, reviewable step at a time, running the full suite after each.
  5. Review every diff, and keep the tests.

The agent does most of the typing. A human still owns the plan, the tests and the merge button.