Field notes / Agent operations
Running a fleet of coding agents alone
What a one-person company learned running four kinds of coding agent against one repository, and the audit that found the instruction files disagreeing with each other.
### YYYY-MM-DD HH:MM TZ · agent · scope
**Did:** concrete work and evidence, with commit only if one exists
**State:** uncommitted, running, incomplete, or complete state
**Watch:** what the next agent must know to avoid duplication or breakageI teach band during the day and I am the only engineer on Virtunity. Since August, most of the code has been written by coding agents. Not one agent. Four kinds, in parallel, against the same repositories, with me as the only human reviewer.
This is not a note about prompting. It is a note about operations: how the work is handed out, how it is checked, what the agents are allowed to do without asking, and what happened when I audited the rules they were all reading. The interesting failures were never in the code. They were in the coordination.
A launcher receipt describes process exit, not task correctness, and never authorization to release.
The shape
One agent coordinates and several execute. The coordinator, which I call the hub, reads the state of the company, writes the briefs, launches the coders, verifies what came back, and talks to me. The coders never talk to me. The hub runs on the most capable model I can get and is reserved for strategy, direction, and the hardest problems. Everything bounded goes to something cheaper.
- 01HubCoordinates
Reads the dashboard and the log, translates an owner decision into a scoped brief, launches and verifies, updates the current view, and frames the result for me.
- 02CodersExecute
Two headless coding agents from different vendors, launched with a brief on standard input and a claim on specific paths. They commit to a dated branch and file the four-line entry.
- 03SubagentsBounded
Native helpers for searches, reads, and gates. They are never allowed to inherit the hub’s model, and they never audit each other.
- 04ResearcherReads only
A separate model for scouting and sourced research. It holds no logins and never posts.
The rule that took longest to learn is the one about the hub’s model. A subagent that inherits the expensive model burns the budget on a file search. A hub that does its own file searches burns the same budget on the same search. The fix was a routing rule written down once and enforced by the launcher’s default model, not by remembering.
The launcher
There is exactly one sanctioned way for the hub to start a coder: a shell script that takes the coder’s name and a brief file. It exists because the raw command lines were being blocked by the hub’s own safety classifier, and a single allow-list rule on one script was cleaner than trying to whitelist a pipeline with redirects in it.
Once it existed, it became the place where the coordination rules live as code rather than as prose.
- It refuses to launch, with its own exit code, while another instance of the same brief is still running. A pidfile, nothing fancy.
- A brief may carry a machine-readable claim: a JSON comment listing the paths it intends to write. Cooperating launches refuse parent and child overlaps, and both the lexical and the resolved paths are reserved so a symlink alias collides too.
- It selects the pinned Node version before handing off, so the gates the coder runs see the same toolchain the site deploys with.
- It streams the coder’s output to a dated log, captures the last message to its own file, and on exit inserts the four-line entry into the shared log with the exit code. Every run leaves a receipt whether or not the coder wrote one.
The docstring on the coordinator is honest about what the lock is not: it cannot serialize editors that do not use it, and it is not a permission or production-safety boundary. Those live elsewhere.
The brief contract
Every brief is a dated file and contains the same sections, because a coder that has to guess any of them will guess wrong in a way that costs a round.
- The outcome, the known evidence, and the unresolved assumptions, named as such.
- The repository and the starting version. Allowed paths and protected paths.
- The required behavior and the smallest useful deliverable.
- Exact check commands with expected results, and dated baselines where a count can drift. “Tests pass” is not a gate. “1,458 passed, 21 skipped, 0 failed” is.
- Which acceptance only a human on a device can give.
- The stop classes, by reference, and the size of the report.
What it forced
Gates must be machine-verifiable. One statement per constraint, exact commands, expected counts, named files. A gate the coder can satisfy by paraphrasing is not a gate.
Five stop classes, and nothing else
Early on, the agents asked permission constantly. In one two-day stretch on a staging deploy they stopped for approval fifteen times. I was the bottleneck on my own automation, and every stop was a context switch away from a classroom.
So I ruled on exactly when an agent must stop, and wrote it as a closed list. An agent stops and reports, briefly, when one of these happens:
- Any production write: the live database, production hosting, live payments, or the app store.
- Real money movement or a billing-plan change on any provider.
- A secret reaching output, a transcript, a commit, or a file outside the owning process.
- Any identity or access grant, role change, or service-account creation.
- Any write outside the paths it claimed.
Everything else: decide, execute, and log. The protocol adds one more line that turned out to matter as much as the list: do not use approval phrases. An agent that ends a message with “shall I proceed?” has stopped without a stop class, and it has done it in a way that looks polite instead of looking like the failure it is.
The four-line log
Every completed unit of work, from any agent, becomes exactly four lines appended newest-first to one shared file: a timestamp and scope, what was done with evidence, what state the work is in, and what the next agent must know. Corrections are new entries. Nobody rewrites history, including their own.
The value of that last rule showed up on the morning I am writing this. The hub had been stamping its entries with times that ran ahead of the machine clock, by hours, while the coders, which read the clock, were correct. The fix was not to edit the entries. It was a new entry that says which times are wrong, gives the real times of the day’s owner actions, and records the rule change: the hub runs date before stamping anything.
The log is also where a finished unit is proven finished. Before the entry is written, the agent commits its claimed paths to a dated work branch and puts the short hash in the entry. Uncommitted finished work is not finished. Push, merge, and deploy still wait for me.
Failure modes I actually observed
Polling the coder
The hub’s first instinct with a running coder was to check on it. Every check reads the growing log into the hub’s context, and the log of a forty-minute build is not small. The coder gained nothing from being watched.
What it forced
Never poll a running coder. One wait for exit, then read its report once, then run one independent check. The wrapper’s exit receipt is process evidence, not proof the task is done.
Briefing the subsystem instead of the user path
Two rounds were wasted on a sound bug because the brief described the audio subsystem and the coder, reasonably, tested the subsystem. It reported “not reproduced.” The bug was on the first screen a student touches, and the subsystem was fine.
What it forced
Brief the user’s first screen and first action, and name the shipped commit the bug lives in. After one “not reproduced,” the hub reads the source itself before spending a third round.
Self-halting on scratch paths
A coder would stop with a stop-class-three report because a build wrote a cache under a home directory it had not claimed. Technically correct, and useless. The build had to write somewhere.
What it forced
Every build or test brief claims its scratch paths and its package caches up front. When the gates are green and only the commit remains, the hub finishes the commit rather than relaunching a coder to do it.
The owner getting lost
Mid-build, with three workstreams moving, I could not tell from the hub’s updates what was done, what was in flight, and what needed me. The updates were accurate. They were also unreadable to the one person they were for.
What it forced
Every owner update ends with three labels: Done, Doing, Your turn. One decision per message. The mechanics go in the log, where they belong.
The audit
On September 6 I had the hub audit every file that reaches a model as instructions: the two constitution files, the three protocols they cite, the brand skill, a scheduled shortcut, the coder rules file, the generated per-repository instruction files, and a sample of recent briefs. The rubric was the current model vendor’s own guidance on what makes an instruction file stale.
Fourteen findings. Ten carried a concrete edit, eighteen hunks across six files, all applied the same day. Three of them mattered.
- The brand rule could not be followed as written. It told the model to verify an instrument count under a configuration key that no longer existed, and hardcoded a number the file had since moved past. Every external copy pass loaded that rule, failed the lookup silently, and fell back to the stale numeral.
- The July rules file disagreed with the September operating setup on six rules while claiming it could not be overridden without a decision entry. Plan-first engineering, mandatory subagent parallelism, session-end paperwork. Every session read both and had to reconcile them on every startup, and each one reconciled them a little differently.
- A dormant shortcut instructed the model to open a secrets file. It had not been registered in the scheduler for months, so it was inert. It was also one re-registration away from a service key in a transcript, in direct conflict with stop class three.
The audit also found what did not exist: no token accounting per surface, so there was no way to measure whether any of this cleanup saved anything. That is on the list.
What it forced
Instruction files drift exactly like code, and nothing tests them. Audit them against the current model on a schedule, and treat a rule the model cannot follow as a bug, not as a rule.
What I would tell someone starting this
- One launcher, one log, one report shape. Agents from different vendors behave differently. The receipts should not.
- Write the stop list as a closed list. An open-ended “ask when unsure” produces fifteen interruptions in two days. Five named classes produce fewer than one a day.
- Claims before edits. The coordination failures were all two agents in one file. The lock is cheap.
- Reserve the expensive model for judgment. Then enforce it in the launcher, because a rule that lives in prose gets forgotten under load.
- Corrections are appends. The log is only trustworthy if nobody, including the hub, can make yesterday look different than it was.
Honest status
The agents were never the hard part. The hard part was writing down, once, what they are allowed to decide.
Counts in this piece come from the project’s own worklog, decision records, the launcher source, and the September 6 audit report.
This field note was drafted with AI assistance from those records. I reviewed the claims against them, which is a slightly recursive thing to say about a note on agent operations.