What the Software Ought to Do. What It Ought to Be.

I replaced my personal finance spreadsheet with a terminal app, wrote none of the code, and used a stack I’d never touched. How well can an engineer supervise an LLM exclusively authoring code in a language and framework they don’t know?

I have a strong preference for upfront thinking. Not in the bureaucratic sense – please, no, I’m deeply allergic – but my curiosity needs to know my domain before I build. That means drawing a domain model, writing out a site map, or just staring out the window contemplating how the average user will interact with the system. Those of you reading who share a similar disposition will recognize the satisfaction of finally moving from “thought product” to “work product” and development unfolds with minimal friction – the pieces snap together like building blocks.

Brooks famously made the distinction between essential and accidental complexity [1]. Essential complexity lives in the software’s purpose, its problem domain and its surrounding environment: the rules, relationships and interfaces of the domain’s systems, social and technical, that exist independently of whatever solution you implement. Accidental complexity arises from the tooling and the intricacies of the implementation itself. Discovering the essential complexity is a human exploration and thought exercise. Expressing those thoughts in documentation, diagrams, or working software, creates tangible representations of that understanding. Inevitably, this work product is an imperfect representation of the thought product, with each medium introducing and shaping its own constraints and distortions – a key part of the system’s accidental complexity.

This is why no single diagram, document, or code snippet fully captures the essential complexity. As Brooks wrote about the invisibility of software, “As soon as we attempt to diagram software structure, we find it to constitute not one, but several, general directed graphs, superimposed one upon another.” [1] Switching between modeling modalities – domain models, wireframes, and documentation – keeps me clear-eyed about the unfolding work in the same way printing a document helps you catch errors you’d otherwise miss.

In this essay I show how AI is a natural fit for this domain-driven custom software workflow. AI does not eliminate the essential complexity – if it did, it wouldn’t be essential anymore. But it supports structured thinking by providing a responsive surface against which ideas can be expressed, challenged, and validated. With AI supporting the requirements and design, those artefacts simultaneously become the engineer’s written understanding of the essential complexity and a central part of the AI’s “harness” [2], [3] to guide its unfolding development. As we’ll see in the case study, I did not author any code directly. I was, however, constantly reviewing its output as if the LLM was my junior and I was a disgruntled tech lead.

Capturing the essential complexity allows you to define what the software ought to do – its purpose. AI is capable of generating working code to fulfill a purpose, but it doesn’t know what’s “good”: what’s understandable by a human, and what’s possible to evolve and be extended. The agent must also be directed toward what the software ought to be – its constitution. This goes beyond a style guide: it’s the architecture, structure, and the logic of the generated code. We establish the purpose by understanding our domain so that the software fulfills its intended function. We tend to the constitution with our knowledge of the craft and engineering principles. Both require the judgement to know what the system ought to do and ought to be.

About the Case Study

I’ve never encountered a personal finance application that fit just right. I’m not disciplined enough to regularly enter my transactions, and there are only a few metrics I’m interested in tracking. I created a spreadsheet, refined over the years, that works for me: the least amount of data entry to get to those metrics. I won’t get into the details of my methodology – I’m not here to sell you on my own idiosyncratic approach to personal finance. I will, however, use it as a case study for spec-driven development with AI.

Converting an artefact like a spreadsheet to an application is an exercise in separating the essence from the accident. The resulting application will naturally differ from the source spreadsheet; but the spreadsheet already captures the system’s purpose as a low-fidelity specification.

The first step is to understand what the spreadsheet is doing. I began by classifying the tabs into distinct modules and diagramming how they interact as depicted in Figure 1. The remainder of the development divides the application into those modules. At this point we haven’t used AI other than to assess clarity – we’re still putting in the effort to discover our domain.

The coloured boxes are modules; the white boxes are
application configuration; interactions between modules are captured
with solid lines; how modules reconcile against each other is covered
with dashed lines.
Figure 1: The coloured boxes are modules; the white boxes are application configuration; interactions between modules are captured with solid lines; how modules reconcile against each other is covered with dashed lines.

I decided to implement this case study in Python using Textual and SQLAlchemy: this is my first time working in that tech stack. I would normally develop my backends with Kotlin using Spring Boot and JPA/Hibernate, and user interfaces with TypeScript and React. But for the past fifteen years, entirely for nostalgic reasons, I’ve been searching for a project that can be implemented as a terminal user interface (TUI). Textual made Python the obvious choice.

