Ian Cooper - Staccato Signals

These are the days of miracle and wonder

The Value of Understanding Code

In an agentic engineering world, what is the value of the code? We set the agent a goal, provide the requirements and the behaviors the system should exhibit, and ask it to write code to deliver them. When that is done, do we review the output, correct it, and iterate toward an outcome? Or is the code something ephemeral we regenerate as needed and never really read? Is it an artifact we care about, or a by-product? The answer decides where and how we apply human engineering effort, and how much work we let the agent do before any review: one task, all of them, or something in between.

In this post, we tackle that question:

  • First, why is the code still worth reading, even when an agent writes it?
  • Second, why does that make our certainty — not a fixed choice of human-in- or human-out-of-the-loop — the variable that should drive how we work?
  • Third, what is a practical model for turning that certainty into a decision about how big a step to take before we review?.

Along the way, we will show how we should distinguish between natural language, how we talk about the model, and the language in which the model is written.

Why does this question matter?

If we consider code an important artifact and operate with humans in the loop, your pipeline will be subject to Amdahl's Law, and we will be limited by the speed of the human review steps. In that case, it seems counterproductive to use more than a handful of agents (with sub-agents for context management or model swapping). Generating code faster will quickly lead to work queuing for the human reviewer. Your throughput can be no higher than that of the human reviewer. Accelerating beyond a handful of agents increases complexity, such as managing merge queues or coordinating builds, without being able to move faster than the human capacity limit.

