We Couldn't Rewrite 10-Year-Old Code Overnight, So an AI Agent Did the Triage
Table of Contents
- The October security advisories
- The EOL dead end: CodeIgniter 2 and Yii
- Getting it to boot: Rector and PHP 8.5
- The long tail: why booting is not working
- Building the AI triage and remediation loop
- Infrastructure debt: when the server OS is too old
- Functional testing with the support team
- What this experiment changed for our modernization plans
Getting a legacy app to boot on a modern runtime is quick; making it safe for production without rewriting everything is where the real work begins.
1 · The October security advisories
In October 2026, security advisories landed across several core pieces of our stack, including PHP, Node.js, and Nginx. The headline threat was PHP2Shell, a critical vulnerability that opened the door to remote code execution. At the same time, separate advisories for Node.js and Nginx required their own inventory checks and patch cycles.
For modern services, the drill was routine. We bumped minor versions on supported runtimes, ran automated test suites, verified health checks, and deployed.
The trouble started with the services that couldn't simply take an upstream patch.
2 · The EOL dead end: CodeIgniter 2 and Yii
Several internal business systems were still running on PHP 5.x and 7.x, built years ago on CodeIgniter 2, Yii, and early Laravel 5. These applications were not abandoned side-projects; they quietly handled active business operations every day.
Because PHP 5 and 7 reached end-of-life years ago, there were no vendor security patches waiting in an upstream package repository.
Rewriting was already part of our long-term roadmap, and gradual service replacements were in flight. But a full rewrite is a multi-month project, not a weekend response to a remote code execution disclosure. We couldn't leave vulnerable runtimes internet-facing, and we couldn't wait for rewrites to finish.
We chose a pragmatic middle path: jump the legacy applications straight to PHP 8.5, keeping the existing application logic intact while eliminating unsupported runtimes.
3 · Getting it to boot: Rector and PHP 8.5
On Thursday, October 22, I started testing with our oldest service, a CodeIgniter 2 codebase originally running on PHP 5.
PHP evolved drastically between versions 5.6 and 8.5: removed mysql extensions, strict typing rules, constructor changes, and deprecated dynamic properties. Manually editing every file across a decade-old codebase would have taken weeks.
Instead of hand-rolling edits, I paired Rector, an automated AST-based refactoring tool for PHP, with OpenAI-assisted code adjustments to handle syntax changes and deprecated framework methods.
The first milestone arrived faster than anticipated. Within about an hour of running Rector rules and fixing immediate autoload errors, the CodeIgniter 2 app booted on PHP 8.5. GET endpoints returned responses, and basic routing functioned.
That initial success felt encouraging, but booting an old app is not the same as having an application that works properly under production load.
4 · The long tail: why booting is not working
Once I clicked past simple GET requests, cracks showed up everywhere.
Some failures were straightforward: methods that PHP 8 removed, functions with altered parameter orders, and calls relying on legacy variable references. Other bugs were subtle. CodeIgniter 2 relied on loosely typed array operations and implicit string conversions that PHP 8.5 treats as fatal errors or type warnings.
Fixing one error manually followed an exhausting loop: read the stack trace, open the file, understand the old 2014 logic, apply a patch, test again, and hit another error on the next redirect.
Worse, fixing an issue in one controller didn't guarantee the other twelve controllers weren't carrying the exact same deprecated pattern. Manual inspection was becoming a massive bottleneck.
That frustration led to an obvious question: if these errors share predictable patterns, why not automate the triage and pattern-matching?
5 · Building the AI triage and remediation loop
I put together an experimental agent workflow using OpenAI to handle the repetitive parts of diagnosing compatibility regressions.
Rather than letting an AI blindly touch code or push to staging, the goal was narrow: turn runtime crash logs into concrete, reviewable pull requests.
App Logs (PHP 8.5 Runtime)
│
▼
[Log Inspection Cron]
│
▼
[Agent 1: Classifier] ─── Discard noise / Unrelated warnings
│
▼ (Valid PHP 8.5 compatibility bug)
[Context Ticket Created]
│
▼
[Agent 2: Remediation] ─── Scans codebase for identical patterns
│
▼
[GitHub Pull Request] ─── Human Engineer Review & Merge
The workflow breaks down into four parts:
- A background process periodically checks recent error logs from our PHP 8.5 test environment.
- A classification agent inspects each log entry. It filters out routine noise and determines whether the crash stems from PHP 8.5 compatibility issues, assigning an initial severity rating.
- When an actionable error is verified, the system generates a ticket containing the full stack trace, request parameters, and referenced file lines.
- A remediation agent picks up the ticket, inspects the codebase, drafts a fix, and searches the rest of the repository for identical deprecated patterns.
Finally, the agent opens a GitHub pull request with its reasoning and diff. I review every line before anything merges.
The agent still produces false positives occasionally or suggests fixes that overlook framework quirks. But having it automatically locate every sister occurrence of a deprecated function across thirty files saved hours of manual grepping.
6 · Infrastructure debt: when the server OS is too old
The code wasn't the only thing trapped in the past.
When we tried setting up PHP 8.5 on some of our existing development servers, package managers refused to cooperate. The underlying Linux distributions were so outdated that modern PHP packages and their required C libraries wouldn't install cleanly without breaking other services.
This was a good reminder: technical debt is rarely confined to application code. It creeps into the operating system, package repositories, build tooling, and deploy scripts.
Instead of wasting time fighting obsolete package managers on legacy hosts, we spun up clean test instances running modern operating systems. That gave us an unblocked sandbox to validate the PHP 8.5 runtime while scheduling OS rebuilds for the production hosts.
7 · Functional testing with the support team
Automated tests and server logs only catch technical exceptions like fatal errors and 500 status codes. They don't catch silent business logic failures.
A page might return HTTP 200 while displaying an empty balance table because an old query failed silently. A form might submit without a stack trace while dropping a critical customer field.
Because our legacy suites lacked comprehensive end-to-end coverage, I brought in our customer support team to test realistic day-to-day user journeys. They know the edge cases and weird workflows far better than unit tests written a decade ago.
Their testing caught several silent data issues that never showed up in PHP error logs. Each reported glitch went straight into our remediation pipeline, allowing us to patch and re-verify rapidly.
8 · What this experiment changed for our modernization plans
We started this project under pressure to address security advisories, particularly PHP2Shell.
Upgrading legacy applications directly to PHP 8.5 was never meant to replace our planned rewrites. It was a tactical defensive move: eliminate unpatched, end-of-life runtimes now, without waiting for ground-up rewrites that take months to ship.
Rector handled the heavy lifting of initial syntax transformation. The AI agent workflow removed the drudgery of hunting down repetitive deprecation patterns across dozens of files. And human validation—both code review on pull requests and functional testing with our support team—kept the changes safe.
Modernizing legacy systems doesn't have to be an all-or-nothing rewrite. Sometimes the pragmatic move is buying your team time by making the old code secure enough to live another day.