This constraint also added a new dimension to the case study: how well can an engineer supervise an LLM exclusively authoring code in a language and framework they don’t know?

I have made the resulting specifications, source code and my prompt logs available on GitHub. There is a lot more detail and nuance available in there and I encourage anyone interested to check them out.

Earning the Mental Model

Prompts are a natural way to start a brain dump in a project like this. It’s a temporal interaction with a statistical model, so you can start off with a “vomit draft” without a shred of shame. If it comes back with something completely unexpected, you can start over. You can ramble, be disproportionately detailed in some areas, underdeveloped in others, and still end up with a refined understanding in response. The LLM is a natural bridge between the thought product and work product.

For example, here’s my first prompt to describe how I do personal finance complete with typos and all:

Let’s start brainstorming some requirements (and starting to fill in the technical charter where requirements impact that, although we’ll dedicate a whole task to that for the most important details).

With this project, we’ll be replacing my personal finance Excel spreadsheet with a Textual UI written in Python. Since it’s not a web app, our Layer 2 documentation will be operations in general instead of a formal web service API.

The project will be split into three modules:

  • Balance sheet
  • Goals
  • Cash flow and expenses

I don’t track each expense. I track the current state of my accounts and make sure the amount I’m retaining each month largely matches what I have entered in my cash flow. At the end of each month, I take a snapshot of my current net worth to assess this.

Balance Sheet

Assets

  • Current accounts (chequing, savings, WISE)
  • Receivables (expense reports from work, manual insurance claims)
  • Investment accounts:
    • Type: RRSP, TFSA, RESP, LIRA, DCPP, Unregistered
    • Constituents: Cash, equities, ETFs, mutual funds, GICs
  • House
  • Cars

Liabilities

  • Credit cards
  • Mortgage
  • HELOC

Notes

Ownership of each account can be Husband, Wife or Joint. Let’s assume that the list of owners is configurable.

The value of investment accounts should be calculatable by its constituents.

Goals

Goals are essentially buckets. Goals can claim a portion of the assets registered above. Usually current assets (bank accounts) are grouped together for this claim; i.e., it doesn’t matter if it’s in the chequing or savings account. Technically, each constituent in an investment can be assigned to different goals, but that doesn’t happen often. That’s something we may capture in the database so it’s forward-compatible, but simplify in the UI to start.

Some example goals: emergency fund, vacation fund, primary and secondary automobile replenishment fund, retirement, children’s post-secondary

Cash flow and expenses

This part is completely separate from the other two. Here, for each person captured (Husband and Wife), we gather:

Salary Gross pay
 -> Less group retirement RRSP contribution
 -> Less taxes and deductions
 --> Salary net pay
Matched retirement contributions

Bonus Gross pay
 -> Less taxes and deductions
 --> Bonus net pay

We then capture our expected monthly expenses in a separate interface in order to arrive at an average monthly expense total. Each expense can be classified as coming from current assets or a credit card, and can be classified as regular or irregular; these are used for reporting/planning purposes only at the moment.

These are then all tied together in the final cash flow:

-> Net monthly salary from Husband and Wife
--> Less monthly expenses
--> Less automated retirement contributions
--> Less automated auto replacement contributions
--> Less automated post-secondary REST contributions
-> Monthly retained
--> x12 = Annual retained
--> Plus boneses
-> Gross annual retained
--> Less large scale expenses (household improvements, major electronics and appliance)
-> Net annual retained
--> Plus all salary automated contributions and matchings (from the income configuration)
-> Total saved

The list of automated contributions should be configurable.

(When asking my chatbot to edit this article, it congratulated me on how this first prompt earned its “vomit draft” label. Thanks, Chat.)

We’re not expecting that this first prompt will match our final outcome. But even with all the prompt’s deficiencies, the model was able to produce a coherent set of functional and data requirements to start us off.

I didn’t realize that immediately, however. My first impression was that the generated specifications read as though someone too junior had written them – things looked too simple to be right. I responded how I would if a human wrote it: I printed them out, read them in isolation, and red-lined them. I took those comments back and sketched out my own wireframes and UML domain model by hand, using a plain-old Claude web chat to provide feedback.

