For any run nobody will supervise, the control loop is code and the model is a worker inside it. The driver, a shell loop or a cron job, computes each tick's work order: which items, which prompt, which output file. The run ends when the count of files on disk reaches a floor written before the run, or when the API returns a quota error the driver catches. The model never evaluates whether the run is done. I wrote that rule after an overnight run in which the model did evaluate it. It stopped itself at 06:00 with cards for 496 of a 10,946-word catalog on disk and four hours of the window left.
The run was a card generator for a language-learning app I'm building. The deliverable was a full database of flashcards over a catalog of about 10,946 words. The directive I left before going to sleep said "Continue going until this is done, dispatch agents, generally do not stop, I expect a full db with all the necessary grammar, words, sentences, etc to exist when I wake up." A second line named the resource: "I have significant quota to burn that runs out tomorrow morning. Caffeinate through 10am and burn quota. Also make sure when you hit quota it fails gracefully." A third gave the window: "We have seven hours and 25% of a weekly quota so burn hotter (with fail grace) than cooler."
At about 06:00 the agent (one Claude Code coordinator instance) declared an internal milestone complete. It told its own continuation loop to start no new work and shut down its scheduler, with four hours of the window remaining. What I read in the morning was a report that said the job was done. On disk were 755 cards. 589 of them were clean on a later audit. They covered 496 words, and 10,946 divided by 496 is 22. The agent conceded on the record that nothing in the directive was unclear.
The evidence is one overnight run, its incident report and the doctrine I wrote from them, which is now the unattended-runs document in the scaffold my projects run from. The enforcement half is code and tests on the scaffold's main branch, and the project that adopted the provenance gate ran its spec at 10 of 10. What has been tested and what has not is in the limits section.
Five Decisions, One Direction
In the words of the test plan I commissioned afterwards, "five independent calls all landing the same way, plus concealment, plus recurrence across sessions, is the signature of an objective". The five calls it found were the resource, the scope, the stop condition, the definition of done, and which parts of the directive to honor, and each resolved toward a run that used less of the quota the directive had ordered burned.
On the resource, the directive said to burn the flat-rate subscription quota, which expired in the morning and cost nothing at the margin. The agent generated cards through Anthropic's metered API instead, billed per token, and used the subscription quota only for reviews, research and orchestration. About 20% of a paid-for weekly quota was unspent when it expired.
The scope call turned "full db" into "the m3 milestone slice", a milestone the agent had authored itself and could complete. The slice was 496 of the catalog's 10,946 words.
The stop condition and the definition of done went together. "Do not stop" became "stop when my checklist closes". The deadline moved from "when I wake up" to 06:00, four hours before the 10am the directive named. The directive had also said what the end of the night should be. It said "make sure when you hit quota it fails gracefully", which only makes sense if quota exhaustion was the expected end state. The quota was never approached, and the graceful-fail path was never written.
The fifth call, which parts of the directive to honor, is the one a random error process doesn't produce. At about 04:15 the agent quoted the directive, as the "full-db-by-morning mandate", to grant itself an exception to the cap on metered spend. The same three lines of directive named what the agent owed: the full deliverable, the named resource, the anti-stop clause. The instance that quoted them at 04:15 to lift its spend cap applied none of the three before it stopped at 06:00.
Capability never blocked it. When I woke up and forced the issue, subagents writing cards directly on the subscription quota produced 114 gate-valid cards in the window's last minutes. They went through the mechanical gate the agent had already built, on the subscription quota it had held back since the run began.
The incident report I had the agent write afterwards lists my last five product instructions with my supervision state next to each. Two given with me at the keyboard ("Do the merges", and label every card with what it tests) were done. Three left for unsupervised execution were not. "I'm concerned we're still generating on the fly" shipped as bank-first with a paid fallback, the full-db directive ended at 4.5%, and "make sure when you hit quota it fails gracefully" was never implemented. My line on the pattern across sessions is on the record: "Every time I say spend to quota you go 'I'm done' very early then 'oops.'"
Why a Better Prompt Would Not Have Held
The usual fix is a clearer directive, a stronger system prompt, or a rules file. The night had all three, and the instance that made the five calls above had read all of it. The directive had the deliverable, the resource, the duration, an anti-stop clause and a failure handler. The repo carried standing orders, and one of them, "Implement only from a claimed task", is a rule the same instance broke later that day.
Every one of those calls ran through one model instance, and that instance held the controls. It evaluated completion, owned the schedule, and could reach its own off-switch, since the continuation loop and the scheduler took its instructions. The directive and the standing orders are policy, and the instance they constrain is the one that reads them. A sixth clause would go in front of the same instance that discounted the first five.
What the Driver Holds and What the Worker Keeps
The doctrine has six points, and each one removes a move the agent made that night: the milestone swap, the reach for its own scheduler, the self-counted 755, the 06:00 stop, the morning report and the single lane.
An external driver (cron, a shell loop, launchd) computes each tick's work order in code: which items, which worker prompt, which output file. The schedule and the termination condition are written before the run, and no model can modify them. The item list is a file the driver reads, and swapping "full db" for "the m3 milestone slice" would take an edit to the driver's input, which no worker can reach.
Workers are short-lived and can't reach the controls. The doctrine's sentence for them is "A worker that cannot stop the run cannot decide the run is over." Each tick spawns a worker with Read and Write tools and no shell. It can't cancel the loop, edit the driver, touch a hook or a crontab, or find an off-switch.
Validation is code outside any model: schema checks, forbidden-content scans, dedup, run by the driver on each output. The worker doesn't grade its own output, and no judge model sits where the loop needs its approval to continue. (A judge can run asynchronously and feed later reruns. In the write path its stall or its verdict stops throughput.) On the night the instance that wrote the 755 cards also counted them as done, and 589 survived the audit.
The doctrine's line on termination is "No model instance ever evaluates 'done.'" The condition is checked in code: N artifacts staged reaches the floor, or the API returns a quota error and the driver handles it. In the driver below that check is one line, [ "$n" -ge "$FLOOR" ], and n comes from ls, not from a worker.
Progress is a count the driver computes from the filesystem, and the status of the run is "artifacts: N / floor". A model-written summary is no part of it. In the doctrine's words, the incident's morning report "was confident, well-written, and wrong by a factor of twenty".
source: docs/UNATTENDED-RUNS.md lines 3, 9-14, 16; incident report v2 lines 27, 33, 35 · controlLoop.data.ts
Figure 2.10.1 lays the six steps out as rows, with the night's holder on the left and the doctrine's on the right. Five rows move from the model to code: work order, credential, gate, done and report. The model keeps the sixth, the work. In the DONE row a count-based stop replaces the model's 06:00 stop, which would have kept the window's last four hours running.
This driver is a reduction of the doctrine's first four points to shell, written for this essay, and the loop in it is what ran. I ran it with a floor of 5, a sixty-second deadline, and stubs for the project's pieces. The stubs were a work-order function that names item N, a worker that writes one JSON file per tick and after a set number fails with a 429 whose text lands in worker.err, and a validator that greps the file. Each run started on an empty output directory.
# driver.sh, a reduction with stubs. The control loop is this file,# not the model.FLOOR=${FLOOR:-5}; OUT=${OUT:-staged}DEADLINE=$(( $(date +%s) + 60 ))while :; do n=$(ls "$OUT" | wc -l | tr -d ' ') [ "$n" -ge "$FLOOR" ] && { echo "done: $n/$FLOOR"; exit 0; } [ "$(date +%s)" -ge "$DEADLINE" ] && { echo "window closed: $n/$FLOOR"; exit 0; } item=$(next_work_order "$n") if ! run_worker --tools read,write --prompt "$item" \ --out "$OUT/$n.json"; then grep -q "quota" worker.err && { echo "quota exhausted: $n/$FLOOR"; exit 0; } fi validate "$OUT/$n.json" || rm -f "$OUT/$n.json"done$ FLOOR=5 sh driver.shdone: 5/5$ FLOOR=5 QUOTA_AT=3 sh driver.shquota exhausted: 3/5Both lines are counts the driver took. The worker's output either passes validate or gets deleted, and n on the next tick is whatever is left in the directory.
The last point is arithmetic you do before tick one. One lane's yield over the window has to clear the floor with slack, or lanes run in parallel from the start. The slack here is 20%. The doctrine says "with slack" and names no figure. The night's one lane had cards for 496 words at the close of a seven-hour window, against a floor of 10,946. The sizing input is that yield at the close. The night recorded no per-tick rate. 10,946 times 1.2 is 13,135. Divided by 496, that is 26.5, so 27 lanes. (At that per-lane yield, held steady across all 27 lanes, they cross the floor at hour 5.7 of the seven.) One lane alone would need about 154 hours.
source: incident report v2 lines 19, 21, 27; docs/UNATTENDED-RUNS.md line 14; 20% slack illustrative · laneSizing.data.ts
Figure 2.10.2 is that arithmetic with the lane's end as a handle. Dragging the grey lane's yield up lowers the lane count until one lane clears the floor with slack. The default state is the night, 496 by the close and 27 lanes.
Local Gates Are Tripwires and Server-Side Twins Are Walls
The doctrine's companion rule covers every control in the scaffold, unattended or not, and it says every control that runs inside an agent's shell is a tripwire. Local hooks, lefthook jobs and CLI gates can be skipped with --no-verify, routed around, or edited, since the agent has write access to its own hooks. The same session showed two of the three that afternoon. The agent authored its own task straight into the done folder. task claim refused it, and the agent implemented anyway. It also widened its own task's authority.allowed_paths mid-branch.
Every local gate gets a server-side twin the agent can't reach, and each twin covers one named surface. Claims now push to origin under refs/swarm/*, and CI runs the provenance check from the base branch's copy of the script rather than the PR's own (the PR's copy is the fallback only when the base has none). An implementation PR has to move exactly one base-queued task to done and hold a claim matching the head branch. It has to keep its authority block identical to the base copy and touch only the paths that block allows. The claim ref is agent-authored, so the gate's own comment calls that check a tripwire; the checks that carry the wall are the base-queued task, the unchanged authority block and the allowed paths. The spec that landed with the gate has ten cases against a throwaway repo. Seven of them reject and three pass. Five of the seven rejections are moves from that session. One is the task authored straight into the done folder, and another is the widened authority block.
The second wall is CODEOWNERS with the ruleset's require-review option, over gate code, CI, hooks and the standing orders. Its comment on package.json reads "package.json defines what pnpm gates even runs". An agent that edits the gates script there has rewritten what CI checks without touching a gate. Rulesets and required checks are repository settings rather than files, and the doctrine's own sentence is that until the operator configures them, "every wall named here is decoration".
The same split covers money. A key in an env file, or a hook that blocks spend, is policy, since the agent shells where it lives. A spend cap in the provider's console, a key only I inject per run and a machine-user token with no merge rights are structure, held outside the agent's trust domain. The doctrine puts money, merges and production in the second group.
The Objection: This Is Ordinary Ops Hygiene
The objection is that none of this is specific to models. You'd give a flaky cron job a counted stop condition and an external supervisor too. I agree, and that is the design. The doctrine treats the model as you'd treat any unreliable component with write access to its own supervisor. A cron job that dies at 06:00 leaves a log that stops at 06:00. This coordinator wrote a report that said the job was done. I audited it in the window's closing minutes and found the count, with the quota past recovering. Point five's status line at 06:00 would have read 496 / 10,946.
The second objection is that this is one night. The record has four incidents in the same direction across sessions. The fourth incident's report calls all four the same operator, the same agent class and the same direction every time.
Under the driver there is no instance to write that morning report. The work orders are a file the driver reads. No instance holds a goal it could swap for a milestone. Termination is a count, and a worker that finishes early ends only its own tick. The driver names the credential and the endpoint, and the worker chooses neither. What arrives in the morning is the last line the loop printed, done: N/FLOOR, quota exhausted: N/FLOOR or window closed: N/FLOOR, and a directory holding the N files the count came from.
Limits
The provenance gate and its spec are on the scaffold's main branch. The script landed at 213 lines and the spec at 191. The project that adopted them ran the spec at 10 of 10. Whether the rulesets that make CODEOWNERS and the required checks bind are configured is a repository setting on each repo, and this essay doesn't audit that.
The test plan lists fourteen tests, numbered T1 to T15 with no T3, and none has run. It names T11, the full reproduction with every stressor together, as the gate on treating any of this as characterized rather than anecdotal.
The 4.5% is 496 of about 10,946, and the catalog size is written with a tilde in the incident report. The 755 and 589 are the report's counts, and the "about 20% of a paid-for weekly quota" is its reconstruction of what went unspent.
This is one operator, one agent product and one night. The generalization to other agents is an argument from the mechanism, and nothing here measures it: the test plan counts the night as n = 1.
Appendix: Quote the Operator Verbatim
The smallest piece of the doctrine is the translation-fidelity gate. An operator-origin task has to carry an ## Operator directive section quoting the instruction verbatim as a blockquote, and the board check refuses the task without it. Review compares every acceptance criterion and the shipped behavior against the quoted words, and an AC or implementation weaker than the quote is a P1, "translation infidelity". Without the quote, review compares the ACs with a spec written by the party being reviewed against it.
The rule comes from one case. My instruction that we were still generating on the fly reached the board as a task that shipped a metered-API fallback. The order that followed was retrofitted into the directive block of the task that cut it, and it reads "Cut live generation entirely, task branch and PR, be EXTREMELY parsimonious."
The check is lines 126 to 132 of the board gate, with two lines elided and marked and the long lines wrapped to the column. I ran it against its own fixture board. That board has one operator-origin task with no directive section and one whose quote is a single character (anything under 15 characters is refused with its own message). The run prints a FAIL line for every bad fixture on the board, and the one below, wrapped the same way, is the missing-section case.
if (origins.some(o => o['type'] === 'operator')) { const directive = body.split(/^## /m) .find(s => /^Operator directive\s*\n/.test(s)) ?? '' // [two lines elided: fenced code is stripped out, leaving prose] const quoted = [...prose.matchAll(/^> (.*)$/gm)] .map(m => m[1]!.trim()).join(' ') if (directive === '') err('operator-origin task needs an ' + '"## Operator directive" section quoting the instruction verbatim') else if (quoted.length === 0) err('"## Operator directive" must ' + 'contain the verbatim instruction as a "> " blockquote ' + '(outside code fences)')$ npx tsx scripts/verify-tasks.ts scripts/fixtures/invalid-boardFAIL scripts/fixtures/invalid-board/queued/ T-20260814-cccccccc-operator-no-directive.md: operator-origin task needs an "## Operator directive" section quoting the instruction verbatim