Products Consulting About Blog Contact Us Česky
arrow_back Back to blog

Towards the Dark Factory: The Agent Sets the Pace

Towards the Dark Factory: The Agent Sets the Pace

Part 3 of 5 in the “Towards the Dark Factory” series

Part 2 described the wall. This part is about what a wall that holds buys you: the agents can be trusted to decide what to do next, and when. Saumya Tyagi’s ladder measures autonomy by who approves the merge; we found a second axis that matters as much on a small team, who sets the pace. At level 2 a person decides what the agent works on next. At the point we reached with Sestava, the agent decides, on a board the person curated, within limits the machine taught us.

Three sentences from the owner

The rule that turned a queue of tasks into a self-directed process was given three times, each time shorter.

On 26 September, day three: “when idle, pick up the next logical task, consider CPU usage.” On 27 September: “once a slot is available, you pick the next task.” And on the evening of 29 September, as the owner went to bed: “when done with the work and a slot is empty, pick a suitable bug during the night.”

Written down as a standing rule, it reads: when a controller hands back, close its run (review, gates, worktree), then pick the earliest-dated open launch item on the roadmap board whose files do not collide with the runs still going, and dispatch it. Items that need the owner (a decision, a sign-in, a device check) are asked for in the report and never blocked on. Announce the pick.

Everything else in this part is about the words “slot” and “collide”.

The machine and its slots

All of this runs on one Mac, ten cores, with the owner’s own work on it during the day. The limits were not designed; each one was found by breaking it.

Two Swift runs at most, and one while a mutation gate runs. On day six three Swift runs shared the machine while one ran a mutation gate. Mutants made the test suite hang until the timeout (about 400 seconds twice, 630 seconds twice); one gate did 32 of 490 mutants in 21 minutes, and the harness cut off the agent waiting for it. The same gates without contention had no timeouts at all. Concurrent Swift runs also build with --jobs 2, so a gate never takes the whole machine.

One booted simulator. The same day three runs booted three simulators. About 170 simulator system processes were runnable, the load average passed 400, and the app hung at launch inside synchronous calls to notifications, speech and audio. Two agents lost an hour each. Now a run shuts its device down when its tests end and waits while another is booted.

One simulator per run, named after the run’s worktree. Before that rule, two app runs tested on the one simulator a helper script picks. Both install the same bundle id, so each run’s tests launched the other run’s build; the app died at launch with a missing symbol, macOS showed “Sestava cannot be opened” dialogs on the owner’s screen, and every test result from the overlap meant nothing. Each run now creates its own device with xcrun simctl create, and the script that finishes a worktree deletes every device with that name.

Disk. A mutation gate ended early at 357 of 516 mutants when free space fell to 600 MB, and a reviewer’s build failed beside it. Each run’s simulator grows to 3 to 4 GB once the app is installed and tested; each worktree holds about a gigabyte of build output; the npm cache once reached 16 GB. A dispatch now checks free disk first, and under 8 GB the coordinator finishes worktrees before starting a run. Build output stays inside the run’s worktree, so finishing the worktree clears it.

Node work (the web editor’s tests, the site’s smoke suite) may overlap with all of that, with one exception learned on 30 September: a 322-spec Playwright suite beside a mutation gate and a UI suite drove the load average to 580, and one launch test took 491 seconds instead of 34. Everything passed. A browser suite for now counts as a Swift run for the slot limit; it becomes a rule if it holds.

The coordinator watches ledgers

A coordinator is the session that holds the board and the slots. It does not read the controllers’ transcripts; it reads their ledgers. mise run status runs a script over every worktree’s progress.md, counts the plan’s tasks and the completed ones, reads the dispatch timestamps, and estimates the finish from the median task duration, labelled as an extrapolation. What it cannot read it reports as a problem; it never guesses a run to be done. It also prints free disk.

The ledger lines are a written format with fixture tests, which sounds like overkill for a text file until you see why. The script was first checked by eye on two ledgers written by one controller. Within the hour a third controller wrote dispatch for dispatched and stamped its lines with seconds: two of three live runs showed 0/9, and one line would have crashed the report for every run. The adversarial review of that pull request found it before the merge, and the fixtures have held since.

Two rules about the coordinator’s judgement came from the same week. Judge a run by its ledger, not by its transcript: a controller whose ledger says “waiting on an agent” is waiting, not stalled; the coordinator once reported a healthy run as stuck from a stale transcript tail. And nudge a controller whose ledger has been quiet for 30 minutes, because a controller stopped while its worker ran and never woke up; one pull request sat for nine hours on one task.

Ledgers that survive a crash