Interestingly, I started overcomplicating things. I didn’t like that all accounts had a scalar balance attribute, but investment accounts re-defined that value to a “cash balance” in addition to having a one-to-many to a list of holdings (equities, mutual funds, etc.). Wouldn’t it be more consistent if everything were a holding – even the scalar balances? This navel gazing went on for a bit in my chat until I finally arrived back at a simpler, more pragmatic model.

A model that looked very similar to that first draft that I thought was too junior.

The lesson here is not that I’m an architecture astronaut brought back to earth by an unforgiving machine. The fact was that I couldn’t evaluate the quality of the LLM’s first draft because I didn’t fully understand the domain yet. The pencil sketches, the chats with Claude, and most importantly, the switching of modalities (domain models, wireframes, written requirements) were all a part of a journey of making the “invisible” software [1] real to me. This is the discovery of the domain’s essential complexity and the software’s purpose.

And notice how much hand wringing was required for a spreadsheet I authored supporting a process I created. As Brooks argued, this is the most irreducible part of writing software, and is critical for a positive outcome. AI was a great help – my understanding is more complete, and my specifications are better than they would have been had I done this alone. But in this discovery phase at least, it’s no silver bullet.

I encourage you to look at the functional requirements and UI specifications in the GitHub repository for more detail. In particular, the resulting domain model that fulfilled the architectural diagram from the previous section.

Harness Engineering with Spec-Driven Development

The widest definition of a harness in AI is “all that is fed into the LLM” – everything but the model itself (including existing code already in the repo) [2]. OpenAI summarizes the outcome of harness engineering as, “Humans steer. Agents execute.” [3] Spec-driven development (SDD) makes the project’s written specifications the centre of the harness. Böckeler defines three SDD approaches with the most common being spec-first: the written specifications are used for code generation and then archived once actioned [4].

For my process, specifications are more than procedural handholding. Specifications capture the purpose of the application – they are the material representation of the essential complexity. The spec-first approach is at odds with this. I started my investigation with GitHub’s spec-kit and although the first few tasks were productive, I found myself going through the motions by the third, generating Markdown that I never found valuable enough to read. The documentation was only a means on the way to the code – essentially an extended prompt. This meant that there was no authoritative source defining the purpose of the project itself.

Like any engineer in a new landscape, I created my own approach. It evolved with this case study, so it’s hardly a standalone framework for me to promote. However, I would like to highlight one departure from other frameworks: I explicitly separated canonical documents from working artefacts. Canonical specifications remain a single source of truth and are constantly kept up to date as the project progresses. They capture the current understanding of the application’s purpose. A documented “technical charter” also exists to establish code generation guardrails: the rules, conventions, and best practices for the generated code.

Working artefacts, on the other hand, are a log of temporal concerns: ADRs, iteration work logs, and deferred TODOs. They serve a purpose and are then meant to be archived, remaining in the repo only for reference and future LLM context. The working artefacts can be visualized as the “whiteboard” that’s used to arrive at a common understanding – in this case with the AI – and then erased once the concepts are decided [5].

Even though I only wrote specifications and prompts, this isn’t “vibe coding” (I’m using vibe coding here to mean development with prompts specifying functionality without direct engagement with the domain’s essential complexity or the generated code). Vibe coding assumes you’re already comfortable with the application’s purpose, or defers its discovery until later. It also only concerns itself with the functional outcome with little consideration for its constitution. As we’ll see, discovering essential complexity and iterating on the application’s purpose through development is non-ideal. Eschewing the constitution often isn’t a problem at first, but it resurfaces unhappily as complexity is layered on. However, this domain-driven approach also isn’t meant for all situations: the site you’re reading this on was entirely vibe coded. I am blissfully unaware of whether this Astro-based site is architecturally sound – and that’s OK for now.

For more detail, I encourage you to read the specs/README.md file in the repository. It also describes how I layered and cross-referenced my requirements – an approach I found to be quite effective.

Delivering the Solution

