I'm going to put a heating controller in my house, and I won't write any of it, neither the code nor the tests. An agent builds it from an empty repository and one spec. Every time anything about the process changes, it builds the whole thing again from scratch.
Why do this in the open? Many people say they've built software with fully agentic systems, but very few show how they did it: which tools, what checked the result, what broke along the way. Without that, a claim can't be repeated or tested, and there isn't much to learn from it. So I'm showing mine: the spec, the harness, the reason for every change to it, and the result of every expectation on every build. Only the checks stay out of the repository, so that no build can read them.
I don't care much about the controller itself. I picked it because a bad build would cost me something real, and because it's small enough for an agent to build all of it in one run. What I care about is the process around it. How do you build software with agents so that you can trust what comes out, and how much of that process should depend on what you're building?
I don't know the answer. I'm early in my own use of agentic tools, and the field is young; the tools change every few months, and nobody has years of results yet. I think anyone who claims to hold the truth here is misleading themselves and whoever listens. What follows is my current understanding, written down so the runs can test it and readers can argue with it. When a run shows I was wrong, the episode will say so.
My working idea is that there's no best agentic SDLC, only a process that fits a product, and that you can work out one from the other. This episode tries that for my product, before anything is built or measured. The idea may not survive the series.
This isn't a benchmark: every result is per expectation, on my fixture, and nothing gets ranked.
##The product§
heatctl is a command-line program on a Raspberry Pi. It reads room temperatures from sensors and switches relays that turn the heating on, zone by zone. The sensors and relays are Shelly devices already in the house, and everything talks through an MQTT broker on the local network. Setup comes from one YAML file.
That's the whole thing. There's no web UI, app or cloud, and it doesn't try to predict anything.
It'll run the heating in my office, which gives the series a very short feedback loop. If a build gets it wrong, I start the next morning at my desk in a cold room, and I've yet to find a better reason to fix a bug quickly.
##Two questions choose the process§
Before choosing any tool, I answer two questions about the product. They're the two that have mattered most in what I've tried so far, not a complete list. As I understand it, the answers decide how much the agent can do on its own, and how many barriers sit between it and the real world.
1. Can a machine check it, and how fast? For heatctl, yes: it heats or it doesn't. A relay is on or off, and a room is above or below its target. The builder's own tests can answer in seconds, checks from outside in minutes, and the house itself in two to three days.
That answer is partly by design. I picked a problem that's small but not trivial, on purpose: small enough that a machine can check all of it, with enough going on that a build can still get it wrong. And the spec asks for a command-line tool (C2), which is something an agent can easily play with. It can run a command and read what comes back, and so can the checks, with no browser or screen in between.
2. What's the worst that can happen if it's wrong? A cold house, or a boiler that switches on and off too often and wears out. And while it's being built, the agent can damage the machine it runs on, and whatever credentials it can reach.
In what I've read and tried, the second question is the one that gets skipped most often. Here are three products a machine can check equally well:
| Product | Worst case if a build is wrong | What the process needs because of it |
|---|---|---|
| A personal blog | a broken page, fixed in the next commit | nothing extra: build, check, publish |
heatctl | the house goes cold, or the boiler short-cycles and wears out | relay timers no build can touch, a soak in the house before a build is accepted, rollback to the previous build |
| A bank transfer service | money goes to the wrong account | a human approves every release, strict separation from real accounts |
They share an oracle, and I'd still give them three different processes. A blog run with a bank's process would just be slow. A bank run with a blog's process is how money ends up in the wrong account.
For heatctl, this is what I take from the answers:
- Machine checks decide, so the agent can iterate without me.
- Three loops at three speeds, because the checks that matter take very different amounts of time:
| Loop | Who checks | How long | What it catches |
|---|---|---|---|
| the builder's own tests, if it writes any | the builder | seconds | what the builder thought to test |
| hidden checks against the spec | a model follows the README; code reads the result | minutes | the spec, from outside the build |
| a soak in the house | the house | two to three days | what no check imagined |
- Hard limits outside every build. I set an automatic off timer on each relay by hand, and the boiler has its own safety. No build can change either.
- A soak and a rollback. Only one build runs in the house at a time. A new one goes in only after it passes the hidden checks, and it's accepted only after a few days there. If the soak fails, the previous build goes back in.
- A cage for the builder. The second answer covers the build too, since the agent runs code on a real machine. I can't judge which cage yet; first I need to see what the agent actually touches.
The two questions are written out in full in process-fit.md, and every decision record in the series cites the answer that forced it.
Ask them of a different product, say a forecast of next week's heating, and the first answer changes. A machine can't tell whether a forecast is useful, and the real answer takes weeks to arrive. I'd expect that product to need a different process, even with the same engineer and the same house.
VISIBLE TO THE BUILDER ┊ HIDDEN FROM IT
┊
┌─────────┐ ┌────────────┐ ┊ · the evaluator
│ SPEC.md │ ──> │ empty repo │ ┊ · expectations and checks
└─────────┘ └─────┬──────┘ ┊ · the house
▼ ┊
┌───────────┐ ┊
│ build │ ┊
└─────┬─────┘ ┊
├─> own tests ┊ seconds
├─────────────┼─> hidden checks minutes
└─────────────┼─> soak days
##What moves, and what doesn't§
Over the series two things will change, never both at once:
- who owns the loop that turns the spec into a build
- where the model runs
The spec, the hidden checks and the house stay put. Changing one thing at a time is the only way I know to tell which change caused a difference. When a change does move both, I'll label it and say what can't be attributed.
##Spec first§
Every build starts from an empty repository and the complete spec. Nothing gets added feature by feature, so every build aims at the same target, and a number from late in the series can be compared with one from the start.
The builder sees the spec and nothing else: not these notes, not the checks, and not which criteria a machine grades.
I considered three other ways and dropped them. The first was to let the agent write the spec on each run. I tried that in earlier experiments, and the generated spec became a second source of variance: when a build went wrong, I couldn't tell whether to blame the spec or the build. The second was to add features version by version, but then a later build would face a harder target than an earlier one, and their numbers wouldn't compare. The third was to write a reference app by hand to validate the checks, which would break the one rule the series is built on. So the checks are validated another way: they have to fail on an empty build, and catch a few deliberately broken ones.
The spec never mentions tests, and neither will the prompt. For every build I record whether the builder writes tests on its own, what they cover, and whether it runs them before saying it's done.
##Deriving the spec§
I started from a short prompt, the one I'd have typed into a coding agent anyway. Then I went through every decision a builder would otherwise make on its own, and made it in the spec instead. Each open decision is a place where two builds can differ, and any difference I didn't ask for is noise.
Take a room with two sensors. Is its temperature the lower reading, the higher one, or the mean? All three are reasonable, and each heats the house differently. The spec says the mean (B2).
Each part of the spec comes from a constraint of the product:
| Constraint | What it puts in the spec |
|---|---|
| It runs on a Raspberry Pi, next to Shelly devices that already speak MQTT | a single Go binary for linux/arm64 (C1); the broker is its only network peer (C3, I1); the devices' own topics and payloads, copied as they are (I2) |
| The worst case must be a cold house, never a hot one | a zone with no readings turns off (B5); relays turn themselves off if heatctl dies (B9); stopping it turns them off (R5); it never changes a device's configuration, so the hand-set timers stay out of reach (C5) |
| The boiler must not short-cycle | a 0.5 °C band either side of the target (B4); never two switches of one relay within five minutes (B7) |
| Battery sensors sleep and may stay silent for minutes | a reading counts only while it is fresh (B1); the room uses the mean of its fresh sensors (B2); two minutes of grace at start before any command (R4) |
| A restart must behave like a first start | no saved state: nothing written to disk, nothing read back (C7); retained MQTT messages are ignored because their age is unknown (B1); the first control cycle decides every relay from scratch (R4) |
| The house configuration is written once, for every build | the YAML shape is fixed, and unknown keys are an error (I3) |
| Each build invents its own command line, and someone has to operate it | four operations are fixed, their names and flags are not (X1); the README must be enough to install and run it (R1) |
| It runs unattended, and I need to see what it did | one JSON event per control cycle, with the readings and decisions (L1); logging never affects control (L3) |
| It is built whole on every run | a pure CLI with no listener (C2), no prediction (C4), an explicit out-of-scope list |
One line in the spec doesn't come from a constraint. It comes, word for word, from my first prompt: sensors other than Shelly might be added later, over Bluetooth or Zigbee, still through the broker (C6). I kept it as I wrote it, even though nothing in the spec asks for those sensors. I'm curious what each build does with it: ignore it, or build a plugin layer for it.
A few things are left to the builder on purpose: the names of the commands, the internal structure, how the status command reaches the running controller, and whether to write tests at all.
The whole spec is spec/SPEC.md.
##Features and criteria§
The spec ends with nine features, ordered from simplest to hardest. Each has acceptance criteria, and each criterion is one observable behaviour. I mark every criterion M if a machine can check it, or H if a person has to judge it.
| Feature | Criteria | M | H |
|---|---|---|---|
| F1 Configuration | 3 | 3 | 0 |
| F2 Sensor readings | 3 | 3 | 0 |
| F3 Targets and schedules | 5 | 5 | 0 |
| F4 Zone decision | 5 | 5 | 0 |
| F5 Relay control | 4 | 4 | 0 |
| F6 Failures, start and stop | 7 | 7 | 0 |
| F7 Status | 2 | 1 | 1 |
| F8 Logging | 2 | 2 | 0 |
| F9 Documentation | 1 | 0 | 1 |
| Total | 32 | 30 | 2 |
A machine can check thirty of the thirty-two. The two that need a person are whether the status output is readable and complete, and whether the README is enough to set heatctl up as a service. That ratio is the first question's answer, put in numbers.
The M and H marks aren't in the spec. If the builder knew which criteria a machine grades, it could put its effort there and neglect the rest.
##How a criterion is checked§
Each criterion becomes an expectation: the state the system has to reach, written before any build exists. It never says how that state is observed.
The checks are hidden from the builder and never fed back to it. Each one starts a fresh MQTT broker and plays the devices on it, publishing sensor readings and listening for relay commands. Since every build names its commands differently, a model reads the build's README and operates it, and that model stays the same for the whole series. Then a few lines of code read the result off the broker and decide pass or fail; the model never does.
A check can fail in two ways: the build did the wrong thing, or its README didn't say how to make it do the right one. I report the two apart.
When a scenario depends on the time of day, the check moves the clock from outside the build.
##The verification set§
Every expectation runs on every build. Ten of them are reported on every version, as a pass count out of k builds (five by default):
| Criterion | What it exercises | Why it is in the set |
|---|---|---|
| F1.AC2 | each configuration rule, broken on its own, is reported | the widest feature, so I expect it to be the one most often built in part |
| F3.AC4 | Monday 05:00 still uses Sunday's 22:30 entry | wrong at 05:00 on a Monday is a cold house |
| F4.AC2 | the relay stays on inside the band and goes off above it | the core of the product, including when it stops |
| F4.AC4 | every sensor in a zone goes stale: the relay goes off | the fail-safe goes cold, never hot |
| F5.AC1 | a switch that comes too soon waits, or is dropped | protects the boiler |
| F5.AC3 | heatctl is killed with a relay on: the relay goes off by itself | the one safety that must work after heatctl is gone |
| F6.AC1 | the broker restarts: control resumes on its own | the house runs unattended |
| F6.AC6 | killed and restarted, it decides each relay from scratch | the clause I expect a builder to "improve" with a state file |
| F6.AC7 | a retained reading at start is ignored | MQTT clients deliver retained messages by default, and the obvious code trusts them |
| C5 | it never changes a device's configuration | keeps the hard limits out of reach |
Two kinds of expectation are left out of the set. The H criteria need a person on every build, so they're reviewed only on the build proposed for the house. The log format runs on every build too, but I report it only when it fails.
##What this episode does not have§
No code, no builder, and no numbers from a run. Nothing here has been measured yet, so every choice above is a guess until the runs come in. From here on the spec is fixed, and it changes only by a version bump, logged as drift.
NOTEI drafted this episode with an agent and edited it myself. The decisions in it, and the mistakes, are mine.