The work we do at Makimo is varied. We work primarily as a software development/consulting company, and that gives us the opportunity to work on multiple projects – both internal and external.
In 2026, the pull requests we create and receive started creeping up in size. Like a beginner's running track record, some of us were going first with 500 lines, then 1k, 2k, 5k, 10k and aiming for marathon distances.
This is not an unusual practice across the industry. Looking outside, I see public GitHub projects with 200 commits over 2 weeks, which is an impressive feat, even unbelievable if we had discussed it 5 years ago.
Intrigued by this fact, I checked how much I contribute. In each of the past two months, I crossed half a million lines changed in all the repositories I work on. Yes, half a million, every month. You couldn't read that even if you wanted to.
The question immediately pops up – how to manage this level of change? One of our leaders got hit by a 15k AI-generated pull request from another engineer he worked with, and was not pleased at all. The first thought that came to his mind was to reject it and ask the person to split the request into more meaningful chunks. The second one was to put LLM against LLM, to help with code analysis. That was met with a mixed response: sometimes the reviews are helpful, sometimes they are nitpicky.
Are there other methods that can help us in managing this level of change? If only the LLMs could deterministically produce good output, like a compiler – then we wouldn't need to check the code at all. It's a pipe dream, completely unattainable.
But what if we shift the problem slightly? Instead of asking LLMs to "make no mistakes" again, what if we focus on the output itself? Can we somehow reduce our sensitivity to the output of LLM generation?
I revisited all the experience gathered by working with AI tools and agentic coding over the past two years at my company, Makimo. And presented below are five techniques that can reduce the sensitivity of a codebase to non-deterministic LLM output and let us reduce the amount of code we need to review. And we'll briefly talk about a possible future, too.
One thing to note – I won't be talking much about optimizing agentic workflows or prompts. On the contrary, my way of communicating with the agent is based on a mindset of managing a minimal set of boundary constraints that would solve a problem and seeing how far I can get with it. As proof, I'm going to include examples, but please do not treat them as universal prompts – rather as a proof of concept that a laconic and pithy briefing can also nudge the agent in the right direction.
Table of Contents
- The first time I skipped looking at the code
- Software production is now like a diffusion model
- Recent breakthroughs and new patterns
- Or use plain brute force
- Are we all a bit alone in this?
- A glimpse into the future
- What did it bring us?
The first time I skipped looking at the code
Let me take you back in time. On 10th July 2025, I did my first piece of work with a coding agent. If I remember correctly, the recommendation came from Lex Fridman's podcast.
I've been using LLMs for longer than that, starting with tab completion and simple function completion in Cursor, then switching to Claude Code, usually working with the newest Opus series of models (xhigh). I mention that out of scientific honesty – though I haven't tested this claim, I believe that one could achieve similar results with any harness and a model of a similar class.
Back in the summer of 2025, I was working on a large commercial project. One part of the system was its database cluster, built primarily to handle gigabytes of unstructured data. The trade-off of this system was that many common use cases, such as account deletion, required a lot of work.
We were asked to clean up the user accounts in the database. In order to do that, one needed to map the entity-relationship diagram of the database, then make decisions about what to do with each of the 20 related entities (including AWS S3 and Cognito objects), then check the ownership of the records managed by these users. If records weren't shared between multiple users, they were safe to drop, too.
Mapping every dependent object, researching what else depended on it and (separately) writing code to delete it was a lot of arduous work, and that's where the agent shone. But because we were dealing with production data, we needed to make sure we didn't remove too much of it.
The way to approach this was to develop additional tools:
- a
--dry-runmode for the delete process - an audit tool that would prepare a review list of accounts to keep and to delete, categorized by reason to keep/remove
- a separate environment where the deletion could actually be tested
Runtime property checking
All of that can be summarized in one sentence: we made our best effort at runtime property checking on the actual database. With the right properties defined, the code does not have much of a say – and we can offload the question of the predictability of LLMs.
The properties included the following:
- all the active accounts stay in the system
- for every account that is not active, keep it in the system for a specific number of days after expiration
- after removal, the account ID should not be present in any of the dependent collections
- if a dependent object's owner count drops to zero after account removal, drop the dependent object for collections (A, B, C) and keep it for (X, Y, Z)
There were more of them, covering corner cases with partial and missing data, etc.
The code responsible for checking the properties was crucial to review. But the rest of it – the actual implementation of the deletion – was the part of the code I just quickly glanced over. Why? Because the properties keep me safe in both false-negative and false-positive cases.
These properties were tested against the actual database and, in the end, the audit we ran classified accounts into 5–6 categories – accounts to keep, accounts to keep (manual) and "removal by reason". After confirming that this was as expected, we tested the deletion on a copy of the production environment, oversaw the actual deletion and smoke-tested whether the platform had the right data.
Software production is now like a diffusion model
Early on, even before using agentic tools, I made the observation that LLMs are good at predicting at a specific scale. With tab completion, it was a single line. Then a single function. Then, let's say, a whole State pattern with a couple of state objects. Then, larger and larger features.
I usually tested the agent's output at a given scale until it was doing the thing I wanted about 90% of the time, and then I slowly started to ignore what was below and switched to a slightly larger scale.
Currently, I usually operate at the level of smaller microservices and applications. A lot of them are internal tooling, so please bear in mind that you need to adjust the scale and the discipline to your case.
I prefer to think of this approach as "diffusing software". By analogy to diffusion models, which generate images by recovering contours and features out of random noise, the work I do with a coding agent is about extracting a program out of the space of all possible programs, by introducing constraints (behavioral and structural) that need to be preserved.
Thinking in this manner requires three things:
- having a good understanding of abstractions in general, as well as of the typical patterns that AI produces – that helps to avoid stating the obvious and to focus on the differentiating features of the software design
- having a generally good understanding of the domain to model – or at least a goal to achieve
- smoke testing (either manual or done by the agent)
One consequence of this style of thinking is that you need to pick your constraints carefully. An image produced by a diffusion model can have any background matching the prompt unless specified. By analogy, edge cases of your program will fall back to some non-deterministic default noise unless deliberately specified. That is fine for prototypes or user applications, but might not work when designing an API or a precise mechanism. It's your decision whether to treat edge cases as a first-class constraint of the system.
Recent breakthroughs and new patterns
I've already described runtime property checking above. And here are the rest of the techniques.
- Contextual tooling and scaffolding
- Library stripping
- Extract microservice
- Playbook application
All of them are techniques that were either not accessible (due to the relatively high cost of writing code manually) or simply not possible in their current form.
Contextual tooling and scaffolding
Contextual tooling means creating tools that are not general in nature, but tightly tied to the context of your project and your project only. The tools for property checking on the database cluster in the section above were one example. Let me give you another one.
Our website was built with WordPress, and my sense at the end of 2025 was that it would be much easier to work on it using AI if we dropped the idea of a database completely and had most of the content written in Markdown or HTML (the idea was correct – it's much easier to work that way). But our investment in the idea of custom blocks made it much more cumbersome to use any existing engine, such as Jekyll or Hugo. Off to custom we go, then!
We built a bundler that would transform all the articles, taxonomies, custom blocks and additions into a set of Markdown/PHP files, extracting all the media assets and generating a new structure.
The bundler was a multi-stage pipeline that loaded knowledge about all the articles, taxonomies, etc. into memory, and then, based on that, was able to conform to a new path-based model and output a directory structure reflecting what was on the original site.
What was important there was the property of idempotency. We built the bundler with the requirement that it would be run many times and refined every time we saw a bug, then worked through the output, spotting bugs one by one. With idempotency, you can re-run the bundler as many times as you want. It should always produce the same output.
By creating such a tool, we gain another technique for offloading the determinism question from the LLM. The output of the bundler can be compared with itself using a simple diff of two directories – or even of a single one, if it's committed to a Git repository. With that foundation, we can work under the assumption that the initial output of the bundler tool is certainly buggy, but because the tool is deterministic and idempotent, resolving enough bugs will make it bug-free to a satisfactory level. In this case, the satisfactory level meant that no content was lost or mangled during the bundling process.
Tools such as this one are scaffolding (in the same sense as in the construction of buildings). You put it up, use it for a certain task, then you dismantle it. In contrast to the concept of scaffolding in programming, it doesn't need to stay with the application, because with AI, code is cheap.
Library stripping
Let's say you've customized a certain third-party library to the point that you've taken ownership of it. In one of my private projects, the journaling app tasks-collector, I had a customized Vue tree component (based on the liquor-tree library), which for 2–3 years stopped me from migrating the whole project to Vue 3. I guess some readers can relate to this: one code-ownership choice made in the past prevents you from simply updating to the newest version of the whole technology stack. That was the case here.
This was one of my first tests of Fable 5, dated 2nd July 2026. The prompt I used:
Hi Fable! One item that stops me from switching the codebase from Vue2 to Vue3 is the forked out @tasks/assets/liquor-tree/ implementation that I'm using with some customizations - specifically to @tasks/assets/liquor-tree/src/lib/Node.js @tasks/assets/liquor-tree/src/components/TreeNode.vue and @tasks/assets/liquor-tree/src/components/TreeRoot.vue
I believe that because I took ownership of this library in this project, there is a bunch of settings that are not relevant to me in the code
So I'm trying of stripping off all the code that is hidden behind an unused option before we take any further refactorings. Could you plan that and help me remove unnecessary code from liquor-tree?
And after fixing one regression (which was found by a manual smoke test), I followed up with a structurally oriented prompt:
now looking at the code left, is there something to simplify? I would look at
- functions / modules to be merged as there is no reason to keep them separate
- functions to be pulled into a single file
- components that could be merged if the code is smaller now
do you see any recommendations here?
As a result, the agent analyzed my project, discovered which options from the library were not used and removed them entirely from the library. It stripped most of the conditional logic within the library and made porting all the code to Vue 3 much easier. You can see the commit history here.
In the past, we rarely had the ability to tailor the libraries we use to the exact case we need, and here coding agents also shine, because this work is not that complicated: find unused options, drop them from the codebase. It's just tedious.
Do we make the software more predictable by removing dead code? Yes, we do. It's interesting that, up until now, all the cases have delegated the LLM's unpredictability to a technique that could handle it better than just prompting for more code.
Extract microservice
Rectifying ill-defined boundaries between system services was a real pain in the past. Sometimes a single decision arising from an accident (like one team having spare time to implement part of the integration on the wrong side of the boundary) could have consequences that stayed in the system for years.
One early symptom of this showed up in Makimo's sales & marketing department. Two applications we use required API keys for the same four systems (CRM, Slack, e-mail, marketing tool) in order to properly integrate them into their workflows. That's eight sets of credentials. Adding a third one pushes that to 12. On top of that, isn't it odd that the sales & marketing department cannot change anything in these processes without asking the owners of these applications to redeploy?
One of my recent experiments was to see how fast I could deliver a working sales-marketing integration microservice for these consumer applications. Each one had its own use case, composed of common steps (add a record to the CRM, notify on Slack, add to a contact list in the marketing tool, etc.). That design would remove both problems mentioned in the previous paragraph, putting ownership where it belongs and keeping credentials to a minimum.
It took me less than a day to achieve that. The microservice was written in Elixir and followed a variant of the hexagonal architecture – a pure domain core, application code for entry points and infrastructure code for integrations. To do this, I pointed an agent at the repositories and asked it to extract the use cases. It did just that. You can see the prompt below:
I'd like to start a new Elixir project with Phoenix, with a underlying Domain-Driven foundation.
The scope of the project will be the sales and marketing integration and workflow manager for the company https://makimo.com/
With this project I want to abstract away the integrations we have into a common language of sales/marketing use cases, with an API and dead-letter queue guaranteeing that the use cases will be satisfied.
The project should be designed as such:
1. infrastructure code – integrations with <Marketing tool> (contact lists, transactional email), <CRM> (add lead, add deal), Slack (message notification)
2. domain code - use cases (coordination functions), which define a specific flow coming from. these are short-lived (up to 10-15 minutes), and can be run as subprocessess via genserver
3. application code - api for validation and converting into use case requests, and setting up the handlers.
isolate the domain so it can be tested with mocked infrastructure.
this API should be authenticated with simple, revokable tokens with scopes tied to the use cases. add a login and user and an admin page to register tokens (and an endpoint to create tokens from console tools)
for now we have 3 use cases:
1. add a lead/deal to <CRM> (manual, from an authenticated console tool)
2. <Service> (replacing the implementation from <Path to Repository> - <Marketing tool> transactional email, add to <Marketing tool> newsletter and contact list + add to <CRM> – change here to lead, not a deal yet)
3. makimo.com contact form (replacing the implementation from <Path to another repository> - <Marketing tool> transactional mail, <CRM> lead
Once it was finished, I pointed the consumers at this repository, asking for their implementations to be replaced with calls to the microservice. Then I deployed all three and smoke-tested both integrations. Once that was confirmed, the boundary between the parts of the system had moved to one that better supports the domain model.
This is also where we should be careful about the failure states of the system. This system may fail on 4 separate integrations, so it's an important job for the engineer to decide what happens there. A set of constraints I adopted for this service later on was:
- each integration can fail independently
- the application retries three times for each integration with a back-off mechanism
- the process enters a partial status, and a failure is reported to a Slack channel
Your higher-traffic, higher-risk application will need more constraints and more verification. And you can leverage AI to write end-to-end tests for these constraints.
Playbook application
This pattern connects to and naturally extends the previous one. There, I mentioned the technique of "pointing at a repository". You can do a lot with that.
In this pattern, a playbook means either:
- a well-thought-out design document that we want to enforce in our organization, describing the standard way of implementing it and common considerations
- a reference implementation that can be copied to another project (it being part of a previous project is perfectly fine – if we know that the implementation was done properly)
In the past, applying a playbook required a lot of attention from a developer in order to conceptualize the playbook and the project, and then overlay the playbook's design on top of the new project. When using a previous project as a reference, it was often easier to forgo trying to conceptualize it and start from scratch, using Google and documentation.
At Makimo, my co-founder Mati created playbooks on how to put internal applications on the AWS cloud, so that even an engineer who is not accustomed to using this cloud can deploy an internal Terraform application within the company cloud behind a common SSO provider. I followed suit and implemented it for three small applications within a day or so.
It has already saved us both money (on common resources such as NAT Gateways) and the time needed to research and maintain multiple sets of similar cloud configurations. The bonus is that you can apply an opinionated IaC setup and reduce the number of decisions needed to deploy common-case scenarios.
Another example of good playbook material is a previous repository with good CI/CD practices. We use GitHub Actions, and it's very useful to point at a previous repository and ask the agent to do the same.
This relies on building internal knowledge. Assuming that the playbooks are reviewed, they can certainly remove a lot of the unpredictability of an LLM's output.
Or use plain brute force
We've discussed a bunch of software engineering patterns and techniques that help us reduce sensitivity to non-deterministic LLM output. But what if we embrace it more?
As a limiting case, let me tell you about another open-source project that I'm working on.
This is openparkan – an engine reimplementation of the 2001 game Parkan: Iron Strategy. This is a project where I honestly do not understand the code. Nor do I wish to.
The purpose of this experiment was to test a specific type of legacy code – minimum knowledge (binaries and data only), unguessable (games have a lot of fixed parameters and tricks that make the game work, so you need to disassemble parts of the application to get the logic), verifiable (by playing or analyzing screenshots).
The way I approached this project was to first discover as much as possible about the data formats. That effort spawned Python tooling that, for every known record type in the game data files, read all the records and checked whether they were within acceptable constraints. A lot of that was discovered through the model's guesswork and by analogy with already discovered formats, and some of it built on the work of previous explorers, who had already mapped parts of the data structures.
Once this first phase was at an advanced stage, I shifted the priority to creating a Rust engine capable of running the game. The first goal was to make the first mission playable, and the agent delivered after some time.
With that, we entered two cycles. In the repository, there is a file, OPEN-QUESTIONS.md, which contains questions not yet checked. I created a command for the agent to spawn four subagents and tackle the questions in groups of three. The agent looked for the answers, values, etc. in the game's files, then implemented them in the engine.
The other cycle was getting feedback – either from playing the game and my memory of it, or from other people's YouTube playthroughs, which I passed to the agents, asking them to reproduce a specific mission. You can see the commands here.
At the two-week mark, we had the first two playable campaigns. Normally, this would require months of dedicated after-hours work from a couple of volunteers. In this case, we bypassed that by creating tooling that helped stabilize the core of the application.
I felt the non-determinism in this project. Some problems resurfaced after seemingly being fixed. Some of the behavior guesses made sense only in one mission and, after being corrected for another one, reintroduced bugs in the previous, already checked mission. But even in this slower and slightly volatile mode, the development settled into a slow rate of progress, polishing more of the mechanics and visuals of the game than it broke.
Was it worth it? Totally. Now I can play this game on macOS via Rust's cargo run.
Are we all a bit alone in this?
When I see repositories on GitHub that are 2 weeks old and have 200 commits, I can relate. Openparkan has over 600 commits and is barely over a month old, and the speed is extreme. And that introduces another risk – the risk of being alone in the team.
In 1969, Jim Morrison, the vocalist of The Doors, when asked in an interview about the future of music, said, “[…] it might rely heavily on electronics, tapes, I can kind of envision maybe one person with a lot of machines, tapes, and electronics set up, singing or speaking and using machines.”
And more than 50 years later, we have solo musicians producing all of their songs on their own. Are software engineers also destined for a similar future, where each engineer will be treated as a one-person orchestra, and only sometimes will they form bands?
Passing context to another person is harder than it was before. It's not a problem of review. It's more a problem of telling the other person where you are when you are not sure yourself where you are. You know what you're wishing for, but knowing the full consequences of what was built is another thing entirely.
Then there's the sanity question. Working at such speed while barely understanding the system is exhausting after a while. I've experienced that myself, and it's a health hazard that the industry might not have an answer to yet. Will that result in an increased rate of burnout in the coming years? I hope not.
None of the experiments I've run has yet produced a good answer to the question: how do we build for this speed and intensity?
A glimpse into the future
The truth is, with the current state of agentic AI, I believe that in tomorrow's world we will be building projects with 100x the complexity of what was possible even 10 years ago, and we should prepare for this jump.
The more alarming thought is that we have little to no robust tooling to work with complexity at that scale. By that, I don't mean that we do not have automated deployments, but that:
- There's not much in the way of reliable metrics that help us see where we should focus our attention in a codebase of such scale, and why
- There aren't many tools to help us verify how much of the domain is in the core domain component, and where the rest is
- There aren't many tools to enforce rules much broader than line-by-line code style
In light of this article's theme (reducing sensitivity to non-deterministic LLM output), I can imagine two relevant items, just to spark your imagination.
Bounded context maps will become a tool for agent orchestration. In Domain-Driven Design, there's the concept of bounded contexts, which determine the limits within which domain language and constraints can keep their integrity. This concept can be carried over into context engineering, where agents working on a service have clearly defined autonomy and a reporting structure for contract/boundary changes.
First example: an upstream API agent can notify all of the downstream consumers that the contract of a single URL has changed, triggering a set of related PRs in the consumers.
Second example: an agent tasked with a modification can ask all adjacent bounded contexts whether the change is safe. Other agents can, depending on the context map relations, issue a veto or make a recommendation. The agent would then get back to the engineer and present a report on the potential hotspots of implementing the change.
Static analysis will be able to better enforce degrees of freedom. The linters of today were built to enforce code style across a team of individuals. To manage larger-scale projects, we need rules that can predictably analyze whole modules or repositories. Then, engineers would be able to pick specific constraining styles, and a deterministic checker would fail any code that does not conform to the specific constraint.
That differs from today's usage of linters. In many projects, linters are based on a default style and one or two changes, often disabling rules to make the checker more permissive. Instead, an engineer would be responsible for picking what degrees of freedom a specific piece of code should be allowed to exhibit when it comes to agent's ability to modify it.
For example, the constraints could be "style: functional", "style: imperative", "no side effects", "no dependencies except...", "only pure functions", "fix the contract and report any changes".
Don't get me wrong, there are projects such as ArchUnit you can use even now to enforce some constraints. But we still need much more fine-grained control over the code structure we expect to see in the parts of our codebase.
What did it bring us?
Today, still in 2026, we're sitting with our codebases open, looking at another pull request with a lot more lines than we were processing last year.
At Makimo, the techniques we develop allow us to focus more on internal platforms and building intellectual property – accessible through playbooks, knowledge bases and tooling. By looking a little bit less at the code aspect, and focusing more on the verification aspect of the things we build, we were able to use our resources to package our internal knowledge into a first productized service, Makimo Health Check, among other things.
As for you, dear reader, you can take the first step and start building your own tooling to help you better verify the output of the LLM, and make your organization a bit less sensitive to its non-deterministic nature. Pick the properties you care about, and use code review where it's actually important. It's just one tool in a larger toolbox.
And now is the best time to get creative! If you could just build a tool that would help you with your work, what would it be?