LEARN · PHASE 18 · LESSON 118

What Breaks in Production

You can name the five ways an unattended parallel run fails, check them in

Type
Learn
Prerequisites
Lesson 117, Review Against the Docs; Lesson 104, Failure
Time
~35 minutes

Learning Objectives

  • Name the five failure classes an unattended run fails into, and which one is the model
  • Run a triage in cheapest-cause-first order and stop before blaming an agent
  • Force four of these failures on purpose and record the observable signal each one leaves
  • Write a preflight that turns two of them from failures into something that can't happen

The Problem

A parallel run finished. Four agents reported success. Three of the four branches pointed at main.

Nothing was lost only because the worktrees hadn't been cleaned up yet — the work was still sitting in working trees that no branch referenced. The agents had done everything except the one thing that makes work real: commit. Isolation is not a backup (Lesson 110), and cleanup does not ask whether you were finished (Lesson 75).

Same repo, different week. A run came back with a red suite, so the agents took the blame — the fanciest-looking change must be the culprit. The actual cause was a wrong glob in package.json: the test command pointed one directory wide. Every agent's work was fine. The witness was lying, and nobody checked the witness because blaming an agent is more satisfying than reading a config line.

Two failures with the same lesson underneath: the model was not the broken part either time. It almost never is.


The Concept

Unattended runs fail in five classes. Exactly one of them is "the model misbehaved," and it is the last one you check — because it is the most expensive to check and the least likely to be true.

flowchart TD
    R["A run went wrong"] --> A{"1. Is the work<br/>saved?"}
    A -->|no| A1["Empty branch.<br/>Branch at main.<br/>Work only in a worktree"]
    A -->|yes| B{"2. Is the witness<br/>honest?"}
    B -->|no| B1["Wrong glob, stale build,<br/>wrong cwd, swallowed exit code"]
    B -->|yes| C{"3. Did isolation hold —<br/>as files and as meaning?"}
    C -->|no| C1["Two agents, one resource:<br/>a config key, an index,<br/>a version, an interface"]
    C -->|yes| D{"4. Does the environment<br/>match the assumption?"}
    D -->|no| D1["Startup-cache trap.<br/>Session socket closed.<br/>Interactive-only behaviour"]
    D -->|yes| E["5. Now suspect the model —<br/>and check the input<br/>before the intelligence"]
Mermaid source — renders as a diagram where mermaid.js is available

1. The transaction boundary — work that was never saved

A change in a working tree is not saved. It is present. git status will show it until a clean, a reset, or a worktree cleanup removes it without ceremony.

The tell is a branch that exists and points at main, or a git log with fewer commits than agents you spawned. The cheapest check in the whole lesson:

git branch --contains <sha>     # names the branch holding this commit, or nothing
git worktree list               # every isolated copy, and the branch it holds

If an agent's transcript says "committed" and the sha is on no branch, the answer is almost never "it lied." It is "the commit went into a worktree whose branch you never merged, and the branch still says main."

2. The witness — the thing reporting on the work

Your verifier is a system, and systems lie in ordinary ways: a glob that matches one directory wide, a build artifact from before the change, a command run from the wrong cwd, a pipeline where a failure exits 0 because something swallowed the code. The red suite that "proved" the agents failed was this class.

The rule: check the witness before the work. Run the suite on the last known good commit. If it is red there too, the agents are exonerated in one command.

3. Isolation — files are separated, meaning is not

Two worktrees cannot touch each other's files. They can absolutely both bump the same version string, both add an entry to the same index array, both rename the same exported function, or both assume opposite shapes for the same interface. Each branch is green; the merge is red.

Worktrees solve the file problem. The graph's blocking edges solve the meaning problem (Lessons 100, 101, 104) — and only if the edges came from data flow. Two nodes that "look independent" because they live in different folders are the false frontier from Lesson 116, and their failure arrives after the merge, when it is most expensive to unpick.

4. The environment — where the harness and reality disagree

Two documented shapes, both invisible until they bite:

  • The startup-cache trap. The harness establishes things about its working directory once, at launch. Create a git repository after the session starts and worktree tools refuse with "not in a git repository" while git rev-parse cheerfully prints a path. The fix is to restart in an existing repo — the session cannot learn about a repo that appeared beneath it (Lesson 108).
  • A session-scoped transport. Cross-session messaging belongs to a session; a closed session receives nothing. A worker that "should have been told" was told, to a socket nobody was reading. This repo asserted the opposite once and was wrong. The default is: the session is the lifetime.