Purpose and constitution require completely different thought patterns. Purpose is a philosophical concern: there’s an art to discovery and understanding, and since you’re venturing into the unknown, it’s near impossible to sequence. The purpose is best settled upfront before there’s anything accidental to bias it. The application’s constitution, however, is judged against something tangible: you can only assess the constitution once there’s code to look at. It can – and should – be sequenced deliberately and delivered iteratively.

To capture this reality, I broke the process into two phases:

  1. Initial domain discovery

  2. Implementation

    1. Project setup

    2. Vertical slices

Phase 2 is subdivided because I didn’t jump straight into vertical slices (that is, iterations delivering working functionality covering all layers of the application). I first established the constitution’s “skeleton” – itself a part of the harness.

Phase 1: Initial Domain Discovery

This is the effort described in Earning the Mental Model: creating a consistent definition of the software’s purpose with the LLM’s support. I started my domain discovery by focusing solely on written specifications and diagrams. This isn’t Waterfall, though, as I wasn’t authoring a complete specification upfront. My objective was to have all the essential complexity discovered and the application’s purpose documented.

A gap in the domain model due to missed essential complexity often impacts multiple layers, interrupting the unfolding development. I only had one case of missed essential complexity in this case study: I under-explored the impacts of having a session-wide effective date. It was clear that monetary values needed to be stored with their effective date, but how other values – such as quantities of shares owned and account discard operations – ought to fit in on this timeline was not fleshed out. Addressing this upfront in Phase 1 would have saved me this loss in momentum.

Before moving to the second phase, I also captured the complete breadth of functional requirements, albeit superficially. Under-exploring essential complexity upfront creates disruptive detours later; however, under-exploring the accidental complexity in this phase is often an advantage. We haven’t yet engaged with the application’s constitution in this phase, so we don’t have the tactile feedback required to evaluate our accidental decisions. If you were to go into detail here, you’d be guessing. When working with AI, a confident guess in a written specification is worse than no specification at all.

A rule-of-thumb is whether the problem you’re considering is ontological: you will lose development momentum to stop and philosophize “What is an account?” once moving on from this phase. Ontological questions – those about what the software is – require a completely different thought process and their impacts are far reaching. These considerations are clearly essential complexity and are core to the application’s purpose – they should be dealt with upfront (conversely, if your project has no ontological questions to answer, then it may be a good candidate for skipping this phase or a lighter-weight process).

However, you’ll be unnecessarily delaying delivery and likely burdening yourself with over-specification if you’re defining upfront whether your entity identifiers should be serial integers or UUIDs. That is an accidental concern that can be deferred as you review the application’s constitution in the following phases.

Phase 2a: Project Setup

The project setup, itself an accidental concern, also benefits from starting broadly before going deep into deliverables. Patterns such as Walking Skeleton [6], where a barebones implementation of the application’s architecture is used to kick off the project, have existed for some time. When AI is writing all or most of the code, these kick-off strategies play a renewed role: establishing the conventions and architecture that the agent will imitate for the rest of the project. This skeleton becomes a reference implementation for all the vertical slices to come.

You could, in theory, jump right into your first vertical slice, but you’d be picking out architectural inconsistencies from the generated code like flies from soup. Establishing the project’s structure, architecture, and domain model first allows you to validate your own understanding – as patterns like Walking Skeleton initially set out to accomplish – as well as to start your project with a reference for the agent to emulate for all subsequent work.

As we’ll see, AI – although largely obedient – repeatedly ignored some written specifications. Having the structure of the project both set an example and architecturally constrain the generated code has been more effective.

Phase 2b: Vertical Slices

I opened each vertical slice by asking the agent to review its requirements and search for inconsistencies. It would always find a couple of questions I hadn’t considered or gaps that I hadn’t identified yet. Except for the “effective date” miss discussed above, what it found was intentionally deferred accidental complexity.

Architectural surprises were expected in this phase. I was constantly evaluating the generated code’s architecture, looking for repetition, and ensuring that the logic was legible. For example, I noticed that UI-specific logic and DTOs were starting to become intertwined with business logic in the service layer. I worked through an ADR with the agent to split the service layer into sub-layers: “core” and “domain” services. Investing in these concerns pays dividends – as it always has, even before AI – in subsequent iterations.