The reason a ledger is the unit of truth, rather than a conversation, is that conversations end. Controllers are cut off by the harness after roughly 120 to 150 tool uses. The weekly usage limit ends every running agent at once, silently, mid-step; it happened on 26 September and again on 30 September at 08:50 UTC, once with uncommitted edits in a scratch worktree. On 29 September the day began from a restart of the owner’s session. On 30 September at 12:27 UTC the user session degraded (no user name from id, an I/O error from getconf, no GitHub, no simulator service) and the Claude Code session crashed without the Mac rebooting.

Every one of those was resumed the same way. A controller writes a RESUME POINT line at every wait: which task, which commit, which brief, what it was waiting for. A successor reads the ledger and continues from that line, never re-dispatching a task the ledger says is complete. After the 12:27 crash, three controllers were cut mid-step and resumed 25 minutes after the restart. Plans longer than about nine tasks are split so a run ends before the cap rather than at it.

The long gates get the same treatment. A full mutation gate takes hours on the loaded Mac (about three for the core package’s 1,730 mutants, about two for the app-logic package’s 1,622), and a controller polling one for two hours spent 20 tool uses and delivered nothing before it was cut off. So long gates run detached, writing an exit line into a file when they finish; a controller never waits on a gate longer than ten minutes; the coordinator watches the file and dispatches the next step when the line lands. The detached gate that was running at 12:27 survived the crash and kept going.

One night, hour by hour

Here is what the pace looks like when nobody is awake to set it. Times are UTC, from the ledgers of 29 and 30 September.

  • 21:15 The owner’s night instruction. The queue: a fix that moves the plan-generation checks outside the language model, a dead Start button after a summary, the app blocking its main thread at launch, and a website fix unblocked by the blog’s merge.
  • 22:00 A read-only Search Console check for the owner turns into two issues on the site’s redirects and missing headings. A haptics fix finishes implementation and goes to review.
  • 00:40 The site fixes are a draft pull request, CI green on Linux too; its review requires changes (hidden links still took keyboard focus; a new page test would fail the first blog post), and a fix round starts. The haptics review requires changes on three points; a fix round starts there too. A plan writer is cut off mid-replay; the coordinator commits its plan and dispatches the re-review.
  • 02:35 Disk hits 600 MB free and ends a mutation gate early. Nine gigabytes are freed (three idle simulators, two derived-data folders, orphaned temp). The launch fix is dispatched once the load average falls to 15.
  • 04:20 The root cause of the night’s app launch aborts is found: the Mac’s audio daemon is stuck, and any audio engine start aborts after about nine seconds on any thread, even at load 2. Every app-hosted test is paused, and the morning list for the owner gets its first line.
  • 04:42 The owner, up early, merges the site pull request.
  • 04:45 The daemon recovers by itself once the machine is idle. App gates for the two fixes run in one agent, one simulator at a time.
  • 05:40 The launch fix is a pull request with every gate green, its old behaviour restored by hand first to show the new test red on five assertions, and a cold warm-up measured at 0.074 seconds.

Nothing in that night was merged by an agent. Two bugs went from the queue through two review rounds each, one to a pull request and one to green gates waiting its turn; one site fix was merged by the owner before six, one environment fault was diagnosed and one disk incident handled, and the owner’s morning list had six items on it.

What the pace costs

The honest ledger has a bottleneck section, and three entries recur.

The serial chain of implementer, reviewer, fix and re-review per task is the quality mechanism and also the wall-clock. A reviewed task was estimated, after the first two days, at 25 to 60 minutes, Swift tasks at the slow end because of the mutation gate. We keep the chain for core and app code; for docs and site tasks one review at the end is enough.

The mutation gate dominates every Swift task. The answer so far is the changed-files run per task and one full run per wave, which is also why the project ships one pull request per wave: CI’s mutation gate is the expensive job, and merging worktrees locally and pushing once keeps it to one run.

And the owner’s decisions arrive in batches. Three pull requests once waited on 19 decisions. The coordinator now records a provisional ruling along its recommendation, small enough to reverse, for technical questions; it does not do that for how the app feels in the hand. Which is Part 5, on 30 October.

Before that, Part 4 (23 October): where the rules in this part came from, and how the agents write the backlog they then work through.


This is Part 3 of the Towards the Dark Factory series. Part 1: Level 2 to Level 3 in a Week. Part 2: The Wall. Part 4: The Agent Feeds the Backlog (coming on 23 October). Part 5: Where the Human Still Is (coming on 30 October).

Related: 140 Pull Requests in a Week · How We Use Claude Code Skills

Cover photo by Hyundai Motor Group on Unsplash.

More from the Blog