Building constraints first
I don't watch much football, but I like games and optimization, and I like the people in my fantasy football league. A bunch of us used to wait tables together at an upscale steakhouse, and playing together keeps me connected to a time before I was a software engineer, before I went back to school, before I transitioned. My team used to be the Golden Poppies, but after finishing second in three of the last four seasons, I renamed them the Silver Poppies. This season, I have the most points scored in the league and have finished in the top three in three of the first four weeks. Things are going terribly. I'm glad we're all staying in touch.
One of those friends took over a cabinetry business, and I'm building software for quoting, ordering, and managing jobs there. I want to give him something rock solid that will be useful and maintainable for years to come, with room to keep improving as the business changes. That includes being able to hand a task to a coding agent when I'm not available to implement it myself, without being afraid of what it might break. We're leaning heavily into guardrails and process to support that goal, putting architectural decisions into checks and providing established paths for adding functionality. My hope is that those investments will make it easier for him, me, or another engineer to keep working on the system long after this initial build.
I'm using spec-driven development, describing the intended behavior and constraints before asking an agent to implement them. Part of that work is deciding what belongs in the specification, what requires my judgment, and what the repository can check for itself. The intent is to make the correct path the easiest path, because LLMs love to cheat. That's a little anthropomorphic, but it describes familiar problems: tests that pass without verifying the behavior they claim to cover, shortcuts across architectural boundaries, and a new pattern for something the application already has a way to do. Each can leave a change looking finished while adding work for whoever has to maintain it. I want the repository to catch more of those problems while the agent is working, so I don't have to rediscover them during every review.
I've flirted with hexagonal architecture in the past, and some of that thinking fits here. Alistair Cockburn's original description of ports and adapters separates application behavior from the interfaces and infrastructure around it, allowing the application to be developed and tested independently of those external systems. In this application, domain code has restricted imports, keeping the database and web framework outside it, and lint rules prohibit direct access to the system clock and randomness. Those dependencies have to be handled explicitly. The checks make that separation something an agent encounters while working, with a specific failure it can inspect and fix.
There are generators to make working within that structure easier. A new command
or query starts with the expected contracts, operation definition, and tests,
and the agent fills in the implementation. The generated tests fail against the
stub and provide a baseline for the feature-specific tests. Architecture tests
also inspect recordings from feature tests to check which operations ran,
whether commands produced their journal entries, whether declared errors were exercised, and whether
database writes stayed within the owning part of the application. An operation
that only returns NOT_IMPLEMENTED doesn't qualify as finished just because its
files exist. These checks cover the paths exercised by the feature tests;
assertions about the feature's intended behavior still need to be written.
This is a fairly demanding repository to work in. Adding a small piece of behavior can involve several files and checks, and I can imagine getting impatient with some of that ceremony if I were writing every line myself. With an agent handling much of the implementation, I'm interested in whether that tradeoff changes. The generators provide a consistent starting point, and the checks give the agent feedback it can act on without waiting for me.
Of course, the checks are code too, and an agent that can edit the application can usually find the files enforcing its rules. The repository treats changes to those guardrails separately: protected changes require my approval, and CI runs the target branch's copy of the guardrail checker, so edits to the checker don't approve themselves. I still control merges and review changes to the CI configuration that runs it. I also need to decide when a rule is wrong or has outlived its usefulness, especially when changing it would make the current task easier.
The real test will be how well this holds up when I'm not the person guiding every change. These checks take time to build and maintain, and a poorly chosen constraint can make perfectly reasonable work unnecessarily difficult. Passing them can't establish that a feature is useful or that we've understood the business correctly, but I want them to catch enough implementation mistakes to make continued development approachable. That means the process has to be usable by my friend too, including a clear way to tell when a change is ready and when it needs an engineer's attention. I'm building the constraints first because they're part of what I'm handing over along with the software.