I also needed to consider the architecture of the harness itself. At the time of writing, you cannot trust that just because something is written in a specification the agent will listen to it. I prefer that pressing Enter tabs from one field to the next. It was specified from the very beginning and yet it was skipped all the way to the very last slice (Cash Flow). To address this (among other inconsistencies), I interrupted the Cash Flow slice to complete a “Workspace Cleanup.” Since at that point I had a stable inventory of all possible user input controls, I standardized and centralized them. This would have already been a best practice pre-AI, but here it takes on an added dimension of moving a rule out of the written harness and into the application’s architecture. Some constraints are too important to remain instructions; they need to become a part of the constitution.

This phase is where you become a disgruntled tech lead. Mistakes that keep happening and code that keeps getting repeated should offend you. Instead of teaching a human, you need to refine your harness – both the written specifications and the constitution of the generated code. As we’ve seen, generated code will diverge from your expectations. This divergence is itself a diagnostic. Often, it’s not until you can see and interact with a system that you are able to judge its purpose and constitution – the “invisible” becomes visible.

Further practical advice from this phase. Some of it is model-specific (Sonnet 4.6 with occasional escalations to Opus), so it may age quickly.

  • You should avoid interrupting the current iteration’s flow. As you’re directing the AI with prompts, keep a connection between the requests. If you notice something awry unrelated to the current work, then take note of it and return to it strategically. Even as I’m writing this, I still have Notepad open with my informal backlog of fixes to complete.
  • The LLM – ever helpful – will often try to do more than you ask. I’ve found it important to not accept changes that aren’t explicitly related to what was asked. It increases cognitive load, and it pollutes the context window as you continue to direct it. If the change was good, I’ve asked it to document it in a TODO file (I dedicated a directory to this) so we can return to it later.
  • By the end of the project, before moving on from the iteration’s opening context window where I asked the agent to review the upcoming scope, I started asking it to author all the prompts I will need for the remainder of the slice. Anecdotally, I found this to be very effective. You can see some of the prompts it generated for me here. Each of these was able to run in a brand-new context window and required minimal refinement after. I will continue to use this approach on projects going forward.

How it Unfolded in Practice

Table 1 shows the commit-by-commit progress by phase and iteration. I make a best effort to break down the number of lines of specifications between essential and accidental complexity: the “Requirements Layer” of the canonical specifications is used as a proxy for the essential complexity, and the accidental complexity is captured by every other canonical specification including user interface flow documentation, service layer descriptions, and the technical charter. Note that this analysis only includes the effort to arrive at the “feature complete” milestone – my ongoing maintenance since June 16 is not in scope.

Phase

Date Range

Working Days

Iteration

Commits

Net “Essential” Spec LOC

Net “Accidental” Spec LOC

Net Source LOC

1 – Discovery

April 29 – May 15

8

N/A

18

1,295

1,620

2a – Setup

May 15 – May 27

9

Project and Domain Model Setup

7

46

38

962

Balance Sheet Service Layer

5

5

6

356

TUI Bootstrap & Dashboard Screen

4

162

1,159

2b – Vertical Slices

May 28 – June 16

13

Balance Sheet

7

71

11

1,484

Investment Editor

9

8

59

2,359

Goals

9

38

2,910

Goal Allocation

3

2

15

1,152

Cash Flow

5

3

38

3,415

Workspace Cleanup (interrupted Cash Flow)

7

110

330

Total

April 29 – June 16

30

74

1,430

2,097

14,127

Table 1: Breakdown of the commits to deliver the application. Commits exclusively related to authoring the scaffolding for the custom SDD approach have been excluded. Working days are not complete working days – they’re just the number of unique days engaged with the project in spare time, so overall, slightly more than once every other day.

Phase 1 is entirely specifications: 18 commits over eight “working days” before a line of source code. Over 90% of the documented essential complexity was captured in this phase. The only other double-digit additions to essential specs came in the two iterations where I addressed that miss: generalizing Money into EffectiveAmount in Project and Domain Model Setup, and putting discard operations for accounts, goals and expenses on a timeline in Balance Sheet.

The accidental complexity was more spread out but still front-loaded with 77% completed in Phase 1. This upfront work included the first passes at the technical charter and the user interface flows. Project setup in Phase 2a was uneven: the Balance Sheet Service Layer focused on the code itself so there was barely any spec work there; while the TUI Bootstrap resulted in a significant update to the user interface principles – this is the deferral of UI specification discussed earlier. The complete vertical slices in Phase 2b were more consistent with each iteration refining the user interface flow specifications and adding to the technical charter, particularly in scoped README files in the src directory.

