Agentic Coding Enables Real Software Engineering
The most interesting problem in software is communicating precisely and efficiently
Once the game I’m working on for the Cursor vibe jam, Galactic Golf, got past Level 6 or so, I began to have two concerns:
The code built with too many individual features would need a refactor
Refactoring would introduce more regressions than I could easily test at once
I’d first experienced the former situation building Supply and Demand; certain features that had a lot of commonality were getting more and more one-off data structures and references, and many features were getting dumped into a single file, and it was becoming a lot of context for Claude to read. I wanted to refactor these into an extensible framework, so that I could add features without needing to edit the code at all.
When it came to Galactic Golf, I made sure to add a modular structure for scenes governing levels early on. However, physics simulation is something I definitely have less personal knowledge of, and therefore something I have to abdicate some control over. As the complexity grew, I wanted a way to iterate more quickly while automating correctness guarantees.
Test-driven development is not a new idea in software, and it’s one of the best ones—sort of a platonic ideal. It works incredibly for most logistical correctness guarantees. However, it seems hardly practical for most application development, particularly when using modern frontend and UI frameworks, which often depend heavily on the all-encompassing state and context of the app to execute anything.
AI can change this dramatically. The Bot is good at handling the particular idiosyncrasies that come with writing tests for specific frameworks—what state needs to be mocked around testing the actual logic that needs testing.
We also tend to feed bite-sized features to AI, which in a way is the pinnacle of what frameworks like cucumber attempt to achieve: articulating your test cases as plain English, so that they serve practically as a common specification.
So, for Galactic Golf, I devised an experiment to test this method. After generating an initial suite of tests for the features already implemented, my instructions for Claude would be: for every new use case, to write a test first. It would run the test, make sure it failed in an expected way, and then run the test again after implementing the feature—typical test-driven development.
My claude.md looks something like this:
## Testing Requirements
Every new feature must have tests. Follow TDD:
1. **Write tests first** in `tests/` before implementing the feature.
2. **Run tests before implementation** to confirm they fail:
```sh
godot --headless --path . -s addons/gut/gut_cmdln.gd
```
3. **Implement the feature.**
4. **Run tests again** to confirm they pass.
Now I put it to the test with an extensive refactor. It’s good for refactors to have a clear product case: A couple of times, I had to make sure changes to ball physics also applied to the projected trajectory.
With the TDD framework, I asked Claude to refactor the code such that both ball and trajectory share physics, so we wouldn't need to make changes in two places.
Did the tests make this a flawless process? Not entirely. At first it didn’t understand that sharing the physics would mean more than just having a set of separate shared physics functions in the same file, it would mean that both code paths need to call on the same aggregate function.
When you tell Claude to go against its embedded behavior, it tends to revert to its old behavior in new context windows. So it’s worth noting I also told Claude to add a comment for itself so that it’ll remember to use the new function:
Did all of this help? Ultimately I’m not entirely sure. It’s always difficult to tell what incidents you’ve prevented. I’ve definitely still had to find many issues through manual testing, and then the other half of the time tests seem to be false positives—and just require a fix to the test itself.
I think it’s gotten better the more the codebase has evolved into tests driven by the actual use cases—beyond what was set up initially. Still, unit tests never tested whether something officially “works,” they just test that “it’s not broken in one particular way.” That’s maybe initially disappointing, but there’s real cumulative value in that.
It’s worth noting that this method consumes significantly more tokens. Luckily, I’ve configured Claude globally to use the caveman skill, which eliminates unnecessary LLM yapping (though it seems to have no effect on thinking). This allows me to even use Claude Opus (which I find is much better at refactors) by default on my $20 plan, only rarely running into my rate limit. I often find the default output way too wordy anyway; it becomes exhausting to parse during long sessions—ultimately not an efficient use of human brainpower either.
One thing’s for certain: even in the age of LLMs, there’s still great depth to be found in the field of Software Engineering. Many of the most interesting problems have always been—and remain—around how best to communicate complex problems and features precisely and efficiently.
From this perspective, it’s easy to see how agentic coding will only increase the need for Software Engineering as a field. It’s also easy to see how they’ll be an incredible point of advancement for the field itself, in giving us the greater means to leverage our principles.


