Efficiently modernizing legacy code (1/n)
If you listen to very senior engineers, then it appears that they are fast moving towards, if not already in, a scenario where 100% of the code is being written by the agents. The rate at which we are writing code, one can argue that one is working on a legacy code if one starts on a new project 2 weeks after it started. It will be safe to say that we will be working on legacy codebases far far more than before. Hence I want to explore the possibilities, state of art, and discover techniques for modernizing legacy systems.
I would do this with actual evidence in an open source manner - sharing the code and my commentary.
Let us start with what is possible with Claude Code / Codex in the hands of very senior engineers. Why very senior engineers? So that we can examine how far these coding agents can be pushed - without the person driving it being the bottleneck. To see the possibilities we would try to setup a really complex scenario for an engineer who has been put on a project like one below:
Broadly we can split the above into three categories.
Given that I have AI with me now I would start with making sure that I have following in place before I do anything else. In this first article, will cover the first two. 3 & 4, we will cover in the next blog.
It is important to remember that pre-AI this remained just an aspiration in many projects - as the business owners, justifiably, wouldn't approve such a long period before anything business-wise meaningful is worked upon.
It is possible that the projects I choose have the supporting code already. Since this is a simulation - we can assume that it doesn't exist and how should we go about building it.
I am picking up OpenEdx as the project to work on. I know nothing about this codebase - except it has something to do with education. I am not very experienced in python too. Platform & App are two repositories of interest. My forked repositories are here (where you can find commits).
1. Run the product and all the tests on local
Makefile
The upstream repo only supports running LMS/CMS inside
Docker (Tutor). To run them directly on macOS for
faster iteration, new targets were needed:
macos-requirements and PKG_CONFIG_PATH auto-detection
to get Python deps building on macOS, run-lms/run-cms
to start dev servers on the host,
migrate-lms/migrate-cms with Tutor config so
migrations hit the right database, and
build-assets/watch-assets for webpack + SASS
compilation outside Docker.
cms/envs/devstack.py
The upstream CMS devstack settings were incomplete for
running outside Docker — they had Docker-internal
hostnames for services, missing theme/site config that
caused Django startup errors, and OAuth2 URLs
hardcoded to port 18000 (the upstream LMS default)
instead of 8000 (where LMS runs locally). The default
cache was also pointing to Memcached which Tutor
doesn't run, causing 500 session errors on every
request. All of these were fixed to mirror what was
already working in lms/envs/devstack.py.
lms/envs/devstack.py
Same Memcached problem as CMS — the default cache
backend pointed to localhost:11211 which doesn't exist
in Tutor, causing Django's session backend to raise a
500 on every request. Service hostnames also pointed
to Docker-internal names (redis, mongodb, etc.)
unreachable from the host. Redis is available at
127.0.0.1:6380 so the cache and all service hosts were
overridden to use that.
scripts/tutor/docker-compose.ports.yml (new)
Tutor's Docker Compose config doesn't expose MySQL,
MongoDB, or Redis ports to the host by default —
they're only accessible between containers. Running
LMS/CMS directly on macOS requires reaching these
services from outside Docker, so this override file
binds their ports to 127.0.0.1.
scripts/copy-node-modules.sh
The script used cp --force, a GNU coreutils long-form
flag not recognised by macOS's BSD cp, causing npm
install to fail on macOS. Replaced with the portable
short form cp -f.
Even though this is a working software due to my own context and preference, I need to adapt it for future productivity. Hence supporting mac and hot reload for the source code.
Building the app
The project already had Github actions, but it wasn't generating the docker image - due to how the project is managed between apache foundation & open edx. If I am working the code I would want to make code changes and deploy them - hence created new GHA workflow for the same so:
So the single Docker image ships the complete LMS/CMS — Django backend + all compiled JS/React/CSS assets together.
Commentary & Takeaways
Build tools for agents
Even when using AI, there are several repeated inefficient workflows. e.g.
Following is a tool/skill written for Claude Code to fix CI failures on its own. Oh and Claude generated its own skill / tool :-).
---
name: check-ci
description: Check GitHub Actions CI status for the current repo and branch
allowed-tools: Bash(gh *), Bash(git *), Bash(bash *)
---
1. Get the current repo and branch:
```bash
git remote get-url origin
git branch --show-current
```
Extract the `owner/repo` from the remote URL (strip `.git`, handle both SSH and HTTPS formats).
2. List available workflows and their most recent runs for this repo:
```bash
gh run list --repo <owner/repo> --branch <branch> --limit 3 --json workflowName,status,conclusion,databaseId,createdAt
```
Present the unique workflow names to the user as a numbered list and ask them to choose one.
3. Once the user picks a workflow, run the script:
```bash
bash ~/.claude/skills/check-ci/run.sh <owner/repo> <branch> "<workflow-name>"
```
4. Analyse the script output and report back:
- **Status**: workflow name, branch, run time, pass/fail/in-progress
- **Failed step**: exact step name that failed (if any)
- **Failure reason**: only the key error lines explaining why it failed
- **Suggested fix**: a concrete next action to resolve the failure
#!/bin/bash
REPO=$1
BRANCH=$2
WORKFLOW=$3
RUN_ID=$(gh run list \
--repo "$REPO" \
--branch "$BRANCH" \
--workflow "$WORKFLOW" \
--limit 1 \
--json databaseId \
--jq '.[0].databaseId')
if [ -z "$RUN_ID" ]; then
echo "No runs found for workflow '$WORKFLOW' on branch '$BRANCH'"
exit 1
fi
echo "=== RUN SUMMARY ==="
gh run view "$RUN_ID" --repo "$REPO"
echo ""
echo "=== FAILED STEP LOGS ==="
gh run view "$RUN_ID" --repo "$REPO" --log-failed
There is more to say, but this is already too long, more in future posts in this series.
Great point on discipline and patience — those are non-negotiable in legacy work. One thing I'd add before even touching the code: make sure all stakeholders actually agree on what the system is supposed to do. In my experience, the biggest hidden risk isn't the legacy technology itself — it's the legacy understanding of the business logic. Every department has a slightly different version of "how it works." When you start modernizing without surfacing those gaps first, you're essentially building a new system on top of unresolved disagreements. AI can accelerate the migration enormously — but it will accelerate the wrong direction just as fast if the business logic isn't clarified upfront. Looking forward to the rest of the series.
Great point about the need for patience & discipline in modernization.
Your point about AI creating even more legacy systems is the most underappreciated take in this whole space. The volume of AI-generated code hitting production right now — much of it with minimal documentation or test coverage — will be someone's modernization headache in 5 years. The disciplined approach you're describing is exactly right. In our experience, the teams that succeed at modernization are the ones that invest upfront in understanding the existing system's business workflows and risk hotspots rather than jumping straight to rewriting.
Great example of thinking at an abstract, systems level instead of jumping straight into code. Setting up the right environment and feedback loops before touching core logic to leverage AI. Looking forward for part 2