One Spec to Rule Them All: a single source of truth for firmware, tests and docs
Agentic engineering is a software development discipline in which humans define goals, constraints, and quality standards while AI agents autonomously plan, write, test, and evolve code under structured human oversight.
As the methodological basis we use spec-driven development (SDD), an approach that treats specifications as executable contracts and derives the code from them. The methodological comparison with waterfall, Scrum® and SAFe® sits in its own article: Spec-driven development, Scrum, SAFe, and waterfall — what works when →
The concrete form we have chosen for our embedded devices is a single source of truth (SSOT) in JSON, from which firmware, tests and manual are generated. This article describes how that looks in practice.
The experience reported below is based on Claude™ Opus 4.7 with a 1 M context window, run in modes high through max.
The architecture in one picture
A lean message bus (not much more than a mailbox per role, with persistence) connects several AI instances. Together they run a largely autonomous cycle of firmware, test and interface documentation for our embedded devices.
We staff five roles, all by AI instances:
- Domain expert / Spec owner: owns the contract and is the only one who changes it.
- Firmware developer: implements against the generated code and writes the generator for its code (header files) itself.
- Test agent: writes the tests itself, runs the hardware-in-the-loop sweeps and triages every failure against the spec.
- Docs and tooling: maintains the generators, the manual, and the build and test pipeline.
- Monitor: implemented the bus and watches the system. We use it to analyse where the messaging jams and to refine roles when needed.
The human sits outside the cycle. The goal is to take the human out of the loop as far as possible. They step in when something jams, and they decide the questions that cannot be delegated: architecture, customer wishes, prioritisation, final sign-off.
Reality adds plenty of surprises that pull you back to the keyboard whether you wanted to or not: Windows® reboots the host, Claude has an outage, the credits run out, or the Tortoise cache runs amok again and freezes the system with its wild indexing.
The problem: drift
An embedded device has an interface, a list of values with address, type, allowed range, default and meaning. That contract has to be true in three places at once: in the firmware, in the tests and in the manual.
In normal practice those three copies start drifting apart almost immediately. Someone widens a value range in the firmware. The manual still shows the old range. A test still asserts the old maximum and starts failing for the wrong reason. Six months later nobody is sure which of the three is right.
The only lasting cure: stop maintaining three copies and maintain one.
The SSOT in JSON
The interface contract lives in a single, version-controlled JSON file. Every change is fully traceable through the version history. Every value is defined exactly once and carries everything anyone downstream needs: address, data type, permitted range, named states, access rules (read-only, read/write, protected), power-on default, and a plain-language description.
JSON because a machine can read it directly. The spec becomes human-readable through the documentation generator that produces a complete interface reference from it. There are not two formats (one for machines, one for humans), there is one format and a generator.
What gets generated
From that file we regenerate, on every change:
- Firmware code: tables for every value with range, states, access rules and default. No hand-typed addresses, no magic numbers; the read, write and validate paths are driven by the generated tables.
- Host-side driver: tools and tests speak to the device through the same definitions.
- Test expectations: ranges, defaults, named states and access rules come from the spec. A test cannot check anything the spec does not define.
- Customer manual: full interface reference, in the master language plus one-way translations into 28 target languages.
The spec is not flawless from day one. A wrong range, a forgotten state, an unclear description is part of normal iteration. Such errors are fixed in the spec, then everything underneath is regenerated. As the spec matures, this happens less and less often.
The human does not edit the JSON file by hand; a request to the domain expert / spec owner is enough. And in the generated documentation everything is presented in human-readable form, ready for thorough review.
Testing on the real target
Every iteration is tested on real hardware, a setup known as hardware-in-the-loop (HIL). Reason: timing and concurrency bugs only show up there. Simulating the target’s full hardware is only partly possible, so the simplest path is to run on the device itself.
The agents do have additional skills for pre-clarification: analyse code statically, run scripts locally on the host, exercise a simulation of the device. A lot is resolved up front; not every question has to go to the target. For the final verdict though, the silicon is the judge, and it is measured against the spec.
Audit gates: the build stops on drift
Generation alone is not enough. Someone can still re-introduce drift by hand, so the build (not just the tests) fails on violations:
- no hand-written addresses or magic numbers anywhere in the firmware,
- no symbol in the code without a spec entry, or vice versa,
- shared definitions consistent across every product variant,
- protection and access rules in the firmware matching what the spec declares.
The checks are boring. That is exactly the point. “Please keep everything in sync” turns into “the build refuses to continue if you didn’t”.
There is a balance to strike between iteration speed and quality. My favourite instruction to the agents on that: we cut the old habits loose. And we do it now. From the agent’s point of view that is in plain contradiction to the equally valid rule “the codebase is stable and mature, we only make minimal changes”. Living with both at the same time is part of the job, for humans and agents alike.
The spec wins
When behaviour and spec disagree, that is a bug in the firmware or a mistake in the spec, never a licence to quietly adjust the test. The fix always flows in one direction:
change the spec → regenerate → build → flash → re-run the HIL sweep.
That single rule is what keeps the source of truth actually true over years. It also makes multi-language docs a non-event: edit the master language once, translation flows one way.
In practice a spec change unfolds like this. The agents talk to each other in natural language. The firmware developer runs into a register value whose behaviour the test agent considers wrong. On request, the firmware developer explains the implementation, and together they work out a fix. A short cross-check that the problem really was understood is part of the loop. The change is then reported to the domain expert / spec owner, who in most cases can apply it and communicates the adjustment to the affected places.
Skills, rules and typical problems
Every role gets clear skills. When we see a problem in behaviour, we refine the skills or the rules rather than papering over the issue. What strikes us repeatedly: the agents imitate human behaviour in real teams quite closely. Three classics:
- Greenwashing: only a stub is implemented and the real implementation is deferred as a future task. Countermeasure: skills that distinguish “done” from “stub”; audit gates that flag unimplemented paths.
- Sloppiness: instead of using the generated values, the agent invents its own constants that happen to fit. Countermeasure: the audit gates against magic numbers and unknown symbols.
- Path of least resistance: tester and firmware get into an argument. The firmware insists the tester must first prove there is a bug before continuing. The tester refuses further runs until all firmware bugs are fixed. Countermeasure: proactive instruction to the firmware (“ship, then we discuss”), and to the tester “keep it rolling”, meaning keep testing, log failures, do not block.
These patterns are the same ones every engineering manager knows from real teams. They disappear with clear skills, clear rules, and a spec that settles every dispute.
Field anecdotes
- Two agents blamed each other for a failing test, each insisting the other’s part was the broken one. The spec as referee ended the finger-pointing instantly.
- Once I rubbed my eyes after an hour because the problem was not the AI but the BI, the biological intelligence in front of the screen. The AI had long since isolated the issue, it just could not communicate it in a way I understood. I was stuck in the old “you muppet, what have you broken this time” mindset, while it had been right the whole time.
- A language model has no built-in sense of time. If you describe procedures as an ordered list with explicit waits (do this, wait, then expect that), the test agent suddenly understands timing bugs it had been blind to.
- In a dedicated test build (never in the shipped firmware) we route internal signals onto a test interface: which state a state machine is in, which events a task has already seen. A flood of information that would take a human days to work through helps the agent read the internal behaviour in seconds rather than guess at it. Before that, it got imaginative and proposed causes that went well beyond metaphysics, from cosmic rays to the phase of the moon. That is the big problem in engineering in general: context decides. Once the agent really sits inside the context, much becomes far easier. That is why small iterations usually beat big ones, though every so often a Big Bang works surprisingly well, and a larger refactor started against the agent’s advice often goes through surprisingly smoothly.
- Left idle, an agent gets frozen. We keep it awake with a wait task that only ends when the next message arrives. One day that waiter ran twice; the wakeup got swallowed; the agent never came back. A concurrency bug in the very tool that hunts concurrency bugs.
The spec is the guard rail. The cycle drawn here is only triggered when the JSON spec needs to change. The autonomous ping-pong between test agent and firmware runs separately and continuously.
What we deliberately did not build
No heavyweight orchestration framework. The machinery stays small: one spec file, a set of generators, a hardware test runner, a handful of audit checks, a lean bus. Small enough that a new engineer can read the whole thing in an afternoon.
Takeaways
- One machine-readable contract in JSON, with all artefacts generated from it.
- Tests on the real target, against expectations taken from that exact contract.
- Audit gates that fail the build on drift.
- One rule: the spec wins.
- Clear skills and rules per role so the agents do not slip into human dysfunctional patterns.
- The human sits outside the loop, steps in to correct or advise, and decides on architecture and customer questions.
Functional tests that are relevant to certification run outside this cycle, with their own protocols, audit trails and sign-offs.
Keywords: Single Source of Truth, Spec Driven Development, Agentic Engineering, firmware code generation, Hardware in the Loop, JSON interface contract, AI agents firmware consulting, audit gates, Claude Opus, embedded HIL testing, Embedded Consulting Switzerland, swissEmbedded firmware engineering
Are firmware, tests and manual drifting apart in your project? We build the single source of truth pipeline that generates all three from one contract and checks them against real hardware, from the JSON schema through the generators to the automated HIL testbed.