Anything a step assumes about its environment — a branch that exists, a file another node wrote, a TTY, a credential in the shell — is this class, and it fails as "nothing happened," which is the quietest failure there is.

5. The model — last, and not alone

By the time you get here you have ruled out four cheaper causes, and the remaining possibility is still two things: the model, and what the model was given. A confident wrong answer from a well-fed agent is rare. A confident wrong answer from an agent handed a stale CLAUDE.md, a truncated file read, or a task with no acceptance check is routine — and that is a harness problem wearing the model's face.

The order is the lesson

Cheapest first, and most-likely first, which here happen to agree. "Is it saved?" costs one command. "Is the witness honest?" costs one run. Suspicion of the model costs a transcript read and probably a re-run. Doing them out of order is how a five-minute problem becomes a day of re-running agents that were right.

Retries hide the first failure

An unattended loop that retries will produce a green run after a red one, and the green one is not a pass. It is a pass plus an unexplained earlier failure. Until you can name what changed between the two, you have a flaky system, not a fixed one — and Lesson 95's verification loops are where that gets designed properly.

The reframe: in production the model is the last suspect, not the first —

what breaks is the seam between two systems, and no seam belongs to either side.


Build It

You cannot debug a failure mode you have never seen. Force four of them.

1. Seed the empty branch

In a scratch repo, spawn or write a change in a worktree, then remove it without merging. Then run the two commands above.

✅ You pass when you can show a worktree whose work is reachable from no branch, and you know which command would have caught it before the cleanup.

2. Break the witness

Make the suite pass on code you know is broken. Change a test glob, or edit a source file without rebuilding, or put a || true in a pipeline.

# run the suite on the last known good commit
git stash list
git log --oneline -3

✅ You pass when the suite reports green on broken code, and you can point at the single line that made it lie.

3. Seed a semantic collision

Give two parallel nodes a shared resource — the same config key, the same index array, the same exported name. Let both pass on their own branch. Merge.

✅ You pass when the merged branch fails while both branches pass, and you can name the resource they shared in one sentence.

4. Leave the environment stale

Start a session in a plain directory, create a repository inside it, and try a worktree operation. Record the exact message.

✅ You pass when you have the message, and can say which single action — taken before the session started — would have avoided it.

5. Write the runbook

Turn the five classes into a triage sheet: symptom, first check, the command, what a pass looks like. Two rows must be a preflight — things you run before spawning, so the failure cannot occur at all.

✅ You pass when every row has a runnable command, and at least two rows are preflight rather than triage.

6. Have someone else use it

Hand the runbook to a person (or a fresh agent session) who did not write it, seed one failure at random, and watch.

✅ You pass when they resolve it without asking you a question, and you can name the row that did the work.


Use It

ThingWhen you reach for it
git branch --contains <sha>First command after any "the agent says it finished"
git worktree listBefore cleanup. Every isolated copy, every branch
Known-good commitRun the witness there before blaming anyone
git log --oneline --graph --allWhen the shape of history doesn't match the number of agents
Blocking edgesThe fix for a semantic collision — after the fact, add the edge you skipped
Startup-cache trapAny "not in a git repository" that contradicts git rev-parse
Session-scoped transportAny message a closed session "should have received"
Unchanged re-runTreat as unresolved until you can name what differed
PreflightTwo rows of the runbook that stop failures happening instead of explaining them

Exercises

  1. Take a parallel run of yours — any size — and run the four triage checks in order against its last red result. Write down which class it was. ✅ You pass when you can name the class, the command that revealed it, and the command that would not have revealed it.
  1. Reproduce the empty-branch failure in a scratch repo, then write the one-line preflight that makes it impossible. ✅ You pass when the failure is reproducible on demand and the preflight is a command, not a rule you remember.
  1. Break your verifier so it passes on broken code, then fix it, keeping the evidence of both states. ✅ You pass when you can show the same commit passing and failing under two different verifier configurations, and you can say which one you trust.
  1. Re-run a previously-failed unattended job. If it now passes, write down what changed — or state plainly that you cannot, and treat the pass as unresolved. ✅ You pass when the outcome is one of "named the change" or "flagged as unexplained," and not "it works now."

Key Terms

