5 systems, instrumentedPhoenix, ArizonaPHX--:--:-- MST

02 / Case study

The Build Contract

How I ship with autonomous agents: a spec, failing tests, a phone approval, then a budgeted run

Stack
Claude Code (cloud) · GitHub · Python · pytest
Measured on
6 packets / 38 commits in 2 days / 418 tests green

Problem

An autonomous coding agent will happily report success. The hard part is not getting it to write code, it is knowing, from a phone, without reading every diff, that what it built is what I asked for. Left alone, an agent can meet its goal by moving the goalposts: loosen a test, skip a check, rewrite the spec it was handed.

So the thing I actually built here is a contract: a fixed sequence where every claim the agent makes can be checked by a command, and the parts that define “done” are out of its reach.

The app it produced is real and I use it: a local sample-prep tool for music producers, with BPM and key detection, stem separation, a waveform editor, and clip export with a source sidecar. But the app is the evidence. The workflow is the work.

What I built

A build loop with one human at the two points that matter: approving the plan and merging the result.

  1. Brainstorm in chat. The idea gets argued out in conversation first, where it is cheap to change.
  2. Commit a BUILD-PACKET. The outcome is a spec file committed to the repo: acceptance behaviours written as WHEN / SHALL statements, a table of done-conditions that are literal shell commands, the protected paths, the milestones, a budget, and the decisions I reserve for myself.
  3. Plan pass, then stop. An agent reads the packet and the repo, writes PLAN.json and the acceptance tests, and runs every done-condition to prove each one fails today. A test that already passes proves nothing, so it has to fail first. Then the agent stops and asks its open questions.
  4. Approve from the phone. I read the plan and the failing-test table on my phone, answer the questions, and approve.
  5. Build under a budget. The agent builds milestone by milestone, autonomously, with a cap of 150 turns, 6 hours of wall clock, and 3 retries per milestone, plus a named kill point where it stops and reports instead of pushing on.
  6. Report, then merge. The run writes REPORT.md: every done-condition with its command and exit code, deviations, and turns and time used. I read it and merge the PR.
## Acceptance behaviours
AB4: WHEN source audio is ready THE SYSTEM SHALL report BPM (raw and
   folded into a configurable window, default 70-180) and key as
   top-2 candidates with confidence and Camelot codes. After
   separation, key SHALL be re-estimated on the instrumental stem,
   and both results stored.
AB5: WHEN the user starts a split in Best or Fast mode THE SYSTEM SHALL
   produce vocals and instrumental stems at the source sample rate,
   emit progress events, and record which model and compute tier
   were used.
AB7: WHEN the user exports a region THE SYSTEM SHALL write a 24-bit WAV
   of the chosen lane with start/end exact to the sample, 5-10 ms
   fades at both edges, [...] and a sidecar JSON beside it.
[...]

## Done-conditions
| id | command                                  | expect | must fail today |
|----|------------------------------------------|--------|-----------------|
| D1 | pytest .../test_foundation.py -q         | exit 0 | yes             |
| D3 | pytest .../test_analysis.py -q           | exit 0 | yes             |
| D4 | pytest .../test_separation.py -q         | exit 0 | yes             |
| D5 | pytest .../test_export.py -q             | exit 0 | yes             |
| D7 | pytest .../test_e2e_fixture.py -q        | exit 0 | yes             |
| D8 | pytest -q -m "not slow and not pc"       | exit 0 | yes             |
Test rules: no network in any test [...]. All audio fixtures are
synthetic (click tracks, chord tones, sine mixes). A 120 BPM click
track reads 120 +/-1, a 174 BPM track folds correctly, and a synthetic
A-minor progression has Am / 8A in its top 2.

Figure 1. From the first build packet, lightly trimmed: behaviours the tests must prove, and the commands that decide done. Every row in the second table had to fail before the build was allowed to start.

How it works

Three rules keep the agent honest:

Done is a command, not an opinion. Every milestone is met only when its done-conditions and the full offline suite have been run and printed with exit codes after the final code change. “It should work” is not a state the report can contain.

The agent cannot edit its own exam. The packet, PLAN.json, the acceptance tests and their fixtures, the agent settings, and the loop file are protected once I approve the plan. The goal condition includes a git diff --exit-code against the approved plan commit over exactly those paths. If the agent touched them, the goal is not met, however green the tests look. When a test genuinely needs to change, that change goes back through me as a plan update, and the commit says so.

Judgment stays reserved. The packet names the decisions I keep, such as any licence that is not clearly commercial-safe or a change to the UI shell, and the actions that need a yes first: a new dependency, a change to a protected path, anything that touches money, email, or client assets. Hitting one is not a reason to guess. The agent writes it into REPORT.md as a question and treats the goal as impossible until I answer.

The environment enforces the rest. The test suite blocks every non-loopback socket, so no test can reach the network. Every heavy part, from the downloader to the stem separator to the GPU probe, sits behind an interface with a fake, so the whole pipeline runs end to end offline on synthetic audio.

The GimmeDat editor with a fixture track opened from a local file. It reads 120.1 BPM and key 8A, A minor, with a yellow unknown-rights label. Three waveform lanes, mix, vocals and instrumental, share one playhead, with a 3.7 second region selected across them. The bottom bar holds play controls, Best and Fast split modes, a vocals export selector, and chop settings with 8 slices on a quarter-note grid.
Figure 2. The editor, running on fixture audio. BPM, key with its Camelot code, three stem lanes on one transport, and export of the selected region. The BPM and key readings are checked by tests against synthetic audio of known tempo and key.

Results and impact

Measured from the repository, not from memory:

  • 6 build packets committed in 2 days, 5 of them built and merged as PRs. The sixth is written and in progress.
  • 38 commits on the main branch, from the first commit on 2026-09-27 to the fifth merge about 23 hours later.
  • 418 offline tests passing, 2 deselected as needing the real GPU machine, on the last merged build. I reran the suite myself for this write-up: 418 passed.
  • Each build finished well inside its budget: about 75 to 95 turns and 25 minutes to 3 hours against a cap of 150 turns and 6 hours. From my approval to the last milestone commit took 19 to 41 minutes.
The GimmeDat library. Two exported items from the same local file, a vocal clip and a chop set, with filters for key compatibility, key, BPM range with half and double time, type, rights and source. Each row shows a waveform thumbnail, duration, key, BPM and a rights label, and a player bar along the bottom previews the selected clip.
Figure 3. The library: every exported clip with the analysis and the rights label it was saved with, filterable by what works with the current key and tempo.
The GimmeDat setup screen. A tool check lists ffmpeg, Deno and yt-dlp as found with their versions, the compute tier as ROCm, the Best and Fast separation models, the last split, and a credit line for the fast model's MIT licence.
Figure 4. The setup check. The app starts even when a tool is missing and disables only the features that need it. The build environment used fakes for all of this; the real GPU checks are a human gate on my own machine.

What I would bring to a growth team is the loop, not the app. A spec committed before any code, done-conditions that fail first, one approval on a phone, a budget the agent works inside, and a diff check that stops it editing its own tests. That is how an agent gets trusted with a real build: not by being told to be careful, but by working inside a contract it cannot rewrite.