Figure 2 depicts the outcome of iterating upon the application’s constitution throughout delivery. Source churn can be interpreted as the amount of refactoring required, either from missed essential complexity or from a refinement to the application’s constitution.

Source churn – the ratio of source lines removed to the total
number of lines added and removed – declining across vertical slices.
Setup and Cleanup Iterations are naturally more variable.
Figure 2: Source churn – the ratio of source lines removed to the total number of lines added and removed – declining across vertical slices. Setup and Cleanup Iterations are naturally more variable.

Cash Flow, our last vertical slice, resulted in the most lines of code produced of any iteration (3,415) with only a negligible amount of source churn (0.002). But that figure needs to be read in the context of the Workspace Cleanup slice that interrupted it. Cash Flow was interrupted after the generated responses to its first prompts were producing exactly the whack-a-mole bug churn that signals an architectural problem rather than a coding one. The cleanup that followed had a source churn of 0.39, the highest of any iteration, as the input components were standardized and centralized across every module built to that point. The LLM didn’t naturally write reusable code, so the reuse the final slice benefitted from was the result of investing in the constitution a slice earlier.

The Unfamiliar Stack

Unfortunately, I don’t have a friend well-versed in Python whom I could treat to the pleasure of critiquing my solution. So, I asked Opus to step up instead. I started a blank context window and asked it to assess the quality of the project with the following prompt:

What’s good? What’s bad? What’s going to be a problem later? Pay attention to the fact that this is my first Python application. All my previous three-layer applications were on the JVM, so please check for a ‘Java accent’ in my Python code too.

It found three notable observations with the first and most profound being transaction handling. On the JVM with JPA and Spring, I never had to think about transaction boundaries. @Transactional marked them on service method, and a failed query rolled back a persistence context that was discarded at the end of the request anyway. In SQLAlchemy, session scope is yours to define.

In my case study, the LLM created a single SQLAlchemy Session for the entire desktop application. The core service methods would periodically commit, but there was no exception handling. That means that if any single query failed, the entire session would continue failing until the application was restarted. The agent introduced this bug, but my lack of SQLAlchemy experience, and the conventions I was holding onto from the JVM, left me unable to catch it.

Next was my penchant for creating a DTO-per-screen and dogmatically insisting that Textual components only ever communicate with “application (UI) services” and never core services directly. According to Opus, this architectural weight is “JVM muscle memory” and a native Python developer would likely let the UI work more directly with the core services and domain model. Here it’s hard to know how much of this is a personality quirk that happens to align with Java thinking (I do love explicitly defined responsibilities) versus Java thinking that has created a personality quirk. Not nearly as critical, but a discernible shibboleth.

Finally, in one of my first commits where I was putting my technical charter document together, I asked the agent to define some reasonable rules for a Python TUI project to follow. One of the rules it added to the document was:

All user-provided data must be validated using Pydantic models or similar schema-based validation.

Non-negotiable: No raw dictionaries or unvalidated tuples shall be passed between modules. Validation errors must produce user-friendly messages.

Incredibly, I made it all the way to having a feature-complete project without one usage of Pydantic. I let the AI do its thing, and like Homer Simpson after putting his faith in a drinking bird, I also found myself disappointed and incredulous. This is a very similar mechanism to the “Enter-to-tab” scenario I described earlier, except here the root cause is a combination of me superficially authoring my technical charter (a real risk in SDD) and being unfamiliar with my stack.

The lesson is clear: to retain ownership over the result, you can’t delegate the knowledge of your craft to the LLM. Just as I couldn’t evaluate the quality of the documented essential complexity until I had complete understanding of the domain, I also couldn’t effectively guide the constitution without knowledge of my stack and tools. Effectively directing the constitution of an agent’s generated code requires knowledge of the technologies employed – or at least the humility and curiosity to ask and learn.

That being said, there are two very easy ways to address this. First, if there is something you’re unfamiliar with, ask the AI to teach it to you. It’s very good at that. Second, leverage the AI to provide constant critical feedback, just like I did here. This case study was only me working alone so I didn’t make pull requests for each slice. In practice, AI-supported pull request reviews are already a common practice and would have defended the project against these pitfalls. Cleaning up the repo before publishing, I worked with Claude to resolve this too which you can find in this ADR.

