How one developer ran an end-to-end legacy migration with agentic AI
30 July 2026
I was given a C++ application that ran on an older generation of payment terminals, documentation for the old platform, documentation for the new one, and a simple objective:
Build the application for the new terminals, and use AI to help.
That was essentially the entire handoff.
There was no business analyst to walk me through the requirements, no architect to explain the design decisions, no QA engineer to describe the expected behavior, and no second developer who already knew the system.
I had to reconstruct how the legacy application worked, figure out what should and should not be carried over to the new platform, design the new application, implement it, and verify the result.
The experiment behind the project was simple:
Could one developer take on the work of an entire delivery team by organizing AI in the right way?
How to recover legacy application requirements from source code
A working legacy application may look like a ready-made specification. The behavior already exists, and the source code appears to contain the answers.
But code does not explain which parts represent business rules, which exist because of the old platform, and which preserve contracts with external systems. A direct C++-to-Kotlin rewrite could reproduce the implementation while changing the behavior that actually matters.
So I did not start by generating the new application.
I first used AI to reconstruct the legacy system backward, treating the code and platform specifications as evidence rather than requirements. The goal was to recover an evidence-backed as-built model before making any decisions about the new application.
See the approach I used to reconstruct the legacy application
The reconstruction was not a single prompt or a one-pass code summary. I used several analysis passes, with each pass answering a different class of questions.
The basic rule was simple: if an important conclusion could not be traced back to observable evidence in the source code or platform specifications, it was not ready to become part of the migration baseline.
1. Map the system before explaining it
I first identified the major modules, entry points, data structures, external interfaces, device integrations, and persistent artifacts.
At this stage, I was not trying to infer requirements. I wanted to understand where the important responsibilities appeared to live.
2. Reconstruct runtime behavior
Next, I followed call paths and shared state to understand how the application behaved at runtime.
That meant answering questions such as:
How does a transaction start?
Which state transitions are possible?
When is host authorization invoked?
What happens after an interruption or restart?
What data survives between sessions?
Where are retries, reversals, or offline flows triggered?
This moved the analysis from code structure to observable system behavior.
3. Identify the boundaries that must survive migration
I then focused on external and platform-facing contracts, including host protocols, device APIs, storage formats, configuration, and payment-related responsibilities.
This distinction mattered because an internal implementation could often be redesigned, while an external wire or data contract might require exact compatibility.
4. Reconstruct features and requirements
Once the behavior was understood, I organized the findings into features, functional and nonfunctional requirements, user flows, data structures, and architecture decisions.
These were not standalone summaries. Important findings remained connected to the code and specification evidence that supported them.
5. Challenge the first interpretation
The first explanation was never automatically treated as the correct one.
Additional passes looked for missing modules, unexplained consumers of flags and structures, contradictions, unresolved references, and behavior that appeared in code but had not made it into the reconstructed model.
For example, an opaque configuration flag might initially look like a local setting. Before accepting that interpretation, I would trace where it was assigned, where it was read, which modules depended on it, which transaction states it affected, and whether the related behavior appeared in the platform specifications.
Only then could the conclusion become part of the as-built model.
How to build a project wiki for selective AI context retrieval
Once I had reconstructed the legacy application, a new problem appeared: where should all that knowledge live?
I could keep feeding the source code, platform specifications, and accumulated documentation back into every new AI session. But as the project grew, that would consume more and more context before the agent even started its actual task.
The opposite approach was just as risky. If every agent independently rediscovered the system, different tasks could gradually start from different interpretations of the same application.
I needed the project to remember more than any individual agent had to read.
So I built a linked project wiki with two separate sides: one for verified legacy as-built knowledge, and another for the forward-looking target design. Three reusable skills controlled how that knowledge was created, retrieved, and structurally validated.
Store broadly. Retrieve narrowly.
The result was a shared source of truth that an agent could enter at any point, retrieve only the context relevant to its task, and follow links back to the evidence or decisions behind it.
See how I structured the wiki and controlled what each agent could read
Two design choices made that possible: separating current-system facts from future-system decisions, and retrieving knowledge index first instead of loading the entire wiki.
1. Separate as-built truth from target design
I split the wiki into two sides.
The legacy side described the application as it actually existed:
architecture;
data model;
modules;
APIs;
behaviors;
functional and nonfunctional requirements;
features;
legacy architecture decisions.
The target side described the application we were designing:
target architecture;
requirements;
user stories;
user flows;
features;
wireframes;
architecture decision records;
roadmap and execution plan.
This separation prevented a subtle but dangerous problem: silently replacing a fact about the old application with a decision about how the new application should work.
A legacy document answered:
What does the current system actually do?
A target document answered:
What should the new system do, and why?
Links connected the two when a target decision depended on legacy evidence.
2. Use wiki-management to keep the knowledge model consistent
wiki-management defined how documents were created and maintained.
It controlled things such as:
document types and templates;
naming and identifiers;
metadata;
side-specific tags;
directory indexes;
links between related artifacts;
rules for creating, editing, moving, and deleting documents.
This mattered because an AI-generated knowledge base can become inconsistent very quickly if every agent invents its own document format, naming scheme, and linking conventions.
The skill turned documentation work into a repeatable operation rather than an open-ended writing task.
3. Use wiki-reader to retrieve context index first
Once the wiki grew, reading everything became counterproductive.
wiki-reader first determined what kind of information the current task required:
legacy knowledge;
target design;
both sides;
task or execution information.
It then opened the relevant index, selected the smallest useful set of documents, and followed additional links only when necessary.
The retrieval pattern was:
Question → Relevant side → Index → Minimum document set → Follow links if needed
This reduced context usage, but that was only one benefit.
It also reduced irrelevant information. An implementation agent working on one feature did not need dozens of unrelated requirements competing for attention.
4. Use wiki-validator for structural integrity
The third skill checked whether the wiki itself remained structurally sound.
It could detect problems such as:
missing metadata;
malformed or duplicate IDs;
incorrect directory placement;
wrong side tags;
missing index entries;
broken links;
orphan documents;
naming violations.
Its responsibility was deliberately limited.
wiki-validator could tell me that a document was structurally wrong. It could not tell me that an engineering conclusion inside the document was technically true.
That still required evidence, additional analysis, and human review.
5. Keep external documentation searchable the same way
I applied the same principle to the vendor documentation.
Separate reader skills handled the target platform documentation and the payment SDK documentation. Instead of loading those corpora wholesale, the agent first found the relevant section and retrieved only the documents needed for the current question.
This gave the project several distinct knowledge sources without forcing every task to carry all of them in context.
How to decide what to port, replace, or drop in a legacy application migration
Once I understood how the legacy application actually worked, the next question was not how to rewrite it.
It was which parts should still belong to the application at all.
A file-by-file or class-by-class port would carry old platform assumptions into the new system. Some business behavior still had to exist. Some external contracts had to remain byte-compatible. Some responsibilities had moved into the new platform itself. Others were no longer possible or needed an explicit decision before development could continue.
So I changed the unit of analysis from code components to capabilities.
For every legacy capability, I compared the behavior we needed to preserve with what the new platform allowed or already provided, then assigned a migration verdict.
Classify the capability first. Then choose the migration path.
That portability analysis became the bridge between understanding the old application and designing the new one.
See how I used AI agents to classify each legacy capability
I did not use a dedicated “migration analyst” agent. Instead, I used general-purpose Claude Code agents, meaning agents without a permanently assigned role, and specialized them through prompts and skills, reusable instruction sets that defined how to perform a particular type of work. For each capability, the agent compared the behavior that still had to exist, the legacy implementation that provided it, the capabilities and constraints of the new platform, and any external contract the new implementation was not free to change.
The project wiki contained the reconstructed VOS3 system, the project’s existing C++ payment-terminal application built for the Verifone VOS/ADK platform. wiki-reader retrieved the relevant VOS3 features, behaviors, APIs, architecture decisions, and data-model documents without loading the entire knowledge base. For the target platform, VAOS, Verifone Android OS, the agent used docs-vaos for platform architecture, security, privileges, signing, connectivity, and hardware access, and docs-psdk-sdi for payment-specific functions. PSDK is Verifone’s payment software development kit, while SDI, Secure Data Interface, is the controlled interface through which VAOS applications access payment-sensitive functions.
1. Start with a capability, not a file or class
The analysis agent first retrieved a legacy capability through wiki-reader and followed its links to related behaviors, APIs, requirements, and decisions. An API, application programming interface, is the defined interface through which one software component communicates with another.
This mattered because a C++ module is only an implementation boundary. For example, transaction processing might span several modules, global structures, dispatch tables, and platform APIs. The useful migration question was not whether those files should survive, but whether the transaction lifecycle, the sequence of states a payment transaction passes through, still had to exist.
In this case, the behavior survived, while the implementation could be redesigned as a Kotlin domain layer and state machine.
2. Ask which responsibilities the new platform already owns
The agent then used docs-vaos and docs-psdk-sdi to determine whether a responsibility should still belong to the application. This was especially important for card reading, PIN entry, payment cryptography, and EMV, the chip-card processing standard used for payment transactions.
On VAOS, these payment-sensitive operations moved behind the terminal’s secure processor and platform interfaces. Instead of rewriting the corresponding VOS3 implementation, the agent classified them as:
Replaced by platform
For example, EMV processing moved to SDI/PSDK rather than being implemented by the Android application itself.
3. Separate implementation freedom from contract freedom
The next question was whether the implementation could change without changing what another system saw.
The host protocol was one example. The Android client could be redesigned internally, but its wire contract, the exact message format exchanged with another system, still had to remain compatible.
The same applied to some legacy data formats and POS/ECR integrations. POS means point of sale, while ECR means electronic cash register. In this project, POS/ECR referred to external checkout systems communicating with the payment terminal.
These capabilities received:
Ports with contract lock
A contract lock meant that the internal implementation could change, but the externally visible protocol or data format could not.
4. Let the agent return “Needs decision”
Not every comparison produced an implementation answer. Some capabilities depended on unresolved choices around privileged applications, signing, key management, secure-processor access, or security architecture.
In those cases, the correct output was:
Needs decision
The agent was expected to expose the missing decision rather than invent a plausible answer. This same principle later became part of the implementation workflow: when a task depended on an unresolved vendor, security, architecture, or business choice, the AI process stopped instead of guessing.
5. Identify behavior that should disappear
The same comparison exposed mechanisms that no longer belonged in the new system. Modem and dial transport, direct device-node access, and root or system-partition access were either unavailable or forbidden on VAOS.
Those capabilities were classified as:
Dropped
The objective was not to preserve as much legacy implementation as possible. It was to preserve only the behavior that still had a reason to exist.
6. Write the verdict back into the shared knowledge model
The migration verdicts did not remain inside an AI conversation. They were written back into the project wiki and linked to the target-system documentation.
Legacy capability
What needs to be determined
Verdict
Target approach
Transaction state machine
Does the business logic remain?
Port
Kotlin state machine
Host wire protocol
Can the format be changed?
Contract lock
New client, same wire contract
Card reading
Can the application access hardware directly?
Replace
Platform SDK
Payment crypto
Where can sensitive data be processed?
Replace
Secure processor API
Modem connectivity
Is it supported by the new platform?
Drop
Not migrated
Key loading
Is there enough information to decide?
Decide
Vendor/security decision
For example, a new feature could link back to its VOS3 source feature and then forward to an ADR, Architecture Decision Record, a short document explaining an important technical decision and why it was made. It could also link to requirements, user flows, related features, and implementation tasks.
wiki-management maintained those documents and links, while wiki-reader made them available to later agents. wiki-validator checked structural integrity such as IDs, locations, indexes, and links. It did not decide whether a migration verdict was technically correct. That still depended on source evidence, comparison across the VOS3 and VAOS knowledge bases, and human review.
How to turn AI coding into a repeatable development, code review, and QA workflow
Once the migration decisions had been turned into target-system documentation and an ordered backlog, I finally had something AI could build from. But generating code was only one stage of the job.
A task was not complete because an AI agent said it was done, or even because the code compiled. It still had to satisfy the acceptance criteria, the conditions defined for the task to be considered correct, follow the project architecture, pass tests, work in the running application, and avoid breaking existing behavior.
So I wrapped implementation in a repeatable loop with separate responsibilities:
The implementation-loop skill orchestrated that sequence for every backlog task. Engineer, Code Reviewer, and QA, quality assurance, were separate working roles, so the same AI context that created a solution was not solely responsible for approving it.
The client environment did not support the custom agent types I originally intended to use. Instead, each role ran as a general-purpose Claude Code agent and was specialized through its model, skill, prompt, and task context. That let me preserve the separation of responsibilities without depending on custom-agent support.
See how I moved each backlog task through implementation, code review, and QA
One implementation detail is important here. I originally wanted separate custom agent types for Engineer, Code Reviewer, and QA Engineer. The client environment did not support them, so all three roles used general-purpose agents. Their specialization came from the combination of the model, the skill they were instructed to use, their prompt, and the context they received.
1. Orchestrator: select and prepare the task
implementation-loop first checked whether the task was actually ready: its dependencies had to be satisfied, it could not be blocked or deferred, and the execution plan had to define the appropriate model.
It then invoked agent-dispatch, which prepared an isolated Git worktree, a separate working copy of the repository where the agent could change code without touching the main working tree.
agent-dispatch also created .task-brief.md with the task ID, scope, acceptance criteria, constraints, and links into the project wiki. The prompt itself stayed small because the agent retrieved the detailed requirements from the shared knowledge base.
2. Engineer: implement and self-verify
The Engineer role used task-implementation.
Before coding, the agent had to read the task brief and relevant wiki pages, identify ambiguity or missing dependencies, check the proposed approach against the architecture and ADRs, Architecture Decision Records, and only then implement the change.
After implementation, it built the relevant modules, ran tests, performed a self-review, and wrote .task-report.md describing the changes and verification results. If the task affected the UI, it also had to run the mock application on an emulator or device and inspect the actual behavior instead of inferring correctness from the source code.
The mock build was a hardware-independent version of the application that replaced vendor-specific terminal services with controlled test implementations, allowing most application flows to run on a standard Android emulator.
3. Code Reviewer: independently check the implementation
A separate general-purpose agent then took the Code Reviewer role.
Its task was not to continue the implementation. It independently checked:
compliance with the architecture and ADRs;
potential bugs;
missing requirements;
unnecessary scope;
quality and relevance of tests;
successful compilation and test execution;
project-specific regression and security gates.
The reviewer inspected the same isolated worktree before anything was copied back or committed.
This separation mattered because self-review and independent review answer different questions. The Engineer asks, “Did I implement what I intended?” The reviewer asks, “Was that implementation actually the right one?”
4. QA Engineer: verify the working feature
If code review passed, a third agent took the QA role and verified the feature against its acceptance criteria.
For UI-related work, ui-verification could launch the mock application, navigate through the affected flow, inspect the Android layout tree, the structured representation of the elements currently displayed on screen, and capture screenshots to confirm appearance.
The layout tree was the primary source for checking screen state; screenshots were used to confirm what the interface actually looked like.
QA also checked that existing flows and tests still worked, so verification covered both the new behavior and regressions.
5. Any finding sends the task back through the loop
If either Code Review or QA found a problem, the findings went back to the Engineer in the same worktree.
After the fix, the task did not jump directly to done.
It had to pass Code Review again and then QA again because a fix that solves one problem can introduce another. The workflow allowed up to three unsuccessful fix cycles before stopping and returning control to the developer.
6. The loop also needed to know when to stop
This was an important part of the workflow.
Automation should not turn missing information into an invented solution.
implementation-loop stopped and handed control back to me when:
the context window became too full;
repeated fix cycles were no longer converging;
a critical technical problem could not be resolved autonomously;
an architecture, vendor, security, or business decision required human judgment.
Only after Engineer, Code Reviewer, and QA had all passed could the task move to its completed state and the execution tracker be updated.
So, could one developer run the work of an entire delivery team with AI agents?
Answering the question I asked at the beginning of this project, I can now say: yes, one developer can take on much of the work normally handled by an entire delivery team with AI agents, but the biggest change was not that AI replaced missing team members. It changed what one developer had to be responsible for.
I spent less effort performing every activity myself and more effort designing the system that made those activities reliable: defining the source of truth, separating responsibilities, setting verification gates, and deciding where automation had to stop.
The project showed me that the real scaling limit is not how much code AI can generate. It is how well one person can preserve shared understanding, control decisions, and keep independent checks around that generated work. Once those boundaries were explicit, AI could take on far more of the execution without giving up engineering control.
Frequently asked questions
Did AI agents replace an entire software delivery team?
Not literally. I used AI agents to perform many of the functions normally distributed across analysis, architecture, development, code review, QA, and project coordination. I still owned the source of truth, important technical decisions, priorities, unresolved blockers, and human approval points.
Why not use one AI agent for the whole migration?
Because implementation and verification are different responsibilities. The workflow deliberately separated Engineer, Code Reviewer, and QA roles so that the same agent context that produced a solution was not solely responsible for deciding whether that solution was correct.
How did the AI agents understand a legacy application with no proper knowledge transfer?
I first used AI-assisted reverse engineering to reconstruct the application’s as-built behavior from the C++ source code and platform specifications. Important conclusions had to be grounded in observable evidence rather than treated as requirements simply because AI inferred them. The developer identified this documentation and validation stage as one of the most human-intensive parts of the project because later implementation depended on its accuracy.
How did the agents avoid loading the entire project into every context window?
I stored the accumulated project knowledge in a linked wiki. The wiki-reader skill used an index-first approach to locate the smallest set of documents relevant to the current question rather than reading the full wiki speculatively.
How did you decide what parts of the legacy application should be migrated?
I analyzed the application by capability rather than by source file or class. Each capability received a portability verdict: port it, preserve an external contract while reimplementing it, replace it with a target-platform capability, stop for a decision, or drop it.
How was AI-generated code verified?
Every backlog task passed through an implementation loop. An Engineer implemented and self-verified the task, a separate Code Reviewer independently checked the code and tests, and QA verified the feature and acceptance criteria. Findings sent the work back to the Engineer, after which review and QA ran again.
Did this approach require custom AI agent types?
No. The environment did not support custom Engineer, Code Reviewer, and QA agent types. Those roles were implemented with general-purpose agents differentiated by the model, skill, prompt, and task context they received.
When did the AI workflow hand control back to the developer?
The process stopped when it could no longer proceed safely, including repeated fix cycles that were not converging, unresolved critical technical problems, context limits, or decisions that required human judgment.
What is the main lesson for other legacy modernization projects?
Do not begin by asking AI to rewrite the old code. First create a trustworthy model of the existing system, determine what actually needs to survive the migration, organize that knowledge so agents can retrieve it selectively, and only then automate implementation behind independent quality gates.
Let’s build your AI-assisted development process
At ABCloudz, I work alongside experienced engineers who combine software modernization expertise with practical AI-agentic development. We can help reconstruct the existing system, organize the project knowledge, define reusable skills and agent roles, turn the findings into an executable plan, and establish independent review and QA gates for implementation.
Bring us the code, the platform constraints, and the outcome you need. We will help turn them into a development process that is structured, traceable, and ready to execute.