I have been extremely curious about full code base transformations for quite a while.
As an engineer many full code base transformation fall in to the categories of:
- Too complicated to do in a scripted fashion
- Too tedious and annoying to do by hand
One such change is converting Ember Input components in the Discourse code base to native input html elements. The transformation is not trivial in that you need introduce new methods and JavaScript imports, choose nice names and so on.
I wanted to write out about my approach and share with the community, in case you find this helpful.
Opening moves… Discourse Vibe
To start my work I created this new helper:
The key piece in dv (Discourse Vibe) is the following docker container:
This is probably where most big projects will spend most of their time and where people usually get stuck with cloud coding agents such as cursor,codex,jules etc.
The container I defined:
- Installs all deps (needed for testing / linting )
- Boots up PG and Redis, then migrates dev/test environments
- Sets up an internal runit based structure for booting discourse/pg/redis.
- This is an interesting issue many have, it can also be solved with docker compose orchestration.
- Sets up some default users in the dev database
- Installs cursor-agent (and a pile of other agents like claude/codex etc)
The key idea is by having a container isolating our code we can easily run YOLO (you only live once) mode, which is required for enormous llm chains.
The dv binary then makes it easy to work with the new container, setting up new agents, and entering the agent containers. For additional quality of life we export the web interface onto the local machine so we can easily visit the Discourse site.
Our first full codebase transformation
To kick stuff off:
git clone https://github.com/SamSaffron/ai-agent-container
go build
./dv build
./dv run
We now have a terminal open in our agent container. (we can use dv agent to define more agents if we wish)
Live instructions and an iterative approach
To start I created (with the help of cursor-agent) a file to provide instructions and keep track of progress:
The most notable part of this file, is that I instruct cursor to provide a hint when each file is converted. During the process it keeps updating the same file containing the instructions:
eg:
plugins/discourse-subscriptions/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-coupons.gjs (converted)
Next we create a small bash file to run stuff in a loop:
```#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$REPO_ROOT"
MAX_LOOPS="${MAX_LOOPS:-200}"
INSTRUCTIONS=$(cat <<'EOF'
Goal: Convert one file at a time from Ember's <Input> to native <input>.
FIRST: read convert-input.md (IMPORTANT)
Do the following:
- Find the first line under the "Files using the `<Input>` component" section in convert-input.md that does not contain "(converted)".
- If a file is found:
- Open that file and replace <Input ...> with <input ...> per the rules in convert-input.md ("Conversion rules and examples").
- Add any needed {{on ...}} handlers and corresponding @action methods in the backing class.
- Remove any now-unused imports (e.g., Input from @ember/component).
- Be sure on has been properly imported
- Ensure translations and UI strings remain unchanged.
- Run bin/lint --fix --recent.
- Update convert-input.md by appending " (converted)" to that file's line.
- Save all changes.
- If no unconverted files remain:
- Create a file named DONE at the repository root (if it does not already exist).
- Then stop.
EOF
)
i=0
while :; do
if [[ -f "$REPO_ROOT/DONE" ]]; then
echo "DONE detected. Exiting."
exit 0
fi
# Run the agent once
cursor-agent -fp "$INSTRUCTIONS" || true
# Optional: short pause to avoid tight loop
sleep 1
# Safety: prevent infinite loops if agent doesn't create DONE
i=$((i+1))
if (( i >= MAX_LOOPS )); then
echo "Reached MAX_LOOPS=$MAX_LOOPS without DONE. Exiting with failure." >&2
exit 1
fi
done
The trick here is that it will keep running cursor-agent in a loop converting 1 file at a time, finally it will write a DONE file which is a circuit breaker.
Extracting the changes
… hours pass…
Once our change is done we can extract the changes
./dv extract --chdir
git gui
Challenges
Not everything is roses, there is still a lot of learning and refining to do:
-
GPT-5 appears to have a tough time applying edits some times, leading to partial file edits
-
Cursor agent only has “mega verbose”
--output-formator “not verbose at all”, I wish there was some sort of in-between mode that give more info thattextand less thatjson -
Cursor agent only has
gpt-5sonnet-4andopus-4.1which is a bit limiting.
Future possible uses
-
Sweep code base for bugs / security issues
-
Global comment cleanup to adhere to a pattern
-
Various other migrations

