// the challenge
From challenge to strategy
In legacy migration, one big challenge we see is risk management. What happens if we deploy? Is it correct? Can we roll it back? And even; do we understand what we are migrating?
To mitigate this, we want to migrate incrementally, observe and measure, and promote slices to production as fast as it is safe, so we can avoid big-bang deploys.
In large legacy systems, there are many places to start and more to migrate. With Bosun we can automate the discovery and implementation, and make it repeatable. That saves a lot of time. Engineers stay in control and can own and customize the process end-to-end.
In our case study, this leads us through the following steps:
- Ensure we deeply understand the domain and current architecture
- Determine a target architecture
- Identify and map migration patterns, language quirks
- Identify seams. What are good, first entry and end points
- Determine a migration architecture; temporary such that we can move safe, and fast
- Identify the slices we can migrate
With the rise of GenAI, it becomes straightforward to instruct an agent to migrate any bit of code. At scale, we need to manage risk, which means small sets of change and verified proof. For this case study, we are experimenting with our new proofing language called Parity. Parity allows us to specify intent in a human and machine readable language, capture legacy behavior, comparing the newer system against it.
- Scope
- Migrate background workers first, where each worker is its own slice. Legacy workers remain in the code, Rust workers are toggle-able, and can run side-by-side for comparison.
- Proof
- Each slice starts with an OpenSpec. Each requirement in the OpenSpec is hardened and automatically validated by our Parity language.
- Review
- Change requests are intentionally small, and include an automatically audited and validated specification, backed by Parity
// the migration
Six steps build one chain of evidence
Bosun allows us to build structured workflows around code, integrating with engineering tools, context, and documentation, so we can break up complex work into controlled, governed, structured, repeatable steps. Workflows are run in strong isolation (MicroVMs) so agents have full freedom.
For each individual step, we use separate workflows. Each produces an artefact that users can review, and serve as input for the next workflow. Results are Markdown, which makes it easy to change if needed. For this migration we used the OpenAI family of models, 5.4 and 5.3-codex. There are many open-weight models available we believe (and some have benchmarked with) to have similar results.
Bosun can pull in more external context if needed. However, since Netdisco is publicly available, there was little need. We build, debugged and improved the automations with Bosun's MCP. This allows us to try out many different solutions fast, using local coding agents.
An interesting note is that the workflows are not limited to Netdisco or Perl.
Step 01 Codebase Assessment
Understand
The first step is to understand what we are migrating. User journeys, features, architecture, existing tests, and so on. For each of these, we instructed agents in parallel to deeply review and report their findings. Then a higher reasoning agent is instructed to synthesize and distill, and finally it is structed using a single shot prompt into Markdown. Agents can use Bosun's code-intelligence to effectively navigate the code. Even with bigger foundational models, we notice a positive difference in quality when have a singular, focussed task.
Why it matters The structured deepdive provides the foundations for deciding how to migrate it downstream.
Step 02 Architecture Decision
Choose a path that can coexist
To help us determine the strategy (we did not decide on workers first up-front), we first identify potential seams, and then ask different agents to evaluate different migration strategies. We synthesize the results and rank them, then based on that decision propose a target architecture, followed by a migration architecture. Again, the result is a Markdown document that we can review. Because of the structured approach, we can audit the full decision. Instructed to keep the architecture close to the legacy, but ideomatic to the target, we get a clear overview, and a plan to migrate the first slices. The background workers in our case.
Why it matters Perl and Rust must coexist while each slice is tested, promoted, or rolled back.
Step 03 Migration Guide
Mapping patterns from Perl to Rust
Perl is an interesting language. Caller ambiguity, surprising type inference, Our favourite `$_`. It has quirks. So does Rust. Our goal is to create a list of legacy and target patterns that each implementor can use. Similarly, we need to analyze code, patterns, datastructures, and dependencies. For each category, we run agents in parallel, producing each a structured list of A to B. We then synthesize this list and again, format a simple Markdown docoument for review.
Why it matters Ensure slices are consistent and mistakes are not repeated.
Step 04 Bootstrap the migration architecture
Build the seam before moving production logic
Next we need to bootstrap the migration architecture. We need to hook into the right seams, ensure it can run side-by-side, and lay the foundations for testing and CI. Given the earlier proposal this is straightforward. An agent with high reasoning takes the earlier artefacts as context, and with the code, makes a detailed plan. The plan is then implemented, a pull request is created, and we double check that CI works and passes. Since Netdisco is well architectured, we can one-shot this. The result is a small hook inside the worker calling code that can run either Perl or Rust, or both. Also configurable opt-in is added and the foundations for tests are layed out.
Why it matters Bootstrap architecture in place so that slices can follow.
Step 05 Slice Backlog
Build the backlog
Given our earlier documents, we now have enough context for slice identification. An agent identifies slices, and produces a structured list of candidates. We then use another agent to quickly rank these. We want to start easy, and demonstrate value quickly. `dumpconfig`, `makerancidconf`, and `getapikey` headed the backlog. For each we have a piece of json we can repeatedly feed into our slice building workflow.
Why it matters A clear overview of work helps us understand scope and progress.
Step 06 A migrated slice
Slice-by-slice with proof
The goal is that the change request is clear in intent, functions as advertised, and does not break anything. This comes down to trust. And auditability and proof builds that trust. We start by creating an OpenSpec-style document. In here, we enrich with a custom language we developed to prove the intent matches the legacy system. This allows agents to assert they are correct, and downstream we can report on the measured parity. This is in addition to a qualitative review and passing tests. We then cut up the specification into smaller tasks, and run agents in sequence to implement them. We then have final checks, both deterministic and qualitative, with one chance to fix, before creating the merge request.
Why it matters A pull request proves code was written. The parity gate must prove both implementations satisfy the same contract.
// executable proof
Experimenting with Parity
Specifically in a legacy system, we want to be sure the new system behaves the same way on the outside as the old system. Traditionally this requires a lot of boilerplate to correctly measure, observe, and validate behavior. Combined with GenAI's tendency to be confidently wrong, migrating large, legacy applications becomes extra complex. We need a way to verify that the intent of legacy system aligns with the new.
Parity allows you to define proof similar that can live inside OpenSpec (and other Markdown). It automatically captures probes and replays behavior in the legacy system, identifies variance, and records. That same recording is then compared against the migrated slice. No code needs to be written besides the proof. We are still actively developing and experimenting with the language and the tooling.
The language is specifically designed for ease of reading. The language is expressive such that the tools can take care of discovery and recording. It automatically uses legacy migration best practices (i.e. snapshots, proptests, variance detection), so that user and agent can focus on writing the proof, not the plumbing.
proof worker.makerancidconf {
example standard {
job = {
action = "makerancidconf"
devices = [
{
name = "switch-01"
ip = "192.0.2.10"
vendor = "cisco"
group = "core"
},
{
name = "switch-02"
ip = "192.0.2.11"
vendor = "juniper"
group = "edge"
}
]
}
}
when makerancidconf(job)
then {
result includes {
status = "done"
}
changes rancid_export
unchanged queue
}
}
- 01
Specify
OpenSpec names the requirement, representative cases, actions, expected result, and the side effects that count.
- 02
Record
Parity runs Perl repeatedly, retains the selected observations, and proposes a digest-addressed contract for review.
- 03
Verify
Parity revalidates Perl first, then gives Rust the same inputs and compares the same result and observation surfaces.
- 04
Retain
The pull request carries a redacted proof report with artifact identities, cases, differences, scope limits, and a PASS, FAIL, or ERROR verdict.
Bosun ran this experiment on a Bosun-owned fork of the open-source Netdisco project. Netdisco did not
commission or endorse the work. We are grateful towards the hard work on Netdisco and we do not intend to migrate it to Rust. The fork and draft pull requests are private.