A Tour

The slideshow below will walk you through the outcome. As you slide through the images, you’ll notice the consistency in behaviour and the reuse of standard components; particularly with the input components and the left/right-hand side split in Goal Allocation and Cash Flow screens. The behavioural consistency is the result of the TUI principles; the reuse of standard components is the result of the investment into the constitution throughout the vertical slices.

You can follow the instructions in the README to run the application yourself too.

Conclusions and Implications

Early in the process, I couldn’t evaluate the purpose of the application without first understanding my domain. Later, I couldn’t evaluate the constitution of the application without knowledge of the technology. Understanding and knowledge are the drivers of this approach. They’re the thought product that sits concealed in the engineer’s mind. But they’re required to exercise the judgement to leverage agents toward a productive and sustainable end.

If understanding and knowledge drive the process, then your blind spots will send you off course. You’re writing the harness, so the generated outcome will be biased toward what you value. Its blind spots will mirror your blind spots.

Comparing AI to a junior developer is common, but at a certain point, it becomes more of a mirror of yourself. I unfairly judged AI’s first draft of my functional requirements because I didn’t understand my domain well enough. I let the constitution slide when I brought my transaction handling biases over from JPA and couldn’t effectively direct the LLM toward a better outcome.

With that in mind, how well can an engineer supervise an LLM exclusively authoring code in a language and framework they don’t know? Well enough, but it’s not free. Had I not asked for a code review, I would have shipped a bug I had no way of catching on my own. Granted, the cost in this case study was modest: Python is about as close to my home stack as an unfamiliar language gets. Erlang or Clojure would have been a much steeper bill.

It’s unwise to draw general conclusions from a single case study. But in the end, the agent delivered a working solution. I know it, I influenced it, and I own it, but I did not directly author it. Implementing this without AI would have been prohibitive for a side project, and evolving it more prohibitive still: the equivalent of adding a new column in Excel would have been a notable manual effort. Now, with the constitution and harness in order, it’s often a single prompt.

This process is slower than the vibe coding we discussed earlier (recall that I spent over two weeks of my spare time just iterating on written specifications). Engagement with the application’s purpose and constitution is insurance against the essential complexity resurfacing unhappily or its constitution making it too expensive to evolve. Whether that insurance is valuable is – like most decisions in software engineering – a judgement call. This approach worked for a financial application with a rich domain model. But it would have been overkill for the blog you’re reading this on: the blog’s purpose was clear before the first prompt, and its complexity only required a superficial engagement with its constitution.

AI knows what’s correct, but not what’s good. It knows how to implement a correct three-layer Textual application, but it doesn’t have the judgement to know if it’s done well. As models advance, they will statistically model intention better, thereby moving the line between what can be vibe coded and what requires this level of involvement with the project’s essence. However, someone needs to understand the domain well enough to judge what the software ought to do. And know the craft well enough to judge what it ought to be. Delegate the first and you get working software that’s ineffective; delegate the second and you have software that only works until it needs to change. Both situations sit invisible under a working system, making engineering judgement more critical than ever.

References

[1] F. P. Brooks Jr., “No silver bullet: Essence and accidents of software engineering,” Computer, vol. 20, no. 4, pp. 10–19, 1987.

[2] B. Böckeler, “Harness engineering for coding agent users.” martinfowler.com, Apr. 2026. Available: https://martinfowler.com/articles/harness-engineering.html

[3] OpenAI, “Harness engineering: Leveraging Codex in an agent-first world.” OpenAI Blog. Available: https://openai.com/index/harness-engineering/

[4] B. Böckeler, “Understanding spec-driven-development: Kiro, spec-kit, and Tessl.” martinfowler.com, Oct. 2025. Available: https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html

[5] R. Garg, “Design-first collaboration.” martinfowler.com, Mar. 2026. Available: https://martinfowler.com/articles/reduce-friction-ai/design-first-collaboration.html

[6] C2 Wiki, “Walking skeleton.” Portland Pattern Repository. Available: https://wiki.c2.com/?WalkingSkeleton