Skip to content

Course · Codex · Subagents

Light orchestration - gate every hand-off and merge worktree branches sequentially

Two diffs and a verdict are sitting outside main. The fan-out got the hardening done in parallel; nothing has come back. That is the debt the last two lessons kept flagging: fan-out does not divide your wall-clock time by the worker count, because the merge tail runs one gate at a time no matter how wide the fan-out was. This lesson pays the debt in counted turns.

Orchestration is the aggregation step, made real

Section titled “Orchestration is the aggregation step, made real”

Back in the fan-out lesson the rule was: write the aggregation first. Orchestration is that sentence turned into a procedure. For the audit it was cheap - concatenate the verdicts - because the workers produced data. The hardening produced code, and code does not reduce by concatenation: it reduces by merge, merges can conflict, and a green suite on one branch says nothing about the next one. So the reduce step grows teeth - a review gate per branch, and a merge order fixed in advance.

The order comes straight from that plan and the one dependency the projection built in: money/parse first, because money/tests asserts the new parse; reversed, main would sit red between merges. money/wire cleared its verification and carries no diff, so nothing of it merges - its verdict gets recorded and its worktree comes down. You can drive all of this from your main thread; as ever, the spawning is prompt-driven, and the plain git below is the durable, verifiable part.

Gate each hand-off with a read-only reviewer

Section titled “Gate each hand-off with a read-only reviewer”

The cheapest way for parallel work to go wrong is a worker believing it is done - a float left alive on a path it never exercised, a pin that asserts the old behavior. The fix is a separate set of eyes before anything merges: a reviewer. Here is how one is actually made, because the obvious tool is wrong: --sandbox is a session-launch flag - it sets the session you start, not any one child. Subagents inherit the parent’s sandbox by default.

A read-only reviewer next to writing workers is a custom agent file: a small TOML file under .codex/agents/ for project scope, or ~/.codex/agents/ for your own machine. It needs a name, a description, and developer instructions; the optional keys include sandbox_mode, which is where the fence gets pinned. Workers spawned without a file keep inheriting the coordinator.

# .codex/agents/reviewer.toml - illustrative shape
name = "reviewer"
description = "Reads a branch and returns a PASS/FAIL verdict against the repo rules; never edits."
developer_instructions = """
You review; you do not fix. Read the diff, check every hunk against
AGENTS.md and the parse-path rule, and return PASS or FAIL with
file:line evidence for a FAIL.
"""
sandbox_mode = "read-only"

Now correct the capability list, because read-only is narrower than it sounds. The reviewer can read every file on the branch, answer questions, and run no-write probes - that is what the fence admits. What it cannot reliably do is run the test suite: pytest writes .pytest_cache and bytecode caches, and a read-only fence is exactly the wrong shape for that. Under never there is no asking at all. So the reviewer reads and rules; running the suite stays with you.

That division is the gate’s design, not a limitation to route around. You, the coordinator, run a posture where pytest is ordinary - the budget-build profile from the approvals chapter lets in-workspace test runs flow - while the reviewer runs a deliberately narrower one. The verdict and the test run are different instruments, kept in different hands. Its only output is a verdict:

> Review the money/parse branch as the reviewer agent. Confirm the
amount parse converts decimal strings to integer cents with no float
anywhere on the path, per the AGENTS.md money rule and the parse-path
rule. Return PASS or FAIL with file:line for any FAIL. Do not edit,
do not merge, do not run the suite.

Two things make this a gate rather than a rubber stamp. The reviewer is a different context from the worker that wrote the change - it never talked itself into it. And its fence is pinned in its own agent file, so it cannot helpfully fix what it found and fold its opinion into the branch: a FAIL goes back to the writing worker, full stop.

Every branch pays one review turn before it goes near main, and those review turns run one at a time - one reviewer, one verdict, one branch.

Merge sequentially, and price the tail you were promised

Section titled “Merge sequentially, and price the tail you were promised”

It is tempting, once everything comes back PASS, to merge it all at once. Do not. Parallel editing was safe because worktrees isolated the files; parallel merging is not, because every merge after the first lands on a main the previous merge already changed. Bring the branches home one at a time, and re-test between each:

Terminal window
# start from the trunk of your local repo
git switch main
# slice 1: the fix, then prove main is still green
git merge --no-ff money/parse
pytest
# slice 2: the pin - after the fix by design; reversed, main sits red
git merge --no-ff money/tests
pytest
# money/wire carried a verdict, not a diff: record it,
# remove its worktree, nothing to merge
git worktree remove ../budgetcli-wire

The diff that came home in money/parse is one line plus an import - the entire real fix under all that machinery, the projection’s honesty in miniature:

# app/importers/csv.py, after money/parse
from decimal import Decimal
amount_cents = int(Decimal(row["amount"]) * 100)

Decimal reads the decimal string exactly, the multiply by 100 is exact too, and int() truncates nothing that was ever fractional. The pin from money/tests feeds 0.29 through the parser and asserts 29 - the truncation the float parse used to commit, finally impossible.

Now count what that cost, in the same turns instrument as ever. Three slices, three review turns - money/parse, money/tests, and money/wire’s verification read - then two merge-and-test turns for the two branches carrying diffs. Five turns, strictly one after another, because git merges into one main at a time and the pytest gate must see each merge in isolation to tell you which one broke something.

That is the number the fan-out lesson told you to remember. Fan the editing out to six workers and the parallel front gets no slower - but the tail grows toward twelve serial turns, six reviews plus six merge gates, because there are now twelve gates to clear one at a time. Reviews scale with slices; merge gates scale with diff-carrying slices; neither scales with anything parallel.

Fan-out buys you parallel work. It never buys you a parallel merge.

The pytest line between merges is the point beyond the count. The pin might pass against the fix and still disagree with something on the wire path - two independently correct changes can conflict where they meet. Testing after every merge means that when something breaks, you know exactly which merge broke it, because only one thing changed. Merge everything at once and a red suite tells you only that one of three is wrong.

If a merge conflicts, that is not the orchestration failing - it is the orchestration surfacing the one place two slices genuinely overlapped. Resolve it in your main thread, or hand the conflict to a worker with both hunks as its brief, re-run the gate, and continue down the list.

Once a branch is merged and the gate is green, its worktree has done its job. Tear down the way the worktrees lesson set up - git worktree remove per slice, git worktree prune for registrations whose directories are gone - so the next fan-out starts from a clean board.

Step back and look at the shape you just ran, because it is the same shape every time, regardless of the task:

  1. Decide the reduce step first - how the branches come back together, in one sentence, including the order.
  2. Fan out independent slices into isolated worktrees.
  3. Gate each hand-off with a read-only reviewer that returns a verdict, not an edit.
  4. Merge sequentially, re-testing between merges, so a break names its own cause.
  5. Tear down the worktrees so the next run starts clean.

That is light orchestration: no engine, no framework - a coordinator thread, a reviewer pinned by an agent file, and git used carefully. It scales from three branches to thirty without changing shape. Only the queue, and the serial turns it costs, get longer.

Score the chapter’s output honestly. The edits hold: git does not forget commits, the truncation pin feeds 0.29 through and asserts 29, and the parse path is float-free for good. The procedure does not hold on its own: nothing about “sweep, fan out, worktree, gate, merge, tear down” lives anywhere Codex reads by default. You just ran five deliberate steps by hand, and you would narrate the same five again for the next parallel job, and the time after that.

A procedure you repeat with the same steps every time is exactly the thing you should stop re-explaining and start packaging.

And the repo now holds a procedure begging for exactly that treatment. Every bank’s CSV lands in a different shape, and every import is the same sequence: read the format, extend the importer, add the pin, check the money rule.

State of the repo as this chapter closes (the course’s labeled continuation; the companion ships the pre-hardening state): the parse path in app/importers/csv.py converts decimal strings to integer cents through Decimal - no float anywhere on it, the rules chapter’s proposal made real code - and the suite is green at ten: nine from before, plus the new pin.

Those money paths are exactly the paths a later chapter puts a deterministic gate around, and replays end to end. The next chapter is Skills: what if a procedure you keep narrating could fire itself on recognition?