TermWhat people sayWhat it actually means
empty branch"the agent didn't do anything"A branch that exists and points at main while the work sits uncommitted in a worktree.
the witness"the test suite"Whatever reports on the work. Check it on known-good code first — it lies in mundane ways.
transaction boundary"it's saved"A working tree is not a commit. Until it's committed, cleanup can and will remove it.
semantic collision"they were in different folders"Two nodes that share a resource (key, index, name, interface) while sharing no file. Green apart, red merged.
startup-cache trap"it says not a git repository"The harness probed the working directory once, at launch. A repo created later is invisible to it.
session-scoped transport"the worker should have got the message"Cross-session messaging lives and dies with the session. A closed session receives nothing.
retry masking"it's green now"A pass after a failure with no explanation for the difference. Unresolved, not fixed.
preflight"running the tests first"A check that runs before the work starts, turning a failure class into something that cannot happen.

What this lesson skipped

Cost and rate limits. A fan-out that dies at call forty is a capacity problem, not a correctness one — Lesson 11 for the shape, Lesson 81 for the runtime caps.

Security. Sandbox escapes and prompt injection are their own failure class and their own lesson (Lesson 114); this lesson assumes the permission boundary held.

Fleet and organizational failure. Settings drift across machines, plugin policy, central telemetry — Lesson 115. Everything here happens on one machine, inside one run.

Designing the verification loop itself. Retries, escalation, and what "unresolved" does to a run is Lesson 95. This lesson is what to check when that loop has already given you an answer you don't trust.

Check yourself

3 questions. Answer before you reveal — recognising the right option is not the same as being able to produce it.

  1. Four agents finished. All four reported success. The merged branch fails the suite. Which order do you check in, and why?

    1. Read each agent's transcript and find which one produced the bad change
    2. Is the work saved, is the witness honest, did isolation hold, does the environment match — then the model
    3. Re-run the whole workflow, since a fresh run usually clears it
    4. Revert the merge and redo the nodes serially, so the culprit can't hide
    Show answer

    B. Is the work saved, is the witness honest, did isolation hold, does the environment match — then the model

    Each step is cheaper and more likely than the next: 'is it committed' is one command, 'is the verifier lying' is one run, and blaming a model costs a transcript read plus a re-run. A is the tempting one because a red suite looks like a culprit, and it is exactly the mistake that had the agents taking blame for a wrong glob in package.json. C burns the whole run to avoid a five-minute check, and will reproduce the same failure. D serializes work before establishing that parallelism caused anything — if the witness is lying, serial runs are just slower and equally red.

  2. An agent's transcript says it committed its work. git branch --contains <sha> returns nothing, and feat-node-b sits at the same commit as main. What is the most likely state, and the cheapest check?

    1. The agent lied about committing — re-run the node
    2. The commit is in that worktree's working tree but was never made, or the branch label never moved — check git -C <worktree> status and its log
    3. The commit was garbage-collected — recover it from the reflog of the main working copy
    4. The sha was mistyped — trust the transcript and move on
    Show answer

    B. The commit is in that worktree's working tree but was never made, or the branch label never moved — check git -C <worktree> status and its log

    The empty branch is the classic shape: the code exists in an isolated working tree that no branch references, so it is one cleanup away from gone. Look inside the worktree before you look anywhere else — status and log there settle it in one command, and the fix is a commit of work that already exists. A is the tempting reading of a mismatch, and 'the model is lying' is both insulting and expensive: a missing commit is almost always a missing step, not a lie. C sends you hunting in the wrong repository's object store; work never committed has nothing in any reflog. D stops looking at the exact moment the trail gets warm.

  3. Two nodes ran in parallel. Each branch passes the suite on its own. The merged branch fails, reproducibly, and rerunning the suite does not change it. What is the first hypothesis?

    1. One agent wrote a bug that its own tests happened not to cover
    2. The two nodes share a resource neither owns — a config key, an index, an exported name, an interface — so they were never independent
    3. The suite is flaky under parallel load
    4. The merge tool resolved a conflict incorrectly
    Show answer

    B. The two nodes share a resource neither owns — a config key, an index, an exported name, an interface — so they were never independent

    Green-apart and red-together is the signature of a semantic collision: the two changes are self-consistent and mutually inconsistent, and the disagreement exists only on the merged branch. The fix is an edge you did not draw, then re-running the dependent node. A is tempting because a failure feels like a culprit, but an individual bug would show up on that node's own branch, where the suite is green. C is ruled out by the reproducibility — flakiness comes and goes. D would normally leave the merge conflicted; here the merge is clean and the semantics are not, which is why a clean merge is not evidence that two nodes were independent.