If we decide that code is not an important artifact, you can remove humans from the loop, and the constraint shifts to your ability to generate valuable requirements. Typically, that requirement takes the form of a specification. In that case, you want to move as fast as budget constraints allow. It makes sense to use a swarm or workflow to create a factory that spits out code that meets the requirement. (Specification is a very overloaded term in agentic coding. In this context, we mean a document that describes all the software's requirements. Having worked in the era when specifications were common, we know they are detailed, with numbered, tracked, and cross-referenced requirements and a supporting test pack. Creating that kind of specification is a considerable investment of human/agent time for a complex product. Much specification-driven development doesn’t refer to this; instead, it refers to collaboration between a human and an agent via a design document. More on that in another post.)

It's worth noting that engineering happens in both approaches: observing the code or creating a detailed specification. In economic models of automation, there are always “weak link” tasks that constrain how fast you can go.

This post takes the first path: code is an artifact we care about, and humans stay in the loop to read it. The swarm-and-specification factory of the second path deserves its own treatment, and I'll return to it. But even on this path, the binary is too crude. “Humans in the loop” is not a single setting; it is a dial. The rest of this post is about what sets that dial. The answer, as we'll see, is our certainty about the theory we are building. Hold that word; we'll come back to it once we've established why the code is worth reading at all.

Two paths. Answer the value question one way, and the human reviewer is your ceiling; answer it the other way, and specification quality is your ceiling. This post looks at the top path.

Why is Reading the Code Important

In the post Coding Is Dead, Long Live Programming, we discussed the idea that programming is theory-building. Briefly, since we moved on from assembly or C as a programming language to languages like C++, Java, C#, Go, etc., code is no longer simply an instruction set for registers and memory; instead, it's how we create a model that we can share with a compiler, a model that expresses our theory of how we can automate a solution to a business problem, and a model that the compiler can turn into an instruction set.

Coding is simply the act of recording the model we have designed in code that both we and the compiler can understand. TDD, done right, helps us produce a theory by exploring it through tests of its fitness for the problem space.

We noted that the code is important because a 3GL remains the optimal medium we have today for describing a computable theory to solve a business problem.

We need to read the code for several reasons:

  • To gain fresh insights by reviewing the theory captured in the code and possibly updating the theory as a result.
  • To keep the understanding of the theory fresh and in line with the code.
  • Maintain quality so the code can be easily changed to meet future requirements.

Building a Theory With An Agent

When we work with an agent using a process like specification-driven development, we discuss our program's theory with the agent and create a model. It's a dialogue, not a monologue; the agent serves as researcher and critic as we work. If we use an approach like specification-driven development (SDD), we may generate non-code artifacts here, such as requirements and design documents, and perhaps even UML diagrams. We work on these documents with the agent as we build our theory of the program.

When we are satisfied that the dialogue represents our theory of the program, the agent encodes the resulting theory for sharing with us and the computer. Code is used to represent our theory, built from our conversation with the agent, facilitated by those requirements and design documents.

This also tells us where the theory lives when we decide code is ephemeral. If we regenerate the code at will, something more durable has to carry the theory forward, and that something is the specification and its test pack. The tests are not just a gate; they are the executable record of the behaviors our theory must exhibit. This is why the two paths from the start of the post are less opposed than they appear: on one, we keep the theory by reading the code; on the other, we keep it in the spec and tests that outlive any particular generation of code.

If we can read the code, we can verify that the model matches our theory. If the code aligns with the theory, then we have agreement between the agent and us, and we can compile and ship. If the model doesn't align with our theory, we go back to the agent for further discussion.

This is why the ability to read and write code becomes important; it is the shared modeling language. Our natural language is not the modeling language; it is the language we use to converse with the agent about the model. This distinction is important and often overlooked: a compiler does not take our natural-language statements and turn them into instructions; it compiles programming-language statements. These remain the language in which the model is defined, not the language of our conversation. Natural language is not a higher level of abstraction here, any more than it is between two human engineers discussing the model.

Where the theory lives. We converse with the agent in natural language, but the theory is encoded in code — the modeling language. The compiler never reads the conversation.

The Value of Insights

If the code is the artifact that describes the theory of process automation we have agreed upon with an agent, then as the theory takes shape, task-by-task, test-by-test, we gain fresh insights, realizing that there may be better models we can use.

It's worth noting that Geoff Huntley, the creator of the Ralph Loop, suggests that for production code, the loop is observed to facilitate insight and learning.

“It's important to watch the loop as that is where your personal development and learning will come from. When you see a failure domain, put on your engineering hat and resolve the problem so it never happens again.

In practice this means doing the loop manually via prompting or via automation with a pause that involves having to press CTRL+C to progress onto the next task. This is still ralphing as ralph is about getting the most out how the underlying models work through context engineering and that pattern is GENERIC and can be used for ALL TASKS.”

— Geoffrey Huntley, Everything is a Ralph Loop

Cognitive Debt

Without learning, we risk Cognitive Debt. Cognitive Debt (or Cognitive Drift) is the growing lack of understanding that occurs when we do not review the code. If the code expresses the theory, failing to observe it won't allow us to update our understanding of the theory in light of how it works or how it changes over time.

A useful test is whether you can “whiteboard how the code works.” Your goal is not the code's syntax but the program design. Can you explain the key design decisions the code represents without looking at the code? If you can't, you have Cognitive Debt. It's the same problem you face when transferring to a new team and initially struggling to understand the design decisions that make up the software. You have to put in the work to read the documentation and code to understand the program's design and the theory behind it.

There is a team version of this that is easy to miss. Naur's theory is held by the group, not an individual author. If the agent holds the theory and no human on the team does, the debt is not the driver’s alone; it belongs to the whole team. And it comes due at the worst moment: an incident, or a handover, when the one mind that understood the design turns out to be a context window that was cleared several tasks ago.

It can be dangerous to rely solely on a quick post-feature-completion skim, as the lack of effort makes it hard for the theory to “stick.”

“It's the difference between listening to audio books versus reading them yourself. You read the words on the page, actively try to understand them, fold them into the larger context of the story, and then develop your own understanding at your own pace. This is a rich, active, and engaged activity that requires your creativity and effort. Listening to an audio book is a passive one done at someone else's pace, leaving less time for careful consideration and understanding. The results aren't the same.”

- Aaron Stannard, Software Hyper-Delivery Is Retarding Us

The Problem of Quality

If we look at the findings of the Faros report, we note that AI code generation has a quality problem:

  • Output is up (a 66.2% increase in epics completed, a 33.7% increase in tasks, and a 16.2% increase in PRs merged)
  • But defects are as well (a 242% increase in production incidents and a 28.7% increase in bugs per PR)
  • High engineering capability (as evidenced by DORA) offers no immunity here.

The agent reviews existing code to guide future code and replicates the patterns it finds. As a result, bad idioms tend to be replicated (agents generate them from their existing context). If you fail to spot this, the cost to undo the pattern grows with each turn. The faster you generate code, the faster the idiom spreads. If the idiom has high coupling and low cohesion (such as duplicated knowledge), it is easy for the agent to accelerate your codebase into a big ball of mud faster than any human developer would.

The speed of agentic coding makes it an amplifier, and without oversight, it amplifies poor quality just as much as good quality. Focusing human attention on generating high-quality code from the start, or steering legacy code toward better patterns, can accelerate improvements.

A Brighter Episode

The quality problem above is not abstract. Here is what “reading the code” looks like in practice, and why the theory has to stay with a human. Watch how far the agent drifts each time I stop steering.

Claude, acting as reviewer, points out that for a new Brighter feature (replay outgoing messages from the outbox when the inbox indicates the message has been seen before, with opt-in), only “seams” tests exist, and there is no end-to-end test.

I ask Claude to address this issue. It writes some tests to do so.

These tests continue to “fake it”. The test directly inserts into the inbox and outbox to mirror messages that have already flowed through. Then it manually inserts a test message for a new flow into the Brighter Channel and runs the pump.

This use of testing seams does not meet the requirement for a real end-to-end test and is exactly what the reviewer criticized.

I push back

We need to:

  • Use the InternalBus to communicate
  • Use an InMemoryMessageProducer
  • The InMemoryInbox and InMemoryOutbox will record the receipt of the message and the outgoing message.
  • Post via a command processor to a handler.
  • The outgoing message will now be on the InternalBus
  • Run the pump via a Performer to process the bus. At the end of the test, stop the Performer to terminate.
  • Now replay the message, using a Post via the CommandProcessor
  • The handler should not run, but we should queue the message to the InternalBus (use Post to do this without the need for a Sweeper)
  • Await the InMemoryOutbox status of the message, changing to be outstanding instead of dispatched, with a timeout on the cancellation token

Claude can then write an effective test using these instructions. But it doesn't time out the polling loop for the outstanding message. I ask it to do that using CancellationTokenSource with a timeout. It tries, but then checks whether the CancellationToken has been signaled via a call to Task.Delay instead of just checking IsCancellationRequested on the token. I steer again.

But it would not have succeeded without help. And we would have had lower quality because nothing was asserted about the flow. Or we might have had a weird “hack,” like using Task.Delay to trigger a timed-out exception instead of checking for cancellation.

Observe the Loop

If we decide that humans should observe the loop to grasp the theory, what does that mean in practice? Do we review at the end of the loop, or review in the loop? One big PR, or many small ones? Anyone who has reviewed a large PR knows that the best you can do (and the best agents will do) is to sample it, looking for key abstractions to review. So one obvious answer is that we want to work in chunks with a reasonable cognitive load. That is less about agentic engineering and more about how we work.

It's worth being precise about what is scarce here. It is not review approvals; it is human understanding. Agents generate code far faster than a human can build a theory of it, so Amdahl's constraint isn't the human's reading speed; it's their comprehension bandwidth. The gears that follow are a way of rationing that bandwidth: spending it where certainty is low, conserving it where certainty is high.

But we can be a little more helpful when we consider the effort we put into understanding the code.

Certainty and Feedback

The answer to where and when software engineers and agents interact is nuanced and not amenable to a binary decision. Like many things, it’s contextual. But there is an answer to what that context is: our certainty about the work. In this context, the different approaches people use when developing with agents become techniques rather than manifestos.

Understanding that variable, certainty, allows us to make informed choices about how we work.

This also answers the question we opened with. Code has value because it is where our theory lives, but the amount of human effort we spend reading it is not fixed. It rises and falls with our certainty. The binary “Does code matter?” was the wrong question. The better one is “how certain am I about this theory right now?” — and that question has a different answer task by task.

  • With less certainty, we need feedback more quickly; we take smaller steps, measure frequently, and revisit the design in response.
  • With greater certainty, we can obtain feedback more slowly; we take bigger steps, measuring in batches and nudging the design in response.

Certainty sets the step. The less certain we are, the more often we seek feedback and the smaller the step we take before review.

Relating this to theory, then: if we have confidence in the theory, we have certainty and can move in larger chunks; if we don't, we move in smaller chunks.

That is useful because other techniques differ in how often they provide certainty-based feedback, and we can rely on them for advice on working with agents.

Driving in Gears

In the book TDD By Example, Kent Beck uses a metaphor to describe how granular your tests should be: driving in gears. To paraphrase:

  • Sometimes you can write tests at a very coarse-grained level, driving in high gear. You understand the behaviors you want, the interfaces you need, and how to implement them. Each test can exercise many lines of code in the implementation. You make large jumps. More than ten minutes might pass between test runs. You are confident you won't go down the wrong path, so the potential waste if you have to revert is not top of mind. Perhaps you write your tests at the level of the HTTP API itself.
  • Often, though, you need to slow down a little, driving in a middle gear. You want to explore the behavior of the code under test, taking your time to define the interfaces and figure out how you will implement them. You write less code with each test, moving in smaller jumps. Typically, only a handful of minutes pass between tests. You know you might have to revert the last chunk, but it’s a manageable risk. Most likely, you write code at the port in “ports and adapters” or in the service layer of a layered architecture, perhaps even dropping down to a facade over the domain.
  • Finally, at times, the path becomes difficult, and you slow to a crawl, driving in 1st gear. You are unsure how to implement the code under test. You can't think about the interfaces because you don't understand how to implement them. You move in tiny jumps. The interval between tests is short. You regularly revert code after taking the wrong path; your code bears a strong resemblance to a spike. Most likely, you test the details directly, perhaps with a plan to discard the tests once you understand how to implement them.

The gears metaphor is useful because it captures the idea that we may apply a technique differently depending on our certainty.

What drives us here is the speed of feedback. How quickly can we get feedback on a decision? The less certain we are, the more feedback we need, since each decision is fraught with risk. The more certain we are, the less feedback we need, because the risk attached to each decision is lower.

Gears and Certainty

We can take this understanding from that: our certainty about the theory, the confidence I have in it, is expressed by how large a jump I make with each test. In other words, which gear I drive in depends on how certain I am of the theory.

During implementation, the agent takes our agreed-upon theory and turns it into a model. So, how much do we observe the model and the code as the agent generates them? Do we review each test before the agent implements it? Do we review the implementation after the agent completes it in response to the test? Do we review each task after it is completed? Do we allow the agent to finish the whole feature and then review the PR? Do we allow agents to review and push to production?

All strategies for when to add human guidance to the agent in the code are possible, and we may use any of them at different times. Instead of picking a strategy via a belief system about how AI Engineering should work, we recognize that any piece of work may call for a different approach depending on how certain we are. The certainty we have in the program's theory determines which gear we drive in, given our need for feedback.

So folks are mostly asking a binary question: human-in-the-loop vs. human-outside-the-loop, when they should realize that both are valid and depend on how certain you are at any given time.

Certainty is the main dial that causes us to shift gear, but we recognize it isn't the only one, as our comfort with lack of certainty can vary by blast radius. A boring, high-certainty change to an authentication path or a payments flow still causes us to downshift, because the cost of being wrong is high even when the odds of being wrong are low. If certainty is 'how likely am I to be wrong,' then blast radius is how much being wrong would cost. When both are favorable, drive in high gear; when the stakes are high, remain in a lower gear, however certain you feel.

Two dials. Certainty picks the gear, but a high blast radius pulls you down one — keep a hand on the stick even when you feel sure.

The Gears at a Glance

Here is the whole model on one page; the sections that follow expand each column in turn.

Driving in gears: certainty sets the gear, and the gear sets how much the agent does before you look.

High Gear

At times, you can observe, at a very coarse-grained level, driving in high gear.

The theory is boilerplate: a simple HTTP API with a well-defined OpenAPI specification; a simple Kafka consumer with a well-defined AsyncAPI specification; and a simple CLI application.

Books, blogs, and documentation all cover exactly how to implement them. The agents' training set includes those books. You are bored by the details.

You want to hand over a specification and come back to working code.

The work may be slash-and-burn; you expect that if significant change is needed, you will return to the specification, modify it, and start over. The complexity and scope of the work make this approach economical.

Strategy

You agree on a design with the agent based on the requirements and acceptance criteria. The agent produces the design. You use an adversarial agent to review it. You skim the design yourself for surprises, but otherwise move quickly. You have the agent create the tasks and ask it to plan the work to be test-first, goal-seeking toward getting the tests to pass. You use adversarial agents to check the design and tasks. Before execution, you may skim the tasks for surprises.

You ask the agent to implement. A green test suite with good coverage is your definition of done.

You are aware that agents rely on existing code for style, but you are confident that your agent instructions and the model provider's training set can provide an adequate approach.

You rely heavily on adversarial agents to verify the code. After a task passes tests, the agent reviews the code, identifies refactoring opportunities, and implements them. A final adversarial review by an agent examines the code for issues that fall below a quality threshold. The agent iterates until the code meets that threshold. You review the final PR before merging. Perhaps you skim the program's theory for any wild gotchas and review for issues of “taste” by looking for code smells the agent may have missed. You refine with the agent if needed.

Tens of minutes might pass between asking the agent to implement the tasks and your review. This creates the risk that you might lose that time and any tokens used if you can't easily re-engineer the work. But you consider it a manageable risk for this problem and the theory needed to solve it.

If you are pushed to revert, you may realize the work is not the simple boilerplate you hoped for. Subtleties remain, and the theory does not turn out as expected, with missing cases or drifting. You downshift into a medium gear and revisit the design.

You ship to production. You are confident that if the tests pass, the theory will be correct, so the potential waste of having to revert is not top of mind. You rely on good observability to “test in production,” with a fast MTTD and low MTTR, so you can respond to issues quickly. The code is simple, and you expect that any issues can be easily triaged and fixed by an agent for later review.

Boredom as a Signal

Boredom is a useful signal for High Gear. It signals that you have the theory, and that what remains is the labor of implementation. When you get bored, your ideation and creativity no longer drive the theory. Instead, you are now on the long road of implementation. For the neurodiverse amongst us, we lose interest in the problem quickly. There is nothing novel here.

Boredom is a clear signal that it is time to shift into high gear and ask the agent to finish. The theory is clear to both of you; the details of implementation should hold no excitement. No alarms and no surprises.

Medium Gear

You don't know the theory at the outset, but you have solved similar problems before and expect to develop a workable theory quickly. You want to make steady progress and leave confident that you and the agent share a working theory. In design, you want to explore the behavior of the code under test, taking your time to define the interfaces and figure out how you will implement them.

You know your experience will guide you, helping you move at a steady pace. You are intrigued by the implementation details and not bored by the prospect of the work.

You want to work with the agent to explore a solution to this requirement, not just delegate it.

The work needs to be sustainable and amenable to future change to justify your investment in the theory. Because you want change, it's important that the theory is supple, allowing future modifications based on new requirements or insights.

Strategy

You agree on a design with the agent based on the requirements and acceptance criteria. There is back-and-forth between you and the agent over the design. You focus on the application's behaviors. You suggest key responsibilities and roles. From those, you work with the agent on key abstractions or code examples. You seek agreement with the agent on the design theory. It takes a couple of iterations for the design to reach the point where you are happy that the agent can represent the theory in code. You use adversarial agents to cross-check the design against the requirements and have them iterate until all issues exceeding a threshold are resolved.

You have the agent create a task list. You emphasize that it should be test-first, goal-seeking toward getting the tests to pass. You review the tests before executing the tasks. You ensure that the behaviors in the test suite match your expectations as defined by the acceptance criteria. Have an adversarial agent cross-check the tasks against the design and iterate until all issues above a threshold are resolved.

You ask the agent to implement.

You review after each task completes, or perhaps after two or three similar ones, only once your TDD tests go green. You seek to guide the agent toward refactoring opportunities. You do this because you want any fresh insights to update your understanding of the theory, too, to avoid cognitive debt. You look for code smells, looking for insights in the code that could improve the design. If changes are needed, you feed these insights back to the agent and work with it to adjust the design and the tasks. If you adjust the design for fresh insights, you may ask an agent to launch an adversarial review of the design changes for consistency after this change.

You are aware that agents rely on existing code for style, so you put effort into the early iterations, keen to establish the style for this work. This will slow the initial iterations, but later iterations will be faster as the agent learns how we want this code written from the code already delivered.

Typically, only a handful of minutes pass between reviews. You know you might have to revert the last task, but it’s a manageable risk.

You feel comfortable that you hold the theory, despite the evolving solution. You are not bored, because the unfolding theory captures your interest.

At some point, though, you may become bored. The design is now stable, and fresh insights no longer appear with each review round. You are happy that the code style has now been established in the codebase. You expect the remainder of the implementation to follow what has gone before, so you switch up a gear to High.

Conversely, after a task, you might realize you don't understand the theory as well as you hoped. Some of the code is opaque to you. You don't recognize the work the agent just provided. Or perhaps you keep running into challenges with the agent’s decisions. You worry about the quality of the code the agent is writing. You keep reverting tasks. You switch down a gear into Low.

A Brighter Example

Brighter has a workflow driven by the /spec command hierarchy. For design, the flow proceeds from /spec:new to /spec:requirements to /spec:design. At any point, you can use /spec:review for an adversarial review. Once you have a design, use /spec:ralph-tasks a process that breaks the design into a series of /test-first tasks that drive implementation in a TDD approach.

Once the task list is built, a user uses /spec:ralph-implement to run a loop, with Opus as the orchestrator using `auto` to avoid permission requests. The iterations of the loop before we halt are controlled by a variable, such as the number of tasks, the budget, or a stop file.

In Medium Gear, you set the /spec:ralph-implement to the number of Tasks to complete before stopping at 1. Once the design begins to settle, you can increase it to 2. Once you switch to High Gear, you set it to 3+ or even ask it to complete all remaining tasks. You decide how large a chunk of work to do before stopping. You think about context management and don't allow the context to grow so large that the agent becomes dumb and costs rise. Your task-by-task context-clearing strategy helps with this. Only your orchestrator builds context over the whole run. You can always restart that if you need to, picking up from the first unfinished task.

Brighter's Ralph loop is sequential; it assumes that the human reviewer is the bottleneck. More confidence in high-gear might lead to a swarm or a dynamic workflow, with tasks run in parallel where possible. But costs will rise, and with a human review step, such speed is often not justified by the cost when driving in medium gear.

Low Gear

You are unsure of the theory because you have not solved a problem like this before. The requirements may be unclear and require further elicitation. The technologies or algorithms involved may be new to you. The domain may be unfamiliar, and you want to explore it iteratively and incrementally. You don't have a clear idea of the theory or of how to discuss it with the agent.

You may lack experience. You feel you need to explore alternative technology solutions to have an informed conversation with your agent rather than blindly accept what it tells you. Perhaps you are a junior engineer. Perhaps you are an experienced engineer, but the solutions require new skills. You need to upskill before you can move at a steady pace.

You are excited to explore what it will take to implement, as this is not something your existing knowledge helps with.

You want to work with the agent as a researcher to understand the solutions to this requirement, but you are not ready to just explain the theory to the agent.

You need to slow down.

The work needs to justify the higher cost. It may be enough that you are learning and will be able to apply those skills to future work, which can then go faster. It may be that this is core domain work that will yield a competitive advantage, so you need to push the boundaries beyond the agent's training set.

Strategy

You may be unsure about the requirements; in that case, engage the agent in a dialogue to establish them and their acceptance criteria. You get the agent to ask you questions. You use the agent as a researcher to explore design options for tackling the problem. You ask it to ideate solutions and point you toward where you can find out more. You have the agent write code to explore unfamiliar tools or interfaces and understand their capabilities. You pause to read blog posts and technical documentation. You discuss how these ideas might work with the agent. Together, you refine them. What theories could you apply here?

You develop a design with the agent. There is back-and-forth between you and the agent. You focus on the application's behaviors. You suggest key responsibilities and roles. From those, you work with the agent on key abstractions or code examples. You seek agreement with the agent on the design theory. It takes a handful of iterations for the design to reach the point where you are satisfied that the agent can represent the theory in code. You use adversarial agents to cross-check the design against the requirements and have them iterate until all issues exceeding a threshold are resolved.

You have learned more about the problem and solution space than before. You have increased your confidence in the theory to the point that the next step is to work on code, so you let the agent proceed.

You have the agent produce the task list. You ensure that the task description is test-first and goal-oriented, with a focus on getting the tests to pass. Given your uncertainty, you ensure that the task list includes a step where the agent will STOP after writing the test and before implementing it; the agent will then invite your feedback on the test. You will review the tests before executing the implementation. Your goal here is learning and feedback. You ensure that the behaviors in the test suite align with your expectations in the acceptance criteria. You review the quality of the emerging interfaces. You seek crisp abstractions that are self-describing, invite correct usage, and are obvious without documentation. You make sure you understand the theory the agent will use to pass this test.

After each of your TDD tests goes green, you review. The refactoring step is guided by you. You seek to understand the theory behind the implementation. You look for code smells to identify insights in the code that could improve the design. If needed, you feed these insights back, adjust the design, and update future tasks to account for them (including the design documents).

If you adjust the design for fresh insights, you use an adversarial agent review to evaluate the design changes for consistency after the change.

Typically, a one- or two-minute pass occurs only between reviews. You know you might have to revert the last task, but it’s a manageable risk.

At some point, you may become confident. You rewrite the remaining tasks to eliminate the need for the agent to seek your approval after writing the test. Then you shift into Medium gear.

A Brighter Example

Instead of using, /spec:ralph-tasks we can use /spec:tasks to build our task list. This generates a similar task list to /spec:ralph-tasks but has an explicit STOP after the agent writes a test, to wait for a human reviewer to approve (or ask for changes to) the test, before implementing.

This pause treats the test as an important part of the design process – it’s where we figure out how to express the system's behavior and what our 'interface' should look like. We go slow because we believe we need to learn, drive quality, or are uncertain enough to recognize that the design may emerge as we go.

When using /spec:tasks to run these tasks, work proceeds task by task, prompting a pause for each new test we write.

Earn Your Gear

We opened by asking what code is worth to engineers post-GenAI. The answer is that code has value because it is where the theory lives.

That was never really the question; we discussed code as the vehicle for the theory in the last blog.

The question is what cost we bear in sharing that theory with the team from the agent.

We suggest setting the price based on our certainty. When we are certain and trust that the code produced by the agent aligns with our theory, we can take large steps. When we hold the theory but are less certain the agent shares it, we reduce the size of our steps. And when we ourselves have no certainty about the theory that the team and agent need to share, we take small steps. At the same time, we adjust this based on the cost and the blast radius if the theory is wrong.

The binary that dominates our discourse — human in the loop or human out of it — is not how we should frame this. We are not one kind of shop or the other. We are AI drivers, shifting programming gears as the road demands: high gear through the boilerplate we could write in our sleep, low gear where the domain is new and the design has to emerge. We shift within a feature, sometimes within an afternoon. Boredom and confidence are the upshifts; opacity and reverts are the downshifts.

What agentic engineering does not do is remove judgment. The skill is no longer writing code; the skill is calibrating our certainty and then deciding how much of that code to read.

And here is the trap.

The economics push us toward high gear because it is faster and cheaper. The Faros numbers — output up, but production incidents up 242% — are what high gear looks like when the certainty behind it is wishful. Boredom is an honest signal that we have earned our upshift. Impatience is not.

So drive in whatever gear the work demands, and be honest about which gear that is. The question was never whether to read the code. It was how certain we are that we have earned the right not to do so.

The other road — code treated as ephemeral, humans out of the loop, the theory carried instead by the specification and its test pack — deserves its own post.

Read more...

One observation from using agentic engineering with Brighter is that the old adage of “work expands to the resources available” is definitely true. In an OSS context, where I am paying for tokens out of pocket, that is my call, but the trade-offs need thought in commercial settings.

Quality From The Loop

The cause, I think, is that the loop of generate => evaluate => repeat. It helps drive quality, typically higher than we would have reached through manual effort.

In my typical setup, a sub-agent (or new agent) with a fresh context reviews the last milestone.

This review agent assigns a score derived from its evaluation. We want to ignore findings below a certain score as “noise” so we don’t get too many “false positives.” We break the loop when all of the evaluation findings fall below that threshold.

Typically, the review process is run after gathering requirements, creating the program design, building the task list, and generating the code.

In essence, it helps to prevent the slop that a first generation may create. Often, that is about the evaluator having a fresh context, both in terms of context rot and the agent’s tendency to assume that earlier work is right, whereas the sub-agent is instructed to be adversarial.

While those iterations increase cost, the result is higher quality, which is what we want. Right?

How Much Quality?

This is our first trade-off. What quality threshold do we need? Well, it's OSS, right? I want folks to be able to rely on it. So, we set a low-ish score for what we want to address.

That is the first cost issue. Some of those items might have been skipped in the past if the trade-off between my time, shipping the feature to get feedback, and the effort was weighed against how important that finding was.

But I also find myself more inclined to take the harder path.

More than that, some features might have choices about what we offer. Typically, what edge cases might we support? It’s higher quality to match more of those cases, but should I? Well, it’s OSS, and I am ensuring that we take care of people who have invested in us. Right? But perhaps in the past, some of those edge cases might have been justified by the small number of users working around the issue, or even by those users deciding we were not the right fit.

A Brighter Example

My current example: I am working on a feature to add DB migration for our Outbox and Inbox. At startup, we will check that you have the latest version, and if not, migrate you. We will lock the producers and consumers during an update, so that it works in a distributed environment.

But what about existing databases? Do we just assume that you are on the DDL we shipped with V10, and only upgrade you from there? Perhaps you are stuck on V9 because the cost of a DB migration is a pain point? Maybe you are on an older, now unsupported version, because of this.

One answer is to go back and figure out all the versions we have shipped from the DDL change history in Brighter. In that way, it doesn’t matter which version you are on; we can upgrade you. (There is a little trade-off in that we can’t switch you from text to binary content as part of that, but you probably don’t want that during an upgrade, as it’s a choice.)

Now, that is quite a lot of research to go through the git history across multiple DBs we support, and it carries a high risk of getting it wrong if we do it manually. But an agent is good at this kind of research. So, before I know it, I am asking the agent to investigate, burning tokens to assess the feasibility of something I would probably have rejected if I had to do it by hand.

I would have favored just getting it out and assuming folks are on the V10 baseline, perhaps V9, as we support that, if I had to do this by hand.

But now, I am burning tokens, and the agent has answers. And now I have spent tokens on the answers, well, isn’t that the hard part? Why not just work with the agent on the requirements and design?

And before I know it, we are burning tokens on the design, after all, it’s quick to see what this will entail.

And having burned those tokens investigating, designing, well… it would be a shame not to spend tokens implementing it.

It’s seductive. I could have made this better than I would have if the friction of the time commitment to OSS hadn’t held me back. I can make my dreams real. I just need to pay for the tokens.

But token costs have always been subsidized…the first hit is always for free kids…and soon the choices may be harder.

Cost Pressure Lowers Cognitive Load

And perhaps, for OSS that many will use, where I feel the token cost because they come out of pocket, I can easily make this call.

But in a commercial setting? If friction is low, I may feel pressure to hit the high bar; I don’t want my colleagues to think I ship AI slop, and I don’t want to produce unreliable software. And so the token cost goes up.

But perhaps, as importantly, the software’s cognitive load is increasing. It handles more edge cases, includes paths for very specific circumstances, and may not opt for simplifications that might have been forced upon us by friction.

When we talk about cognitive debt, it’s not simply about failing to observe the loop or to appreciate that we are still programming, just not coding. It’s also about our ability to add software that we might have previously rejected because of friction.

We have been burned in the past, when we made something hard easy (for example, when we made it easy to write a new service via FaaS and ended up with a nanoservice sprawl). It’s hard not to believe that we won’t get fooled again.

Rising Token Costs To The Rescue?

But perhaps rising token costs will actually help. Maybe it becomes the new friction, the new “is this worth it”. Once it was my time, or commercially, the team’s time, when there were so many other things to build. Now it’s the token spend. Will this be the best use of our token budget this month?

The free lunch may be over…soon…but maybe some friction will help us keep cognitive load lower again.

The Cost is Always Ownership

It’s an old adage that the cost of ownership is 10X the cost of creation. Much of that has to do with the lifespan of software and the folks needed to support it, compared to the build costs. Whilst no one has really reviewed this request in the world of agents, early indications of cognitive debt indicate that it probably holds true.

Making it easier to add software comes with the responsibility to ensure the software we create is worth owning; we could bankrupt ourselves with cognitive debt.

Copyright © Ian Cooper 2024+ All Rights Reserved.

Summary

In this post, we argue that the role of a coder now belongs to a coding agent, but that the distinct role of a programmer defines the software engineer's role in interacting with the agent to produce computer programs. At the heart of this, I think, are answers to questions we have about software maintenance, cognitive debt, and the education of junior engineers.

Coding is Dead

Commentators in the tech industry have recently declared that “coding is dead.” The triggers for this statement are:

  • LLMs, whose transformer architecture excels at pattern recognition and translation, have achieved a high success rate on key coding tasks, particularly since the December 2025 model releases.
  • The improvements in coding agents have pushed the success rate for the code that an LLM generates above the 80% threshold, where agents can begin the journey toward independently doing work without human review for short tasks and increasingly lengthening.
  • A broader understanding of good practices in context engineering—using the file system as memory and aggressively managing context by creating fresh agents—has helped prevent the drop in quality caused by context rot.
  • The improvement of techniques to capitalize on agents' relentlessness to get them to iterate toward high-quality outputs via clear acceptance criteria for “done-done,” avoiding low-quality output caused by reward hacking. See How To Ralph.

Of course, all statements like “coding is dead” are hyperbolic, but it would be foolish to refute that in 2026 and beyond: agents will write the majority of code.

If coding is dead or will be dead by the early 2030s, what will software engineers do?

Programming is not Coding

The terms programming and coding can seem interchangeable, but historically, they are not the same. They refer to different activities, but for many years we have performed them together, making the distinction unclear.

Following Yourdon and Constantine in Structured Design, we assert that software engineering has the following roles:

  • Analyst: talks to the customer to elicit the requirements for what we want to automate.
  • Designer: determines how to meet the requirements and required quality attributes of the software, for example, how we split into modules, how those interoperate, scale, provide transactional integrity, etc.
  • Programmer: decides on the design of programs: algorithms, data structures, and design patterns – writes coding specifications. QA of the program.
  • Coder: implements the program design and has knowledge of syntax.

In the latter two roles, the original distinction between programmer and coder stems from an era when code was often punched out on cards rather than encoded in a high-level language. A programmer handed the coder a coding specification that defined the program to be written:

“Each component subprogram is coded using the coding specifications. Ideally, this phase would be a simple mechanical translation; actually, detailed coding uncovers inconsistencies that require revisions in the coding specifications (and occasionally in the operational specifications).” Bennington, Herbert; Production of Large Computer Programs.

Once, in larger organizations, different people performed the roles of programmer and coder. The programmer specified the required code and handed the resulting coding specification to the coder, who then authored the code to be loaded into the computer.

With the introduction of high-level languages, it became easier for developers to perform both the programming and coding roles simultaneously. The compiler did much of the work that the coder had done before, so the program specification could be completed in parallel with coding. Later, the RAD/agile movements leveraged the ease with which programs could be specified as code was written, speeding up software development by collapsing many steps into a single process and shifting quality left via Test-Driven Development.

It is because of high-level languages and RAD/agile practices that many people use “programming” and “coding” interchangeably. But they are not the same.

Coding or Programming: What is the Difference?

You can reason about this difference easily. Once you learn multiple programming languages, you come to appreciate that all of them have variations of the same concepts: control structures, variables, etc., that only differ by their syntax. By contrast, data structures and algorithms don't depend on specific language syntax. Object-Oriented models of your domain can be developed using CRC cards or UML, without writing code. We think of these concepts as existing outside the syntax and the code, at the level of the program. You can reason about a program in a conversation, or on a whiteboard, without needing to use a specific syntax.

When writing code, you probably used to look up what you needed in documentation or examples and translate it to your domain. These statements were code and reusable as such, once fitted to the logic of your program.

We often overestimate the importance of coding in this merger of roles. It is likely that you spend more of your time as a programmer debating which frameworks and libraries you want to use, and what classes and interfaces form part of your model, than you do over whether you should use a for or while loop.

Knowing which syntax to use in a particular language is the coder's role; knowing what the code should do is the programmer's role.

Alex Booker provides a clear description of the difference between coding and programming in his video Coding or Programming: What Is the Difference?”

XP: Elaboration, CRC Cards, Programming & Coding

Armed with this understanding, it may be helpful to identify when a developer is in a programmer or coder role. Consider the roles we took in an XP-like software development life cycle (SDLC). In a classic XP cycle, the four roles rotate through a familiar sequence. Notice how little time is actually spent in the coder role once you decompose it:

The crucial observation is at the bottom. In pair programming, only one person is ever in the coder role at a time. The other — and, in mob programming, everyone else — is in the programmer role : watching, correlating with the design, and catching any drift.

This is why so many developers underestimate how much of their day is programming rather than coding. You switch roles constantly, and the coder role is always just one half of a pair at most.

Because we have moved away from thinking about the separation of coder and programmer, we have lost sight of how much this classic XP cycle actually shifts focus away from the coder role toward the others.

The Middle Loop

In their book Vibe Coding, Steven Yegge and Gene Kim discuss the Middle Loop, the construction of the context the agent uses when carrying out tasks. We often call this specification-driven development. This agent-assisted loop maps onto the activities of the classic XP loop almost exactly — with two changes: the agent takes the coder role, and a new researcher role emerges that was previously invisible because it was folded into programming.

The middle loop runs once per story; the inner loop runs once per task. The loop is fundamentally the same as XP, but the developer no longer performs the coder role.

We introduce a new role of researcher. Previously, when a programmer visited Stack Overflow, read documentation, or extracted candidate objects from a requirements document, it was just another part of the programming process. The agent makes it separable: you can hand “go read the existing codebase and tell me what patterns are established here” to the agent as a discrete task. The agent can help with heavy lifting, but the decision about what to do with that research, based on discernment and taste, remains with the programmer. For that reason, we call this new role: researcher.

What hasn't changed is more important than what has. The analyst, designer, and programmer roles still belong to the human. The inner loop looks like pair programming, where the agent holds the keyboard; the coder role is permanent, but the programmer role, the one that watches and correlates and builds the theory, is still human-in-the-loop.

A speed difference exists, but it is easy to misread. The agent eliminates the friction between “we've agreed on the design” and “the code exists.” It used to be difficult to feed design insights from the inner loop back in — you'd have a better idea, but resist it because of the rework cost. The specification was hard to change. We fell for the “sunk cost fallacy,” but with an agent, you can iterate on the design freely because execution is cheap. The constraint shifts entirely to the quality of your theory.

The key, though, is that much is familiar from our first loop. Even with an agent's help, many roles still fall to the human-in-the-loop.

High-Level Direction Judgement and Taste

“It’s not perfect, it needs high-level direction, judgement, taste, oversight, iteration and hints and ideas. It works a lot better in some scenarios than others (e.g. especially for tasks that are well-specified and where you can verify/test functionality). The key is to build intuition to decompose the task just right to hand off the parts that work and help out around the edges.” Andrej Karpathy, x.com

Judgement, taste, etc., are the programmer's responsibilities. The agent is good at coding, but it needs help deciding what to write. That is why we still need the programmer role and don't simply delegate that to the agent. Maybe one day it will do that role too, but that day is not today.

Watch the Loop

Why suggest that the programmer/coder split is like pair/mob programming, where one partner watches the other? Because the agent needs to be observed.

“It's important to watch the loop as that is where your personal development and learning will come from. When you see a failure domain, put on your engineering hat and resolve the problem so it never happens again.

In practice this means doing the loop manually via prompting or via automation with a pause that involves having to press CTRL+C to progress onto the next task. This is still ralphing as ralph is about getting the most out how the underlying models work through context engineering and that pattern is GENERIC and can be used for ALL TASKS.” Geoffrey Huntley, Everything is a Ralph Loop

Now, we might argue that this is inefficient. Do you need to review the agent's output? It's just coding. Just use a linter and check that it passes the tests.

If agents can produce syntactically correct code indefinitely, what's the actual constraint?

Programming as Theory Building

Peter Naur's essay, “Programming as Theory Building,” states that the most important part of programming is theory building: “ On the Theory Building View of programming, the theory built by the programmers has primacy over such other products as program texts, user documentation, and additional documentation such as specifications.” The theory, the knowledge built up by the programmer in crafting the program, is important for three reasons:

  1. The programmer can explain how the program maps to the problem domain: “the programmer must be able to explain, for each part of the program text and for each of its overall structural characteristics, what aspect or activity of the world is matched by it.”

  2. The programmer can explain the decisions that led to the choices expressed in the program text and documentation.

  3. The programmer understands how to modify the program to support change: “Designing how a modification is best incorporated into an established program depends on the perception of the similarity of the new demand with the operational facilities already built into the program.”

It is the latter point that is key. In large codebases, our ability to maintain a program depends on our understanding of the program's theory. The theory lets you modify the program successfully because you understand how the program's theory should be adjusted to meet the new requirement.

“The point is that the kind of similarity that has to be recognized is accessible to the human beings who possess the theory of the program, although entirely outside the reach of what can be determined by rules, since even the criteria on which to judge it cannot be formulated.”

The risk is that modifications made to a program, without understanding the theory, lead to decay. Most of us have experienced decay when modifications accumulate as additions that don't fit the program's overall theory. Eventually, the program becomes incoherent, and we are forced to rewrite.

“The death of a program happens when the programming team possessing its theory is dissolved. A dead program may continue to be used for execution in a computer and to produce useful results. The actual state of death becomes visible when demands for modifications of the program cannot be intelligently answered. Revival of a program is the rebuilding of its theory by a new programming team.”

Theory is the key to maintenance. If you don't have the theory, you will struggle to maintain the software because you will struggle with the role of a programmer.

“A very important consequence of the Theory Building View is that program revival, that is, reestablishing the theory of a program merely from the documentation, is strictly impossible.”

Code Reviews and Theory Building

In teams that don't use pair programming, a common compensation is to do code reviews. The purpose of both pair programming and code review extends beyond quality assurance; it is about sharing the program's theory.

Often, teams struggle with reviews as flashpoints or have arguments in pairing because they fail to do enough theory-building when they pull the story from the backlog, a process we call elaboration or alignment. A simple way for many teams to improve code reviews is to avoid doing design there but to shift it left into elaboration when the story is pulled from the backlog. This shifts the theory building to an earlier step. The review then just confirms that the code matches the theory. Conflict arises when the theory is unclear during pairing or review.

Pair programming and code reviews spread the theory through the team. Junior developers often don't have the same facility with theory-building as senior developers. Pairing, or code reviews, allow developers to exchange the theory through conversation and reach alignment.

These exchanges work best when they focus not on the code but on the program's theory.

The danger is that a new developer on the team, who has yet to build the program's theory, in conversation with other developers, incorrectly specifies the program without it. Without the theory, their programming specification is likely to be wrong, and the resulting code will be wrong.

InnerSource, OpenSource, and Theory Building

OpenSource, or InnerSource, projects often encounter this need to help contributors acquire theory. Often, Open-source projects identify tasks that require less theory to help new contributors get started. Once contributors establish a track record, maintainers begin investing time in helping them understand the theory.

LLMs and Theory Building

The LLM cannot be used in the analyst, designer, or programmer roles, only as a researcher to support, because it cannot capture the theory of the program as expressed by Naur.

  • The LLM does not have the context that lives outside the code, docs, and ADRs.
  • The theory cannot be reconstructed only from the artifacts in the git repository [Naur].
  • The LLM struggles with large programs. They remain too large for it to hold it all in the conversation and avoid context rot.

As such, an agent struggles to understand the program's theory and fulfill the programmer's role. This is why the design is a conversation with the agent. This is why the loop must be observed. As the programmer, you are developing the program's theory.

This, then, is the danger of handing the role of programmer to the agent, rather than using it as a researcher. You no longer have the program's theory, which will cause the software to decay with modification.

This is why, when working with an agent, it is important to observe the loop: we need to build the program's theory. We may choose to work either as a pair programmer, reviewing tests and implementation as they are developed, or as a code reviewer, reviewing the agent's PR. Again, both will be easier if we shift left and put more effort into designing our program and building a clear theory.

Agents' speed at writing code does mean that the old conflict, in which design insights that update our theory are much more easily applied, persists. With hand-written code, we may resist the amount of work a change to the theory represents for an insight. But with an agent, we just ask the agent to update the design, change the tasks, and add tasks to modify existing code.

Of course, updating the design for a new theory requires tests that confirm the behavior remains unchanged when we change the theory. This is why we practice TDD. (This is also why we do TDD against behaviors and not details – because we want to be able to revise our theory, not make it rigid).

A Programming Episode with Brighter

As I am writing this, I am working with an agent to pick up a backlog item for Brighter. Configuration for Brighter can be hard. Even for simple CQRS, you need to ensure that pipelines are consistently sync or async, handlers are registered, and when you use messaging middleware, message mappers and transformers, subscriptions and publications, reactor and proactor pipelines, the whole thing has plenty of opportunities to trip users (including agents) up. The “pit of success” for Brighter is a major source of friction, leading developers to reject us in favor of other frameworks.

What we need is static analysis, diagnostics, and dynamic analysis (Roslyn) to help. It looks like a useful backlog item to burn some tokens on.

Working with an agent to fulfill this, once we outline the requirements, requires us to pass our ADR steps.

We use our workflow spec:design to kick this off, and the agent has made a pretty good attempt to design a tool that meets the requirements.

But Brighter is a mature codebase, so it gets it wrong. It can't hold all of our code in its context. Its approach isn't dumb; it just misses a lot. It fails to understand the levels at which configuration can occur: local, with producers to the external bus, and with configuration to an external bus.

It doesn't yet have a theory for how to do this.

I give feedback on it. I explain the theory of the levels of configuration. I tell it about IBrighterBuilder and how that can help.

It refines its approach. It's better, but while it recognizes that we need a model of our pipelines, it fails to associate that with using our IAmAPipelineBuilder<T> to create pipelines; instead, it describes (a dry run, as I call it) rather than building.

I give it more feedback. I explain the theory behind using IAmAPipelineBuilder<T> to populate a model describing a pipeline for validation and reporting.

We re-run, and now it proposes extending PipelineBuilder with describe methods to build a model. This looks better.

I notice how it is setting up its rules. We have an implementation of the Specification pattern already checked in for future functionality. It is in an assembly that depends on this one, but we can move it and its tests. I discuss it with the agent as a better way to implement this.

This is iterative, a conversation with the agent as we work out the theory of how this fits together. I already hold much of the theory of how Brighter works. I can extend that theory to what might work to implement this feature. The agent can pursue this faster than I can, by reading existing code and figuring out what it would take to make it real.

This is programming. We are building the theory of how this works.

Cognitive Debt

What happens if we don't build on the programmer role, build the theory, but instead delegate the programmer role to the agent? We are already beginning to see that this problem is happening and being recognized.

  • Cognitive Debt increases – the gap between our theory about the program and the code and documentation. Naur tells us that documents are not enough here.
    • Because of the lack of theory, it becomes hard to make decisions.
  • Coherence declines. The software no longer has one theory; the agent lacks context for this. Instead, multiple theories exist. As the number of theories grows, our ability to make decisions rots, because there is no theory to make them against.
    • Our software becomes a “big ball of mud,” and the only way to recover the theory is to rewrite it.
  • Production issues become hard to fix because no one has the theory anymore.
    • Indeed, some companies may find that their “legacy” code problem becomes existential.

We have to own the theory; the speed that coding agents offer in writing code leads us to forget that we need to build the theory, too.

Can LLMs Build Theory?

Agents are continually new developers who struggle to retain the theory, even when it is presented to them in conversation. So agents, like new developers, don't have the theory and can be coders but not programmers without supervision.

When using an agent, developers experienced with the codebase may be slower, precisely because they need to explain the theory to the agent. For short tasks, where explaining the theory might take longer than just doing the work, manual effort may be faster because of this cost.

As codebases expand, we seem to encounter increasing issues in codebases where the programmer role has been delegated to an agent with no theory, which thus treats every new requirement as novel. This accretes multiple theories into the codebase, which eventually becomes incoherent for agents or people.

Will Agents Develop Theory?

Theory is our goal when building software, a theory that is shared with the machine, our colleagues, and everyone else.

Memory is the key problem for agents with theory, both forgetting the theory behind changes and the amount of context required to keep those decisions in mind when modifying existing code. It's possible that developments in LLMs over time will break down this problem, allowing agents to build and retain theory. An agent with persistent memory, access to all historical conversations and decisions, and the ability to track the codebase's evolution over the years could potentially hold a theory.

A sufficiently capable model reading a mature codebase can probably identify the patterns in the code. But theory isn't this pattern recognition — it's the understanding of why those patterns were chosen. That “why” lives largely outside the artifacts. It's in the conversations, the rejected PRs, the ADR that was never written, the whiteboard that was wiped. It's in the philosophy. (For example: It's the why of Brighter choosing to have a single-threaded message pump over using the thread pool).

The deeper issue is that theory is socially constructed and maintained. It exists in a team's shared understanding, updated continuously through conversation, pairing, review, and argument. An agent can participate in a single conversation but can't be a persistent member of that social process — at least not in any current architecture. Memory features help at the margins, but don't change this structurally. Even if the agent could acquire a theory, it would remain problematic for humans to collaborate if they cannot socialize that theory with the agent.

The question is whether an agent can be a persistent, socially-embedded participant in theory construction — and nothing about current trajectories suggests that's imminent, because it requires continuity of identity and stake in outcomes that agents don't have.

For now, then, the programmer role requires a person.

A Programming Episode with Brighter

Reviewing the ADR, the agent wrote, I realize we now have two approaches to validation. One uses our Specification<T> pattern, but the other creates a bespoke validation infrastructure. This seems ugly. If we are moving to the Specification<T> pattern, why do we still have a bespoke class to check our pipeline?

After reviewing the ADR, I realize the issue is reporting failures. Our Specification<T> pattern will give you pass/fail, but won't tell you which rule failed and why. It also explains an earlier issue in which the review noted that we were throwing an error from a Specification<T> when we reached a failure case that should not occur. We need to implement the Visitor pattern in our specification to collate all the errors.

I explain this change to the agent, noting that using the Visitor pattern will allow us to accumulate error messages in the graph, which can be retrieved if the combined specification evaluates to false. I tell the agent to ask me questions so that we can have a dialogue about this design.

The agent tries to overcomplicate this at first. When it answers, it can't figure out how to make this work. At a guess, the model doesn't recognize this approach. It can't initially see how to translate the multiple rule failures from its bespoke code, so it comes up with a ForEachSpecification<T>. I have to walk the agent back to basics on this approach, explaining that each Specification<T> is a rule; a rule may result in multiple breaches, to be recorded in an IEnumerable<ValidationResult>. I then explain that if there are any results, the predicate fails. The visitor pattern is then used to retrieve the results. I suspect that I assumed the agent understood this pattern combination, but it does not.

The agent now understands and presents a summary. The summary matches my expectations, so I ask it to go ahead and change the ADR.

The code looks simpler. I spot some failures to extract methods, leaving a complex implementation. Although this is just an ADR, I will have the agent change it now.

Overall, this conversational process is faster than writing this out by hand. We are getting closer iteratively.

But I also appreciate that I had simply accepted this, without properly reviewing, I would have had something less maintainable, because there were no clear patterns flowing through the code, but different ideas lashed together. I have worked on codebases where new insights did not lead to refactoring, leaving the codebase feeling like a lesson in decision archaeology. That was what would have happened here, without effective review. Because I took on the agent's roles as a researcher and a programmer, we iterate toward a cleaner design.

I feel comfortable spending more time here than I did before. The rush to get to writing code is gone. The agent can handle that quickly. Instead, I iterate on the design, confident that the agent can deliver it quickly.

New Developers, LLMs, and Theory

Why are senior developers more productive with coding agents?

  • They already have the theory, and they can explain it to the agent.
  • Once you have the theory, it is easy to produce new coding specifications.
  • Once you have a coding specification, the agent can write the code far faster than humans could.

A senior developer can fulfil the role of a programmer.

By contrast, a new developer (experienced or not) lacks the necessary theory and will make mistakes when specifying a program for an agent. Someone with the theory must thus review the new developer's code (when using an agent), since the new developer does not yet have it.

A new developer has to learn the role of a programmer on this team, as they don't have a grasp of the program's theory. How do you onboard someone onto a team? Through ADRs, documentation, code reviews, conversation, and social inclusion. An acceptance that they will be slower.

  • A junior developer is still working on the skills required to build theory; they will be slower.
  • A new developer on the team will need to acquire the theory; they will be slower.

So, before we can be productive with agents, we have to learn the program's theory. We accept that they will go slow when they don't have the theory. And that remains the answer. The junior developer needs supervision from the senior developer. The new developer needs onboarding. Only once they grasp the theory can they write new specifications without supervision.

Even the senior developer will need to slow down at times. Novel changes will require even senior developers to build a theory; they will be slower. You can be fast when you have the theory, and the change is “boilerplate” for you. You must be slower in novel situations that require theory building. Indeed, where the code is novel, pair with the agent to review each test and each implementation. Your goal here is less “correctness” of syntax, the provenance of the coder, but acquiring the theory, the provenance of the programmer.

The question you need to repeatedly ask now is: Do I have the theory here? Only then, go fast. How do you know if you have the theory? Can you explain it to someone else?

For a junior, that means the same way as before — pairing, reviews, elaboration.

Does Stack Matter?

One simplistic idea is that the stack you use does not matter. Now that you can use an agent to write the code, what does it matter what stack you write it in? The problem comes down to whether you can build the theory of the program if you don't understand the stack. In some situations, you may be able to understand the theory without knowing the language, but in others, the nuances of how we use language features to represent program theory may be important. Typically, as complexity grows, so does the likelihood that the stack – not just the language but the frameworks and libraries we use – is important to the theory, and as such, we have to understand it.

In cases where we need to understand the stack to build the theory, this would create unmaintainable code in which the programmer does not understand the stack they are working with.

The Need for Speed

“There's a lot of noise around how “Ai generates buggy software”. The truth is writing the code was always 20% of the time. Testing and refining was always 80%. But now as that 20% compresses 10x, we want to compress the 80% as well... That's the mistake.” Balint Orosz, X.

The pressure with AI is to take the productivity benefits of eliminating the coder role and apply them to removing the other roles: analyst, designer, and programmer. It's tempting, surely you just need to configure an agent to perform that role, and you will go even faster.

The problem is that whilst agents can take on the coder role, particularly when we iterate towards a solution, their tirelessness enables us to iterate towards quality code. But the programmer role, building the theory of the software, using discernment and judgement, remains the purview of the “human in the loop,” not an agent.

“All code must be human reviewed, and you should not send a PR for review unless you can personally vouch for it. That means pre-reviewing the code your agents generate before it gets reviewed by a colleague in an actual PR review... but coding was only ever half the job to begin with. Now we have more time for the other half (which largely consists of figuring out *what* to even build)” Nick, @Theurgistic, X.

We have the opportunity to improve the quality of the code produced by using agents and to achieve genuine productivity gains, but we can remove only one role here.

“When it comes to AI agents / AI tooling + coding, I hear an awful lot of talk about:

Efficiency

Iteration speed / PR output rate / lines of codes produced

I hear zero mentions about:

Quality

Customer obsession

This will bite back, and it probably already is...” Gergely Orosz, X

Return of the Programmer

Armed with the understanding that we have merged two roles in our thinking for years, how does this help us think about the future for software engineers?

In the era of agents writing code, this distinction between *programming* and *coding* becomes important. With coding agents, the agent is fulfilling the role of the *coder* at the levels of accuracy that mean it is rapidly taking over that task. But human software engineers continue to own the task of *programming*. The two have been merged in recent years, but we need to recognize their differences to make better decisions about who owns each task. The important role for the *programmer* is building the theory of the program. Without theory, we risk cognitive debt and unmaintainable software.

Read more...

tl;dr

Brooks' hypothesis in “The Mythical Man Month” is that the nature of software development means that no further “order of magnitude” improvements to productivity remain to be found has proven remarkably solid despite forty years of software history.

Claims made that LLMs will cause significant changes to software development can be tested against Brooks' argument.

Like previous attempts to find a silver bullet such as 4GLs, it does not seem likely that LLMs as a software authoring tool will be succeed. In particular, like 4GLs they suffer from the problem that because software spends most of its life in maintenance, the cost of change makes most improvements to the cost to author irrelevant; in fact, some rapid authoring techniques make maintenance harder and increase the lifetime cost of software.

They can and will add value as an “expert assistant” but this cannot be an order of magnitude improvement due to the fact that it cannot impact the essential complexity only the accidental complexity where no order of magnitude improvement remains to be obtained.

As such whilst AI tools may provide valuable additions to IDEs or other tools used for software development, they will not replace software developers, or the need for high-level languages.

Introduction

When it comes to LLMs as a developer tool or replacement of software developers, popular opinion seem predicts one of three main outcomes to the current boom:

  • AI Developers: Under this model, software developers no longer need to write code. Instead that task is now done by an AI coder. Subject matter experts interact with an AI that does all the work of writing the code.
  • AI Assistant: Under this model, software developers get assistance from common or repetitive tasks from an AI assistant. This increases their productivity; by how much becomes the concern. Does this reduce developer headcount?
  • AI Winter: Under this scenario, the LLM revolution fails to deliver the advertised gains, and may even lower productivity because in the longer run it gets in the way as companies who outsourced their code to an AI are forced to do so much remedial work it would have been less expensive to hire a software engineer write high quality code.

For anyone considering a career in software development the difference between the outcomes is stark. In one the the job of software engineer would seem to disappear for most people, automated into obsolescence like the weavers; in the second the job may change, but the number of people the industry employs declines making it particularly difficult to obtain entry-level roles; in the third wider AI research is set back by the LLM hype cycle's collapse.

The agenda here is the productivity of software development; the hope is that AI will make software development cheaper. For AI maximalists software engineers will no longer be needed; even for the minimalists less of us will be needed for a given task. The optimists spin this as more software engineers available for work; the pessimists as more engineers out of work.

Predicting a future is fraught; a lot of people don't like to make predictions but adopt a “wait and see” attitude. This is particularly true when we begin to talk about a fast moving field like AI. And it is worth pointing out that I am neither involved in frontier AI research, nor do I have any formal background in AI.

So why contribute to the debate?

The productivity of software engineers is not a new problem. For years businesses have been looking for the “silver bullet” that will deliver radical changes in software productivity. The “software crisis” was the term coined at the NATO Software Engineering Conference in 1968 to describe the difficulties in writing software: cost overruns, loss of money and fatal accidents. Whilst we have made many strides in how we do software engineering since that time, history suggests that progress is incremental and not revolutionary.

Software developers are subject to the law of supply and demand. As the Information Age has created an insatiable demand for software engineers so the cost of software engineers has risen. It the costs rise enough it becomes economically attractive to invest in increasing supply or productivity (this is a part of the software crisis).

  • Retraining people as software engineers (once non computer-science graduates into software engineering; more recently boot camps).
  • Looking for silver bullets, improvements to productivity through increased language abstraction, OOP, FP, frameworks etc.
  • Creating low code tools to make Subject Matter Experts (SMEs) software engineers.
  • Outsourcing to increase the supply via cheaper, foreign labour.

AI is just another attempt to fix the problem.

Not Talking About LLMs as a Component

This post is not about the usage of an LLM as a component in your system to solve a particular problem such as a customer chat bot; it just focuses on how LLMs are likely to impact software developer employment.

A valid criticism is that this post does not consider the possibility of LLMs solving the essential complexity of some problems and the impact of that on software developer employment.

The likelihood is that this impacts specific sectors where LLMs are a good fit for specific challenges over others. That discussion is outside the scope of this post.

What is Software Engineering?

The primary activity for software engineers is the creation of computable models. Presented with a set of functional requirements, we figure out how to turn that into something a computer can do.

This involves two steps: modeling the problem in a computable fashion and turning the outcome of that, the design, into code – instructions the computer can understand. Both of these steps are significant, though it doesn't really matter for this discussion how we define the modeling process: use cases and UML diagrams; CRC Cards; boxes and lines on a whiteboard; or a TDD test list or other code-based exploration of the model.

A key problem here is that productivity improvements typically address only the second step, turning a design into code, and ignore the first step creating the model.

Ironically, this misconception may have been exacerbated by agile software development techniques that tend to prefer exploratory modelling with code, fooling people into believing that modelling was not happening, just turning designs into code. This is not true; agile engineering practices just seek to use code for both modelling and instructions for the computer (and are themselves a reaction to CASE tools, see below).

No Silver Bullet

In a 1986 Fred Brooks wrote a paper “No Silver Bullet” that suggested that further improvements on developer productivity would be hard won. Indeed Brooks offered that there would be no order of magnitude increase within the decade. Brooks identified that this was because there were two facets to the task of software engineering and only one could benefit from productivity improvements:

  • Essential Complexity: the work involved in modelling a solution to a problem
  • Accidental Complexity: what is required to turn that model into computer code (in essence writing code)

Brooks' argument is that productivity gains can only come from reducing accidental complexity. The essential complexity of a problem is irreducible:

“I believe the hard part of building software to be the specification, design, and testing of this conceptual construct, not the labor of representing it and testing the fidelity of the representation.”

“All of the technological attacks on the accidents of the software process are fundamentally limited by the productivity equation:

Time of task = ∑i (Frequency)i x (Time)i

If, as I believe, the conceptual components of the task are now taking most of the time, then no amount of activity on the task components that are merely the expression of the concepts can give large productivity gains.”

Brooks, Frederick P.. The Mythical Man-Month: Essays on Software Engineering, Anniversary Edition (2nd Edition)

Brooks proposed that unless we assume that we have yet to solve 9/10 of the problems from the accidental complexity of software engineering, no innovation in software engineering can produce an order of magnitude productivity jump. For Brooks, we have already crossed the point where we have made the really big gains in productivity. Improvements may chip away at the accidental complexity but not enough complexity remains to radically shape up how we write code.

The reality is that forty years since Brooks wrote his paper most of the tools of programming have changed little since Brooks wrote his paper in 1986. There have been incremental improvements: OOP, frameworks and libraries, package managers, agile engineering practices etc. but no order of magnitude increase. This could be considered proof of Brook's hypothesis. Any hype by the LLM maximalists surely has to overcome Brooks' argument.

Generations of Languages

Most of our efforts to improve productivity have focused turning the design into code.

We first programmed computers by flipping switches; over successive generations of languages we have moved the level of abstraction up from being physical inputs into the machine to something that can be typed and understood by a human. We call each successive raising up of the level of abstraction from the machine a language generation.

The generations can be understood as follows: each higher generation could be implemented by generating code of the lower generation. This is useful as it allows the existing tools from that generation to continue to work. We can thus progress incrementally. So a 3GL compiles to a 2GL which is assembled into to a 1GL which directly instructs the machine.

More formally:

  • First Generation: Machine Code, binary input of codes understood by the chip, originally by operating switches on the machine:
  • Second Generation: Assembler, human readable macros that can be assembled into machine code:
  • Third Generation: High-Level, contain control structures, variables etc. Originally a compiler turned these into assembly which was then turned into machine code. A modern compiler turns statements into object code; an interpreter turns statements into byte code which it then executes. High Level Languages include C, C++, C#, Go, Java, Pascal, Rust, Zig and many other languages used today.
  • Fourth Generation: Declarative not Imperative statements. Naively the 4GL generates a 3GL output that is then compiled, but just as 3GLs don't strictly work this way any more, not all 4GLs generate 3GL statements but instead directly create object or byte code. 4GLs include: Visual Design Tools (Visual Basic, Access, PowerBuilder, SQLWindows); Domain Specific Languages (HTML, WPF, HCL); Data DSLs (SQL).
  • Fifth Generation: It's questionable if a 5GL has ever really existed, in the sense of generations; the term was co-opted for AI programming languages in the 90s, such as Prolog, but they are not really raising the abstraction to be a true 'generation'. We will ignore them for the purpose of this discussion as they have had little impact outside academia.

The Triumph of 3GLs

Today, most of us use 3GLs. We talk about compiling (or interpreting) our code, and we rarely need to deal directly with assembly or byte code. This has proved to be a productive level of abstraction. (I entered the industry at the point where inline assembler was still enough of a performance hack that you had to know a little bit of assembler even if you were using C or C++).

Alongside that, we use 4GLs in specific scenarios where they have proven useful. Some of the most common examples that spring to mind are:

  • SQL to work with data
  • HTML, CSS to work with web documents
  • HCL (Terraform) to declare infrastructure

In addition: many developers use visual designers to generate code for desktop and mobile UIs.

Although 4GLs have thrived in these niches, the general purpose programming language, the 3GL, has remained the workhorse of our computing abstractions. 3GLs have increasingly extended their syntax to support a mix of imperative and declarative statements (for example LINQ in C#), and generate code from these 4GL statements.

Why Did 4GLs Only Excel in Niches?

Perhaps the domination of 3GLs is surprising. If a 4GL can raise the level of abstraction it should make us more productive, why have 4GLs remained in niches? Why have we not shifted up a whole abstraction layer. There have been many historical attempts to do just that. In the mid-90s it seemed that 4GLs might dominate software development. It would not have been unusual to find most of the code in an enterprise being written in Visual Basic or PowerBuilder. What happened?

The problem is a classic trade-off: what many innovations in the space took from the accidental complexity of authoring software, they added into the accidental complexity of owning that software.

This is a problem we repeatedly forget, and relearn. Any successful software will be changed as the needs of its users change. Most of the lifetime of any successful piece of software will be in the ownership phase. During ownership our costs will be the cost of change. So to reduce overall costs, we need to reduce the cost to change our software.

“The first term of what I’ve dubbed “Constantine’s Equivalence,” then, is that the cost of software is approximately equal to the cost of changing it.”

Beck, Kent. Tidy First?

Whilst improvements in the productivity of authorship initially prove attractive, and gain traction, because the cycle has not reached the point of maintenance, the problem of ownership inevitably shows up as we have to live with the costs of what we authored.

Optimizing for authorship is usually a poor trade-off in the long-run as we spend more time on maintenance and modification that on creation. (In some scenarios software is highly-disposable, and in those contexts low authorship cost may be an advantage, but for most software if it becomes successful it will spend much long being owned than authored, so that is where the cost lies.)

The problem for many tools in the 4GL era is that whilst they made software easier to author, they made it harder to maintain.

CASE Tools

in the 1990s the large players looking at 4GLs backed Computer Aided Software Engineering (CASE) tools. CASE promised to take use visual designs tools and generate software. CASE tools include what we now call Low Code environments and Model Driven Engineering; these are just current iterations of these tools.

CASE tools included both workbenches and environments.

Workbenches

Their goal was to enable experts to design software without software engineers. Tools like DataEase, MS Access or Hypercard offered simple Line of Business application development where all that was needed was a form editor and a database.

The popularity of many of these tools led to the growth of “shadow IT” where business departments developed their own software, outside of the traditional IT department. Given the backlogs faced by IT departments, this seemed a solution by decentralizing the provision of IT systems.

The problem became that the departments simply created new software developers, albeit ones skilled with Workbenches. Any tool requires training and experience to use; it doesn't come as part of the skill set of being a subject matter expert. Workbenches shifted many subject matter experts into software developer roles.

At first, this was empowering, because decentralized IT empowered end users to solve their own problems, often avoiding long backlogs, are cost. But such departments then hit all the problems of ownership. When individuals left, departments had no backfill or found it difficult to transfer software not written with the idea that others might maintain it. In many cases they were forced to turn to IT for further maintenance.

IT had engineers skilled in 3GLs; these engineers then rewrote these 4GL workbench applications in 3GLs. Overall the cost was higher than having IT written it in the first place.

In addition, compliance and risk concerns emerged as the management of data spread throughout the organization, which required the application of software engineering discipline but was now uncontrolled.

Some organizations even banned end users from writing their own software due to the costs of this “shadow IT” hangover.

(Arguably a spreadsheet is also 4GL Workbench, although it is not usually bucketed alongside other Low Code tools.)

In addition these tools worked well for a range of built-in scenarios, but outside of those scenarios could not work at all. They could not be extended. When these changes were needed, the business had to reach out to IT. Again, to meet these new needs IT often had to rewrite the software created by these workbenches.

As a remedy, workbenches began to transform by adding programming in a high-level language, effectively becoming developer tools.

The developer versions of these workbenches included tools like Visual Foxpro, Visual Basic and PowerBuilder; these developer versions could be extended more readily by a 3GL. They were a compromise, trying to take the benefits of forms based programming in reducing the cost of writing UIs and marry it to the ability to extend with 3GLs. They were often be used to replace tools written by a “shadow IT” workbench. These were successful in the 90s but developers found that their limitations, particularly their orientation around form-based programming, were difficult to escape, particularly to capitalize on changes in technology.

Because these tools could only be used to write form-based software, they were not suitable for other applications. Their skill sets were seen as less flexible than those of true 3GLs, and thus less valuable, often resulting in developers looking down on those using these tools. In turn that made these jobs less desirable.

These issues came to a head with the rise of the web. These workbenches focused on rich-client development. With the move to the web and their implicitly distributed nature these tools were left behind. Developers working in these skill sets needed a path out and onto the web if they were to remain relevant Whilst tools tried to catch up, the pace of change of the web outpaced the re-development of these tools.

Quickly frameworks replaced workbenches. Most of the benefits to developers in using a workbench tool could be provided by a framework. For example, MS created WebForms for .NET as a way to leverage the form-based programming model and help the workbench developers transition onto the web.

The problem was that whilst workbenches offered productivity they could not adapt well to change, either to changes to how the business wanted to work, or to changes in technology.

Of course, the complexities of the form-based, stateful abstraction of Web Forms over the stateless protocol of HTTP soon began to tell in maintenance. Web Forms was rapidly eclipsed by Domain Specific Language oriented frameworks like Ruby on Rails (see DSLs below).

Nonetheless Workbenches continue to exist today for use by end users and developers. For example Microsoft's Power Platform. For the purposes of this blog it is worth noting that few developers today fear that they will lose their jobs because end users can use a Workbench like Power Platform.

Environments

UML extended simple OO diagramming notations to allow them to become a 4GL. (As an aside a lot of people feel that extending UML in this way is what killed UML, and its predecessor notations with it as a useful OO diagram notation).

In this view of software engineering analysis becomes new key role: capturing use cases, developing analysis and design models in UML and generating much of the software (or at least scaffolding much of it to fill in some blanks). The reasoning does make some sense. If code is just the accidental complexity and modelling the essential complexity, why not focus our tooling on modelling, and then just generate the code. The implications, for example no longer caring about the high-level languages you generate the code in, seem to have value.

But the CASE revolution never happened. Why?

The problem turns out to be that this creates two models: the one in the analysis tooling and the one in the code. And they can become out-of-sync. UML could not capture all of the design; a graphical notation had many ambiguities and details that needed filling in via a programmer using a 3GL. This breaks the whole edifice of the environment. As soon as the programmer began to modify the generated model, the two became out-of-alignment. Further change became complex, because the tool had to account for change made by the programmer. This required round-tripping: model –> code and code –> model. But round-tripping never worked – because UML was not complete.

Once again the same problem surface for 4GLs used in this role: the cost of software is in ownership, and thus change, and not in authorship. Because these tools made software harder to change, they were more expensive in the long run.

If we are optimizing for readability, more code faster is not better.

As an aside, two movements can partially be seen as a reaction to the failure of environments, both of which focus on code as the primary artefact:

  • Agile: which favors working code over modelling tools
  • Domain Driven Design advocates may note its admonition to use one model, expressed in code, as a reaction to environment led approaches).

These emerge precisely because it proves to be easier to model in code than in a graphical notation.

Code Generation

Software developers have used code generation as technique for many, many years. Typically the goals are both productivity, by removing boilerplate or repetitive work, and heavy-lifting for junior developers by creating a “paint-by-numbers” framework for them.

These work best when they are “run and done”, with no round-tripping issue. Even so, they tend to have risks associated, such as the investments in these tools baking in older frameworks, language versions etc because they become expensive to maintain.

Attempts to do round-tripping with code generation often fail.

Maintenance tends to provide a particular problem as a result. If you find a defect in one application's generated code and it is likely that the defect has propagated to all codebases that used that version of the generator. If you want to add new features or enhancements, then this needs to be added to every codebase, or we have to cope with legacy feature sets in specific applications. This propagation of errors and misalignment on features is an expensive burden once code generation is used at scale.

In many cases code generation is replaced by frameworks. The advantage of a framework is that you can fix the issue once, create new features and simply redeploy, thus reducing the cost of ownership (which as we have stated is the cost worth reasoning about).

Finally, code generation often suffers from the problem that it does not build understanding. Because the team did not build the code up, or use publicly available, well-documented frameworks, the onboarding costs are high.

DSL Revolution

We mentioned earlier that Ruby on Rails created the shift toward the framework as the primary way to reduce the accidental complexity of writing web sites over the Workbenches of the 90s. Part of the success of Rails was due to Ruby's syntax making it possible to use many declarative constructs within the language; Rails created a Domain Specific Language (DSL) for web programming.

In 2010 Martin Fowler wrote a book called Domain Specific Languages; the era of Domain Specific Modeling beckoned. Inspired by developments in the Ruby community and the promise of Language Workbenches, it seemed that developers would write DSLs with Subject Matter Experts for given domains using Language Workbenches. Those DSLs would be used to craft software. No more writing your eCommerce site in HTML and a 3GL, you would write it in a DSL that generated the relevant code for you.

The aspirations of this movement reached further, envisioning a small number of developers and a large number of domain experts glueing together their work: a small group of developers create re-usable modules that can be configured via DSLs and chain them together to create products, the software factory.

But the DSL revolution never happened. We did not all start working with software factories. Instead, we kept using 3GLs.

What happened?

As we will now be familiar from other failures, DSLs suffered key issues:

  • Problem of Modifiability: when the domain changes you first need to change the DSL before you can express the new concepts. The DSL developers have specific expertise – language parsing and building tools via a language workbench – and work often queues for those developers, slowing throughput in maintenance.
  • Problem of Onboarding: everybody needs to learn your DSL. That creates a barrier to hiring/onboarding as opposed to a 3GL
  • Problems of Feature Set: you can do 80% easily but 20% not at all. This becomes particularly acute during technology change – the 4GL tools often have no provision for that technology change, and cannot be re-tooled as quickly as new libraries can extend a 3GL.
  • Problem of Tooling: Without tooling that allows debugging, tracing or inspection of DSL statements it becomes hard to maintain. DSLs tools often give way to 3GLs because of this.

Ultimately DSLs work best where the domain is stable and we build a tool chain to support them: UIs (HTML and CSS), data access (SQL) etc.

But even in areas like Infrastructure As Code (IaC), hard to debug, feature set etc makes it arguably easier to use something like Pulumi than Terraform. See also: AWS CDK, Aspire etc.

The dream of domain experts writing code over software engineers interpreting the requirement often fails because it tends to create write only software, even assuming domain experts are willing to become DSL programmers in the first place.

In addition, the goal of the business user writing software via a DSL was always a myth:

So is this the hook – business people write the rules themselves? In general I don't think so. It's a lot of work to make an environment that allows business people to write their own rules. You have to make a comfortable editing tool, debugging tools, testing tools, and so on. You get most of the benefit of business facing DSLs by doing enough to allow business people to be able to read the rules. They can then review them for accuracy, talk about them with the developers and draft changes for developers to implement properly. Getting DSLs to be business readable is far less effort than business writable, but yields most of the benefits. There are times where it's worth making the effort to make the DSLs business-writable, but it's a more advanced goal.”

Fowler, Martin, 2008 DSL Q&A. See also Business Readable DSL

Avoid Writing Software Instead?

4GLs also declined because we moved into the era of SaaS. Instead of most businesses building bespoke software, the target of much of the 4GL revolution, many businesses switched to using SaaS offerings via the web. This is because most businesses don't want to turn their subject matter experts into shadow IT, they want to provide them with tools to do their job, and SaaS provided a way to do that.

The demand for “shadow IT” reduces under this model as it is met mostly by SaaS tools. Compute for many common tasks becomes a utility, competing on price and reliability over feature set.

For the SaaS vendor the goal of empowering end users to write code over software developers fades away; there are few advantages and many disadvantages of shifting code into the problem space than remaining in the compute space.

The Arrival of Prompt Engineering

What can the past tell us about the likely trajectory of LLMs driven by Prompt Engineering and their impact on software development. To recap our key points so far:

  • Essential Complexity cannot be reduced; improvements to tooling just reduce Accidental Complexity
  • Reductions to Accidental Complexity must focus on Ownership of software and not the Authorship as software spends most of its life in maintenance, being changed.

Interestingly enough, Brooks considered the impact of AI on programming back in 1986 and divided applications into two buckets, Automatic Programming (AI Developers) and Expert Systems (AI Copilots). Both are attempts to find a silver bullet to the software crisis through AI.

(For now I am not going to consider the idea of autonomous agents writing software – an attempt to increase the supply of engineers through the use of AI workers. Nothing about LLMs indicates to me that they can act autonomously. They require humans to give them tasks – in other words to program them).

Automatic Programming

AI developers are a case Brooks calls Automatic Programming. Automatic Programming is an attempt to give a computer natural language instructions to author code.

“In short, automatic programming always has been a euphemism for programming with a higher-level language than was presently available to the programmer.”

Parnas, David 1985. Quoted by Brooks, Frederick P.. The Mythical Man-Month: Essays on Software Engineering, Anniversary Edition (2nd Edition)

Can an LLM instead be used to allow natural language to specify requirements in a given domain that can then be used to generate code? If so, does this lower accidental complexity?

Many proposals for AI replacing developers pitch themselves as a better language workbench – more of the app can be declaratively described in natural language and the code generated. Attempts at natural language programming (NLP) have often only allowed a constrained usage of language, but the promise of an LLM for automatic programming is that you can use Prompt Engineering techniques to write the code.

In this light, the LLM is being used as a 4GL as we are using it to generate 3GL statements. (We do have some the ability to generate 4GL statements too, I'm not sure that really makes an LLM a 5GL). Our 4GL will consist of Prompt Engineering statements. But rather than being truly declarative – describing what we want – Prompt Engineering uses triggering – pushing the LLM to trigger a response based on solutions to the problem it has been trained on.

The question is whether we can devise a toolchain or set of techniques for use with software written via Prompt Engineering statements that can solve the problems that we had with 4GLs to date?

  • Modifiability
  • Tools
  • Feature Set
  • Onboarding

Modifiability

We use Prompt Engineering to trigger the LLM into generating statements in an existing 3GL (or 4GL); by definition it can't do anything else as it must have existing statements to re-purpose.

This means that either:

  • Evaluation of that code can only be done by someone skilled in a 3GL (or 4GL)
  • The tooling needs to reach the point that we can maintain software written via Prompt Engineering without dropping down to t