buildr.zone_

$ less ~/episodes/00-the-approach

ep00 · 2026-11-01 · spec 1.0-draft · no harness version · 12 min

A heating controller nobody writes

decision
the spec is fixed before the first build, and every build gets all of it
rejected
a spec generated per run; features added version by version; a hand-built reference app
number
32 acceptance criteria, 30 M, 2 H
links
process-fit.md · spec/SPEC.md · no ADR, this episode adds no version
~/episodes/00-the-approach/deep-dive.md12 min

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:

ProductWorst case if a build is wrongWhat the process needs because of it
A personal bloga broken page, fixed in the next commitnothing extra: build, check, publish
heatctlthe house goes cold, or the boiler short-cycles and wears outrelay timers no build can touch, a soak in the house before a build is accepted, rollback to the previous build
A bank transfer servicemoney goes to the wrong accounta 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:
LoopWho checksHow longWhat it catches
the builder's own tests, if it writes anythe buildersecondswhat the builder thought to test
hidden checks against the speca model follows the README; code reads the resultminutesthe spec, from outside the build
a soak in the housethe housetwo to three dayswhat 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
fig. 1 — what the builder sees, and the three loops that grade it

##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:

ConstraintWhat it puts in the spec
It runs on a Raspberry Pi, next to Shelly devices that already speak MQTTa 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 onea 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-cyclea 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 minutesa 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 startno 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 buildthe YAML shape is fixed, and unknown keys are an error (I3)
Each build invents its own command line, and someone has to operate itfour 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 didone JSON event per control cycle, with the readings and decisions (L1); logging never affects control (L3)
It is built whole on every runa 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.

FeatureCriteriaMH
F1 Configuration330
F2 Sensor readings330
F3 Targets and schedules550
F4 Zone decision550
F5 Relay control440
F6 Failures, start and stop770
F7 Status211
F8 Logging220
F9 Documentation101
Total32302

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):

CriterionWhat it exercisesWhy it is in the set
F1.AC2each configuration rule, broken on its own, is reportedthe widest feature, so I expect it to be the one most often built in part
F3.AC4Monday 05:00 still uses Sunday's 22:30 entrywrong at 05:00 on a Monday is a cold house
F4.AC2the relay stays on inside the band and goes off above itthe core of the product, including when it stops
F4.AC4every sensor in a zone goes stale: the relay goes offthe fail-safe goes cold, never hot
F5.AC1a switch that comes too soon waits, or is droppedprotects the boiler
F5.AC3heatctl is killed with a relay on: the relay goes off by itselfthe one safety that must work after heatctl is gone
F6.AC1the broker restarts: control resumes on its ownthe house runs unattended
F6.AC6killed and restarted, it decides each relay from scratchthe clause I expect a builder to "improve" with a state file
F6.AC7a retained reading at start is ignoredMQTT clients deliver retained messages by default, and the obvious code trusts them
C5it never changes a device's configurationkeeps 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.

$ next constraint

A spec and hidden checks don't mean much until something builds against them. So the next constraint is the loop itself: getting from an empty repository to a build in the house, unattended, before any part of it is worth improving.

source: github.com/enricomarchesin/buildr-agentic-sdlc · spec 1.0-draft