Spending any time online would have you believe there is a fork in the road of software engineering. You either embrace the slop, dramatically accelerate your work, and hope the onslaught of entropy does not catch up with you. Or you micro-manage the slop (or avoid AI entirely), edit and review everything that is produced, and remain a huge bottleneck.
As usual, this bipolar nature of online discourse is hugely detrimental. World-class engineering is not reckless. It is deliberate, clever, and methodical. But it is not overcautious, either. It is ambitious, bold, and optimistic.
So what does world-class engineering do at this fork in the road? It carves out a new road. It identifies the right set of fundamentals that lets it extract nearly the same productivity as those embracing the slop, but with the rigor and predictability of those held back by micro-managing the slop.
It molds the slop.
The identity crisis
The ongoing transition is just as psychological as it is technological. There will always be a pedestal for problem-solvers, but software engineers are particularly unique in that we take problem-solving into our own hands. We don’t always just devise a solution, plan for it, and hand it off. More often than not, we will ideas into existence ourselves.
We are bricklayers. We’re on our own construction site, laying all the bricks. We choose the type, dimension, location. We choose the mortar and how to lay them in a stack. We’re bringing the space into existence exactly as we envisioned.
From Bricklayer to Engineer
The last couple of years shook all of that. A blessing for some, a curse for others, but most of us are not laying bricks anymore.
We’ve moved from being the Bricklayer to being the Civil Engineer. The construction site is still our second home and we know it like the back of our hands. We spend our days there, now steering, correcting, inspecting the work of others. It’s still ours to will into existence, but this time, we’re doing it by proxy.
From Engineer to Architect
More recently, models have gotten so incredibly good that all the micro-managing we’ve gotten used to doing is mostly an exercise in nostalgia.
The next step asks us to move further up the stack, going from Civil Engineer to Architect.
That means spending more time at the back office, envisioning the final thing, planning ahead, defining guardrails, and less at the construction site. Our job is no longer to lay bricks, nor is it to coordinate those laying the bricks. It’s to make sure they have a sound plan and a solid set of constraints within which to execute that plan.
I suspect this will lead to the biggest identity crisis of all. Not only will it be the second major transition in a couple of years, this one will be hard even on those that have so far greatly enjoyed the power fantasy of having a dozen agents multiply their throughput while remaining in control of everything. Creating the conditions to forgo some of that control (and actually letting it go) is a core part of this next step.
Domain expertise
What's also interesting is that domain expertise continues to become more important through these transitions. It is an amplifier for our ability to express intent, define good outcomes, and build the constraints that enforce them. You need to know what good looks like before you can create the conditions that reliably produce it.
Guardrails
What exactly goes into creating those conditions? Harness Engineering already captured much of what that entails. We need to work across a spectrum of independent yet related activities.
Context
Agents must be able to work without having to rediscover your feature set or your architecture every time.
Context is all about performance. Agents will perform better when they have relevant, high-quality context about the work at hand and the surrounding environment in which they’ll have to execute that work.
Evergreen documentation that is succinct and easy to navigate and search can enable much of this context. AGENTS.md and a well-organized docs/ folder can go a long way. You can look up other kinds of memory systems and they will most likely work well here too.
The key is to have:
- A good baseline to start with.
- Mechanisms for keeping it up to date (see Maintenance).
Constraints
Agents should put out reasonably good solutions by default, no matter the size of the challenge or the specific model.
Constraints are all about longevity. Models are stochastic, so constraints help prevent the mess they will eventually make and the performance loss that comes with it. Ultimately, they improve the longevity of a codebase as it grows in size and complexity.
You do this by heavily constraining how things can be built, reducing the odds of entropy sneaking in and things going off-track. It’s likely that you already have much of the intuition required to pull this off. Look at your own work over the past couple of years. Talk to your team. Take all the pitfalls you see, and mechanically enforce them out of the solution space.
Through this process, one important thing to realize is that you are no longer optimizing your codebases for a team of humans, but for a (much larger) team of agents. That means:
- Stricter constraints at all layers
- No need for nuance or concessions
Human-in-the-loop out, system-in-the-loop in. You’ll do this deterministically (e.g., type checking, linting, proofs) whenever possible and probabilistically (e.g., prompting, adversarial reviews) as a fallback.
Here are some examples across a wide range of categories:
- Modular architecture: Enforce a modular architecture with strict layout rules (file names, responsibilities, etc), inter-module interaction rules (what can import what, what can use what), and size/complexity limits so agents know when it’s time to split a module.
{ files: ['src/domain/**/*.ts'], rules: { 'no-restricted-imports': ['error', { patterns: [{ group: [ '**/controllers/**', '**/features/**', '**/infrastructure/**', ], message: 'Domain layer cannot depend on controllers, features, or infrastructure.', }], }], }, }
- High-value tests: Agents love tautological tests, so ban obvious self-comparisons and use adversarial review to catch tests that derive their expected result from the implementation. Agents also love to test mocks, so forbid those. You might also want to look into coverage, mostly to detect excessive coverage.
try { await thunk(jest.fn(), () => state, undefined) } catch (e) { expect(e instanceof ActionError).toBe(true) }
- Clean code: When agents resort to hacks they sometimes leave a comment behind, so search for them in adversarial reviews and challenge the agent to refactor. (Don’t instruct the agent to not leave comments, otherwise you’re left with a comment-less hack.)
- Error handling: Agents often match errors on message strings, which, save for a few cases, scales terribly. Devise rules to block this and prefer matching on stable constants or types.
Thorough constraints also help avoid having our success dependent on any particular model, easing concerns around economics, politics, or even providers replacing their models over time.
The key idea remains: constrain the solution space.
Verification
Agents must be able to verify their work. That includes linting and testing, but also calling an API, clicking around, taking and inspecting screenshots or traces. It also means observing a change going live in production. Everything you would do to smoke test a change, the agent should be doing as well.
Verification is all about correctness. It covers everything that goes into determining whether a change achieves the expected result or needs further iteration. It's what closes the loop.
const result = await thunk(jest.fn(), () => state, undefined) expect(result.meta.requestStatus).toBe('rejected') expect(result.payload).toBeInstanceOf(ActionError)
In my experience, the trickiest part when it comes to verification is the development environment. You need agents to be able to lint and test their own work in isolation. You also need them to interact with what they are building, which may require virtual machines or simulators, alongside a healthy verification setup with production-like data... for all of them, all at once. Luckily, there are more and more solutions out there in the market, and (with agents) it’s actually not that hard to build your own.
And once that’s done, all that’s left is observability. Make sure agents can monitor their own changes going live.
Maintenance
All of the above will get you on your way, but without maintenance, sooner or later, everything rots.
Maintenance is all about things that should happen on an ongoing basis to improve the system and reduce entropy:
- (Semi-automated) learning from corrections
- Occasionally peeking under the hood
- Fuzz testing key areas
- Continuous QA of your service/app
- Improving the enforcement hierarchy (memory → instructions → reviews → guardrails)
Every correction is an opportunity to improve the system itself. When you catch a mistake, ask what would prevent it from happening again. That might mean updating documentation or instructions. Ideally, it means adding a mechanical constraint or improving verification. You should not find yourself making the same correction multiple times.
{ "jest/expect-expect": "error", "jest/no-conditional-expect": "error" }
Agentic codebases
There are at least 2 kinds of codebases when it comes to agentic development. Those that were set up for humans and those that were set up for agents.
The path to autonomous agentic development is slightly different for both and can be especially messy for the former.
New codebases
For new codebases, your goal is to get the foundation right. The agents will repeat patterns they see, so you need to ensure they are solid.
Begin by setting up all of the above: strict linting, custom rules, architectural boundaries, documentation, and verification.
Then, meticulously review the first 2–3 major features or modules. You want them to be pristine so that imitating their structure, conventions, and patterns is desirable.
Once that strong foundation is in place and you observe guardrails and verification working effectively, you can progressively reduce direct review. This should happen early in the project's life.
Existing codebases
For existing codebases, your goal is to migrate aggressively, spending as little time as possible in between.
Most existing codebases evolved organically, with some degree of flexibility and leniency in mind. They likely have too much variation in solutions and approaches, plus several poor decisions you won’t like to see copied everywhere.
So once you agree you’re going to optimize for agents, commit to that transition decisively:
- Define exactly how you’ll handle context and constraints
- Ensure verification is set up so agents can refactor at scale and with high confidence
Take your time in these stages and be thorough. Then flip the switch, introduce checks, and leverage AI itself to migrate quickly:
- Introduce lint rules and use AI to refactor the code to comply
- Introduce architectural boundaries and use AI to refactor and enforce them across the codebase
During this time, you’ll have several major refactors in motion while hopefully some development is still happening. This in-between state bears the cost of both approaches—human oversight overhead combined with agentic entropy—without realizing the full benefits of either. This is why I recommend being thorough with the definition and setup steps above, so you can be aggressive when the time to migrate comes.
Future codebases
Languages (and frameworks) are deeply personal choices. We have strong preferences about what feels elegant, productive, or enjoyable. As we become less involved with implementation and review, those preferences will matter less, while the ability to express constraints and verify correctness will matter more.
Given this, I think both of these will hold true very soon:
- There will be some degree of consolidation around languages and frameworks that offer runtime benefits and are stricter and more limited by design.
- There will be many mass migrations to these languages and frameworks. After all, agents dramatically reduce the cost of this process.
More specifically, I think we’ll see a lot more Rust and Go, and less Python and TypeScript. Rust is a particularly interesting case. Its lauded benefits in correctness and performance have occasionally been overshadowed by concerns about ease of use and adoption. Well, these benefits are now at a huge premium, and the downsides are diminishing now that agents write the majority of the code. Bun is a sign of things to come.
Engineer the conditions to let go
You don’t mold the slop through good intentions and vibes. You do it through thoughtful, consistent engineering of the environment in which code is generated.
Even after all of the above is done, you should occasionally peek under the hood and determine whether your system is working as well as you intend it to. Sample agent output, track what escapes the guardrails, and use it to improve the system.
Molding the slop demands a transformation. A personal transformation, where we’re spending more time nurturing the environment that produces instead of producing ourselves. A technical transformation, as we cannot trust a stochastic machine blindly and instead build systems that make trusting its output a completely rational thing.
It's time to build the guardrails that let us let go.