Lesson 6: Hands-On: Upgrading Your Harness into a Small Graph
Learning goals:
- Weld routing, fan-out, merge, review loop, and reporting from the first five lessons into one
orchestrate.mjs: the plan lives in code, each node still runs the stop_reason loop from Course 7 (Agent Harness Fundamentals: Loops and Control), intermediate results stay in script variables
- Get the review loop actually spinning, and watch both ways it can stop—one ticket fixed per gate report and done, another returning identical reports two rounds in a row, judged no further progress, flagged needs_human
- Persist the entire graph's execution trace into
run-state.json and run.jsonl, then reconcile against the real-run summary table: which node spent how long, how many model calls, how many tokens, how many gate rounds
Prerequisites: Completed Lessons 1–5, able to run the harness loop from Course 7 (Agent Harness Fundamentals: Loops and Control) | Previous: << Lesson 5
First, see it run
The first five lessons pulled the parts apart: who holds the plan (Lesson 1), chaining and routing (Lesson 2), sectioning and voting plus a bounded concurrency pool (Lesson 3), orchestrator-workers and the four elements of delegation prompts (Lesson 4), the review loop and how to compose these patterns into what Lesson 5 calls a "graph" (Lesson 5). This lesson welds them into one file.
The task is deliberately mundane: inbox/ holds six customer support tickets, and the job is to write a reply for each one that can be sent as-is. First, what it looks like when it finishes:
Every terminal output in this lesson comes from this script's actual runs, copied line by line—not one line is a hand-typed example. Two things change each run: the millisecond timings, and the run_id (it's a base-36 timestamp). Everything else—classification results, call counts, token numbers, gate round counts, which ticket is needs_human—is a pinned constant. The reason is explained later in the "Verification Setup" section.
Worth staring at first is that final 1. This isn't an error—it's a verdict: six tickets, one couldn't finish automatically, so the exit code isn't 0. Every run of this graph produces a conclusion that CI or a cron job can parse, not just a pile of logs.
What the graph looks like: the plan is those dozen lines in main()
Start with the script's skeleton. Using "graph" and "node" is the vocabulary Lesson 5 introduced—this is our own visual system, not an official concept, and it rests on exactly one primary anchor: the workflow script itself holds the loop, branching, and intermediate results1. The snippet below is the literal implementation of that statement:
Lesson 5 drew a composed graph first; this graph is a variant of it, with three differences: Lesson 5 split by difficulty into "simple / complex," here we split by topic into billing / bug / other; Lesson 5's fan-out was "one complex ticket dispatched to three workers then merged," here it's sectioning—"six tickets each assigned one handler"; Lesson 5's back-edge returned to a separate [Draft] node, here it returns to the original worker. Why these changes are all collected in the "Reconciliation Table" section at the end.
routed, drafts, items—these three const declarations are the entire graph's state. They're plain JavaScript variables, not some typed state objects, and there's no merge strategy—intermediate results stay in script variables1, nodes pass data via function return values. No model sees the full picture: the routing model sees only six ticket texts, the billing worker sees only its assigned ticket, the review gate sees only one reply file.
This is what the workflow vs. agent architectural distinction looks like in code: LLMs and tools are orchestrated through predefined code paths2, not the model autonomously directing its own processes2.
Five nodes, each handling one segment:
Only two of the five nodes actually call models. Not every node has to be a model—this is the lesson's cheapest and most easily overlooked rule: merge and report are pure functions, the other category in fanout uses a string template, review's first filter is a few lines of includes. Anywhere deterministic code can give the same answer, there's no reason to pay the cost and latency of a model call.
Inside nodes: still the loop from Course 7
Pin down the innermost layer first, then the graph makes sense. Each model node internally runs the stop_reason loop from Course 7 (Agent Harness Fundamentals: Loops and Control), unchanged:
The four steps in the loop body—push assistant, execute tools, push tool_result, reassign response—are word-for-word identical to Lesson 6 of Course 7, even the comments are copied. Valve 1 (max turns) is in the original position: at the loop body's start, before turns++. Leaving a max iteration count as a stop condition for loops is standard practice for keeping control2.
Compared to Course 7, two changes, both outside the loop body: client and system changed from module-level constants to parameters (three roles need different stubs and different system prompts, must be passed in); token and call counting moved from inside the loop body to a wrapper layer outside the client, the loop interior unchanged:
This change has a cost and it needs to be stated: Course 7's Valve 2 (token budget) originally relied on the accumulated value in the loop body; that accumulator isn't in the loop anymore, so Valve 2 didn't make the move either. In this graph, each node's stub response queue is fixed-length, exhausting the queue will throw directly, can't run away; but when you swap stubs for a real client, put Valve 2 back—either have metered throw when over budget, or move counting back into the loop body and restore Course 7's original form. Valve 3 (spin detection) and Valve 4 (human approval) likewise didn't move; the reason is listed in the "Reconciliation Table" section later.
The tool half is also copied: one turn's response contains multiple tool_use blocks, return that many tool_result blocks, a tool throws and it's wrapped into is_error: true and passed back to the model, rather than crashing the entire process.
Node one: Routing—one cheap call, then tighten the output
Routing classifies an input and directs it to specialized follow-up tasks2. It's the graph's cheapest model call: one request classifies all six, no tools, no reply writing.
The key is the middle ten lines, not the model call. The model returns free text, every downstream branch depends on this value, so it must be tightened into one of three legal labels before entering downstream: lines not matching the format are discarded; categories not in the whitelist fall to other; tickets without even one matched line, parsed.get(t.id) ?? "other" catches.
I deliberately had the stub return "投诉" (a Chinese word) for the last ticket—not in the whitelist. The real-run log shows this tightening:
The model gave a self-invented label, code clamped it back to other, and left a record saying what was clamped. Downstream branches only recognize values code has vetted—this is the practical difference between a routing node and "letting the model directly decide where to jump next," and it's why routing can be unit-tested.
Node two: Fan-out—three workers and a bounded concurrency pool
Fan-out follows sectioning: breaking the task into mutually independent subtasks and running them in parallel2. Here "independent" is natural—the six tickets have zero dependencies on each other, order doesn't matter.
Three categories, three handlers, only two are models:
The concurrency pool is Lesson 3's pool (called pool there, runPool here): tasks sit behind a cursor, spawn limit consumers to grab them, done when exhausted. The ceiling genuinely works, not decorative. Adjust it to 1 and run again, the fanout line's timing will noticeably lengthen (call count and tokens identical, milliseconds will jitter as usual):
494ms vs. 247ms, call count and tokens identical. Concurrency buys wall-clock time, not less work—this remains true after switching to a real API, except then you also need to consider the provider's rate limits, making the ceiling even more essential.
Delegation prompts: all four elements present
All three model roles' prompts follow Lesson 4's four elements: objective, output format, tool guidance, task boundaries. Subagents need an objective, an output format, guidance on tools and sources, and clear task boundaries; without adequate description, workers duplicate work, leave gaps, or fail to find what they should3. The billing worker:
Four lines, each doing its job: objective determines what it writes; output format gives downstream gate something to check (the "start with ticket ID" requirement directly maps to the gate's first rule); tool guidance nails down "where amounts come from" to lookup_order, blocking the path of inventing numbers from ticket descriptions; task boundaries both block out-of-scope actions and preemptively ban filler words.
The bug worker's version swaps content to checking known-issues DB, citing issue numbers, forbidding self-invented numbers; the router's "tool guidance" says "this step gives you no tools, judge solely from ticket text," matching the empty tools array passed in code. These three prompts' differences are themselves routing's payoff: after classification, each writes its own, no need to cram three kinds of work's requirements into one prompt—this is precisely the separation of concerns and more specialized prompts routing enables2.
Node three: Merge—pass references, not payloads
merge is pure code, zero model calls. It does two things: write each draft to out/, then collect a lightweight manifest for downstream—{id, category, handler, file, oneLine}, one file path plus a one-line summary, not six full replies. (Simultaneously it also creates a record for each ticket in run-state.json, fields shown in the complete code's Section 9.)
This brings the multi-agent system engineering advice into a single-process script: have specialized agents store outputs in external systems, pass only lightweight references back to the coordinator3. In that retrospective, this advice solved context bloat from "everything relayed via the lead agent"; here it solves the small-scale version of the same thing—the review node needs "which file should be checked," not all six full texts piled in one variable passed around.
So the review node's first action is re-reading content from file:
This step looks redundant—everything's in the same process anyway, just pass the string directly. But it buys two things: the file in out/ becomes this ticket's sole source of truth, whoever edits it that's what review checks; and once this edge needs to go cross-process or cross-machine, only readFileSync this one line changes, the node-to-node contract doesn't budge.
A quiz
By this point, three of the graph's five nodes are complete: routing is code-tightened, merge is pure code, and the upcoming gate will also be pure code. The most commonly heard question at this juncture can be posed directly.
Node four: Review loop—gate filters first, failures go back to the furnace
The review node does check-fix-recheck: run a checker, fix what failed, repeat until it passes or stops making progress1. It's the only place in this graph where "a model's output gets sent back for rewriting."
The first filter is deterministic, a few lines of includes and done:
Two rules, both the kind Course 10 (Verification and Quality Assurance: Don't Let 'Looks Right' Slip Through) said "if it can be determined deterministically, don't ask a judge": the reply must contain the ticket ID (customer support systems key on it), must not contain filler like "please wait," "thank you for your patience," or "we'll handle it soon" with no information content. Both don't require semantic understanding, string inclusion suffices, result same every time, and it conveniently produces a report string that can be fed directly back to the worker.
The LLM judge here could do "is the reply's tone appropriate," "do the facts exceed what tools returned"—things truly unjudgeable via includes. But it must rank after the gate: gate is free and deterministic, let it filter out clear issues first, what's left is worth spending a call to consult a judge. This graph only installed the gate layer, because this batch of tickets' acceptance criteria happen to be expressible as rules; when acceptance criteria include words like "tone appropriateness," add the judge layer per Course 10's tiered judgment allocation.
The loop itself looks like this:
Three break statements correspond to three ways to stop, matching what Lesson 5 declared: pass (while condition naturally false), no further progress, hit max rounds. The third if is a patch—other category replies are pure code template generated, no worker to send back to, if the template itself is broken, only option is direct handoff. This run didn't hit it (template is constant, must pass gate), it's kept because if someone corrupts the template string, I'd rather see a no_rewriter record than a spinning loop.
What gets fed back to the worker on recycle is straightforward: previous version full text + gate report + one sentence "only fix issues named in report, rewrite complete reply" (assembled in callWorker).
Both ways to stop actually happened in this run
I planted two scripts in the stubs, making each exit of the loop execute once.
T-1005: Fixed correctly, done. Bug worker's first version forgot ticket ID (first rule fails), gate returns missing_ticket_id, worker adds the opening line per report, second version passes:
T-1004: Revised, but not fixed, loop stopped itself. Billing worker's first version wrote "please wait," gate returns filler_word:稍等; worker rewrote a version, sentence entirely different, longer, added an explanation, but that word remains. Second round's report identical to first round:
At this moment gate.report === lastReport holds, loop judges no further progress, stops, marks this ticket needs_human. It originally had two more rounds of budget (MAX_REVIEW_ROUNDS is 3), but spending them would be wasted—same report fed back, most likely same reply returns. The "no further progress" exit's value is here: it cuts losses earlier than max rounds, and it gives an informative conclusion—not "tried three times still fails," but "it doesn't understand this feedback," which is precisely the signal to escalate to human.
The difference between the two exits in data is immediately visible:
Both tickets' gate_rounds are 1, round count alone can't tell success from failure; the dividing line is gate_reports length—it records every failed report, including the last one that caused the stop. T-1005 leaves only one entry (second version passed, no second report), T-1004 leaves two with identical content, stop field writes the conclusion directly as no_progress.
Node five: Reporting and tracing
The final node is also pure code: print state.nodes and per-ticket breakdown as two tables, count needs_human, determine exit code. All passed is 0, one needs human is 1.
Tracing splits into two files, each with its own purpose. run.jsonl is Course 11 (Observability and Debugging: Seeing Every Step Your Agent Takes) structured logging, one JSON event per line, each carrying ts and run_id, grep-able after the fact—this run totaled 39 lines, the excerpts in earlier sections are all grepped from it verbatim.
run-state.json records execution trace (distinct from "graph's state = those few script variables"), written per Course 9 (State Management and Persistence: Making Long Tasks Survive Interruption) style: write .tmp first, then rename atomic swap, killed at any moment, on disk is either the previous complete state or the new complete state, never half a JSON:
Write timing is "persist after each small step": after each node completes persist once, inside review node after each ticket's judgment persist again. The reason Lesson 5 quoted—incrementally tracking each agent's result is precisely the premise for recovering a run within the same session1; a workflow that fans work out across many small agents preserves more progress than one long agent1. This graph isn't a multi-agent runtime, but the same statement holds here: six tickets are six independent progress units, dying mid-review, already-persisted ones shouldn't disappear with it (fan-out phase hasn't achieved this yet—see Reconciliation Table item 3).
To see this statement's actual effect, use STOP_AFTER=merge to stop the process after fan-out, before review:
run-state.json at this moment (excerpt):
Three nodes' accounts are in, all six tickets' category, handler, output file paths are in, six draft files already persisted in out/. Only review segment lost: all tickets stuck at status: "drafted", stop: null. This state is sufficient to support a resume—read drafts back from out/, start directly from review node. Notice T-1005's one_line happens to expose the draft's flaw: opening lacks ticket ID. Review hasn't run, so this flaw hasn't been caught yet.
(STOP_AFTER only recognizes merge as the one value, it's Course 9's controlled crash point's simplified version: exit code 0 all pass, 1 has tickets for human, 2 stopped early no verdict, 3 is script itself crashed—four codes non-overlapping, CI can tell "ran but some need handoff" from "crashed" at a glance.)
Complete orchestrate.mjs
Below is the full text, one continuous block, copy-paste it into orchestrate.mjs in an empty directory then node orchestrate.mjs. Zero dependencies, no need for npm i, no need for package.json (.mjs suffix already declares it's an ES module), and no need for API key—model client is a stub. First run will create inbox/, kb/, out/ and write those six tickets.
Six hundred seventy-nine lines total, of which roughly one hundred ninety lines are data fed to stubs (SCRIPTS table, six ticket original texts, known-issues DB, stub client), the actual orchestration logic—five nodes, concurrency pool, gate, and entry point—roughly two hundred fifty lines, another forty or so for observability and state tracing. This scale is deliberate: one loop plus a few patterns is genuinely something implementable in a few lines of code2.
Verification setup
Every terminal output in this lesson came from this script's actual runs, not by "run multiple times and pick a good-looking one," but by pinning two sources of non-determinism in advance.
Swap model for a stub that plays a fixed queue. SCRIPTS is a table, key is "ticket id + which version," value is a pre-written response sequence; each messages.create call spits the next one in order, queue exhausted and still calling throws directly. This way "which ticket calls which tool in which round, when model finishes" are all constants. The stub also left an assertion: create must carry model and max_tokens, missing one throws—real client requires these two parameters, stub won't cover for you, so you don't discover the gap the day you swap to real client. This method used from Course 8's hands-on all the way here, so the object being verified is your control logic, not the model's performance that day (real models are non-deterministic, same input can still give different responses4).
The stub also adds a fixed 60ms delay, replacing real network round-trip. Without it every node would be 0ms, concurrency pool's effect wouldn't show in the summary table at all—the POOL_SIZE=1 comparison above (494ms vs. 247ms) relies on it.
Two loop scripts planted in stubs. The review loop needs to genuinely spin, which requires something genuinely failing gate. So:
T-1005#1 (bug worker's first version) deliberately omits ticket ID, triggers missing_ticket_id; T-1005#2 adds the opening line, second version passes—this demonstrates the "check-fix-recheck" normal completion exit.
T-1004#1 and T-1004#2 (billing worker's two versions) both carry "please wait." Two versions' sentences entirely different, lengths different, but gate looks for whether that word is present, so two rounds' report strings identical, triggers "no further progress"—this demonstrates the loss-cutting exit.
The two scripts' writing has craft: not making the second version verbatim repeat the first (that way even humans see it's a dead loop), but making it "revised, but not fixed correctly." This is the most common failure mode in real loops, and precisely what the "two consecutive rounds identical report" criterion catches.
Controlled early stop. STOP_AFTER=merge stops the process after fan-out, before review, exit code 2. It's Course 9's CRASH_AFTER simplified version: making "interrupt at which step" a precisely specifiable parameter, rather than relying on luck to hit it. The above drafted state run-state.json came from this run.
Reconciliation table: This graph owes debts to earlier lessons, clear them line by line
A course reaching its capstone hands-on, the easiest mistake is quietly overturning rules established earlier. So reconcile line by line here, discrepancies written explicitly.
1. Loop body matches Course 7. The four steps in loop body—push assistant, execute tools, push tool_result, reassign response—word-for-word identical to Lesson 6 of Course 7, even comments unchanged. Valve 1 also in original position. Declared differences: runAgent's signature added client and system two parameters (three roles need different stubs and different system prompts), create call added a system field; token metering moved from loop body to metered wrapper, so Course 7's Valve 2 (token budget) didn't follow, Valve 3 (spin detection) and Valve 4 (human approval) also didn't move—this graph's tools are only read-file and lookup-order, both read-only operations, no high-impact actions needing approval; stub queue finite, can't spin away. Before connecting real API, these three valves must be installed back.
2. Delegation prompts four elements complete (Lesson 4). Three prompts—router, billing worker, bug worker—each wrote full objective, output format, tool guidance, task boundaries four sections, one line each, can line-by-line compare3.
3. Concurrency pool has ceiling, merge passes references not payloads (Lesson 3). runPool's limit is hard ceiling, POOL_SIZE=1 vs. POOL_SIZE=2 timing difference already verified. merge onwards passes downstream {id, category, handler, file, oneLine}, full text stays in out/, review node reads back from file itself3. Declared difference: Lesson 3's pool was "same batch of subtasks run in parallel," here the pool spans three handler types—two model workers plus one pure code template, template's entry into pool costs almost no time. Pool's semantics unchanged (in-flight task count doesn't exceed ceiling), just tasks themselves heterogeneous. Also one thing Lesson 3 established but here omitted for script brevity: Lesson 3 required each lane separate try/catch, letting single-lane failure not drag down whole batch, runPool lacks this wrapping—cost is fan-out phase any one lane throws, whole batch of drafts won't persist. Before connecting real API must add, real network single-lane timeout is normal.
4. Gate before judge, loop stop conditions match Lesson 5. First filter is deterministic code, not model; this lesson didn't install LLM judge layer, because this batch of tickets' acceptance criteria happen to be expressible as rules, installing it would be wasted money—Course 10's tiered judgment is this order: deterministically judgeable judge first, what's left consult judge. Loop stop conditions three kinds: pass, no further progress, hit max rounds1 2, Chinese concepts one-to-one match Lesson 5. But field and value names changed: Lesson 5 landed in reason field, values passed/no-progress/max-rounds, here lands in stop field, values gate_pass/no_progress/max_rounds (criterion swapped from judge to gate, hyphen also changed to underscore per this lesson's snake_case convention); additionally Lesson 5's rounds counts generation times, draft counts as round 1, this lesson's gate_rounds counts rewrite times, draft is round 0, so same ticket, two lessons' round count starting points differ by one. Declared difference: code has fourth exit no_rewriter (pure code template has no worker to send back). This isn't a pattern Lesson 5 omitted, it's this graph's specific situation—Lesson 5's loop preset "producer is a model," here one category of producers is template. This run didn't hit this branch.
5. "Graph" phrasing matches Lesson 5's declaration. Full text's "graph" and "node" are both this lesson's own engineering metaphor, Lesson 5 already explicitly stated when introducing this visual system, it's not any primary material's official concept; the primary anchor it can stand on is only that one: the workflow script itself holds the loop, branching, and intermediate results1. This lesson didn't add any new terminology—"state machine," "state objects passed between nodes" none used; Lesson 5 defined "edge" (whose output feeds to whom) only appeared once when explaining the merge → review data flow, not new vocabulary. routed / drafts / items are just three ordinary local variables.
6. run-state.json atomic write matches Course 9. Write .tmp first, then renameSync swap, not one step missing. Write timing also per that course's caliber: persist once after each small step completes, not once after full run completes.
7. Observability caliber same form as Course 11, but coarser grain. One JSON event per line, each carrying ts and run_id, grep-able after the fact. Four differences: (a) Course 11's logger records content summary (shape, length, first few chars), this lesson only records id, category, filename, report string and counts, doesn't record reply full text—full text already in out/; (b) association field Course 11 calls trace_id, here called run_id; (c) that course's core is using span_id/parent_id to串成 a trace tree, this graph although node→worker→tool three-layer nested, didn't implement parent-child linking, so no trace tree; (d) initLog() each run clears run.jsonl, only keeps most recent run, to do Course 11's cross-run comparison (v-good vs. v-bug), must change to append by run_id separate files. To connect this graph into real trace system, Course 11's span fields need to be added following that pattern.
8. Orchestrator-workers pattern, this lesson intentionally didn't implement (Lesson 4). Lesson 4's orchestrator-workers, key is "dispatch how many, each does what" decided by model watching input on the fly; this graph isn't—how six tickets classify, each category goes which worker, locked into CATEGORIES and three constant prompts before writing first line of code. This is precisely Lesson 4's "can predefine then don't make dynamic" direct application: this batch of work's shape is known, shouldn't hand decision authority back to model. So strictly speaking, welded into this file are four patterns (chaining, routing, parallelization-sectioning, review loop), voting supplements fifth in Level 2 exercise, orchestrator-workers is the one barred by this batch of tasks' nature.
Boundaries
This graph manages something small: one process, one batch of tickets, runs and exits. It's worth building because the five steps "tickets arrive → classify → handle by category → check → report" were locked down before writing the first line of code. If the task becomes "figure out what this customer actually encountered over the past six months, how many steps needed you judge yourself," then this graph is the wrong architecture—that kind of open-ended problem where you can't predict steps in advance, can't hardcode a fixed path, inherently belongs to an autonomous loop2.
Several boundaries, state them explicitly:
Fan-out is synchronous, will hurt at scale. The pool in fanoutNode must wait for the entire batch to complete before entering merge. This is precisely the bottleneck that real production system acknowledged: synchronous execution simplifies coordination, but creates bottlenecks in the information flow—one subagent taking forever, entire system stuck waiting3. Six tickets, each at most two calls, this bottleneck doesn't hurt at all; six hundred tickets, each ten calls, it becomes "slowest one determines whole batch's wall-clock time." Whether to change to asynchronous, must calculate the cost: async lets agents work concurrently, spin up new ones on demand, but it adds difficulty in result coordination, state consistency, error propagation across subagents3—these three don't exist in synchronous version, because order is code-determined.
Review loop's two rules are shallow and brittle. includes("稍等") will mis-flag sentences like "no need to wait, already handled" as filler. This is Course 10's old warned problem: overly strict deterministic validators will judge correct as incorrect. For real production, these two rules need calibration against a small batch of real replies, or demote them to "flag for judge re-review" rather than directly send back for rewrite.
Swap to real API, only swap stub, structure unmoved. makeStubClient(queue) swap to new Anthropic(), delete entire SCRIPTS table, rest not one line changes—runAgent was always written to real API's stop_reason / tool_use / tool_result shape, model and max_tokens always carried. After swap three things will change: classification result will jitter (same tickets, two runs might land in different categories), gate rounds will jitter, token count will jitter; one run costs money and time; Course 7's three unmoved valves must be installed back.
Each added layer of complexity must pass the "measurably improves" gate. Every pattern in this graph can be individually removed: don't do routing, one generic prompt can also reply to tickets; don't do fan-out, serial run six also finishes; don't do review loop, manual spot-check is also a method. After removal whether metrics drop, drop how much, must test to know. Only when complexity genuinely improves results, is it worth adding2.
💻 Exercises
Recap
- Four patterns welded into one file (voting supplements fifth in exercise, orchestrator-workers intentionally absent because dispatch can be predefined), "plan in code" this statement has concrete shape:
main()'s dozen lines are all control flow, routed / drafts / items three ordinary variables are all state. LLMs and tools are orchestrated through predefined code paths2, script itself holds loop, branching, and intermediate results, model's context only holds what it needs for this step1
- Not every node has to be a model: five nodes two call models,
merge, report and gate's first filter all pure code, other category goes string template. Anywhere deterministic code can give same answer, no reason to pay one call's money and latency
- Routing's value not in that call, but in those ten lines of tightening code after call: model's free text pressed into one of three legal labels, downstream branches only recognize values code has vetted; specialized prompts are dividend classification bought2
- Fan-out's concurrency must have ceiling, merge must pass references not payloads—output hits disk, pass only lightweight references downstream3, review node reads back from file itself. Synchronous fan-out doesn't hurt at this scale, scale large it becomes bottleneck3, changing to async must pay three costs: result coordination, state consistency, cross-subagent error propagation3
- Review loop is check-fix-recheck, until pass or no further progress1, plus one max rounds safety net2. Deterministic gate ranks before judge; "two consecutive rounds identical report" this criterion cuts losses earlier than max rounds, and conclusion it gives more informative: not "tried three times fails," but "it doesn't understand this feedback"
- Incremental trace brings recoverability: persist once after each node completes is precisely the premise for a run being continuable within same session1 (cross-process, cross-machine continuation is this lesson's own added layer promotion after persisting state to disk); pair with write
.tmp then rename atomic swap, killed at any moment disk has one readable-back complete state
- This graph manages one process, one batch tickets, steps locked-down work. Steps-unpredictable open-ended problems should go back to autonomous loops2; each added complexity layer must pass "measurably improves" gate2
Twelve lessons complete here.
Looking back, what you have now came piece by piece: Course 1 (Claude Code Skills: Build Your Own AI Workflows) you wrote your first prompt, learned to state requirements clearly; then came tool calling, workflows, skills, multi-agent collaboration, all the way to Course 7—that course had you write a loop yourself, while (response.stop_reason === "tool_use"), from that day agents are no longer a black box to you, but a piece of code you can read. Course 8 (Context Engineering: Spending Finite Attention Where It Counts) taught you to manage its context, don't let the loop spin until window bursts. Course 9 taught you to make it survive interruption, killed can continue from last stopped place. Course 10 taught you to verify its output, separate "looks done" from "done." Course 11 taught you to see its process, when things break have logs, have traces to check. This course taught you to compose multiple loops into a graph that itself holds the plan.
These six things are six facets of one thing: In code you wrote, you're controlling a non-deterministic thing. Loop is your writing, context is your management, checkpoints are your saves, acceptance criteria are your definition, logs are your print, plan is your arrangement. Model very strong, but it works within this control code you built.
Final step lands on concrete action: swap orchestrate.mjs's makeStubClient(queue) to new Anthropic(), delete SCRIPTS table, install back Course 7's three unmoved valves, then dump your work's real piled batch of tasks—real tickets, real logs, real todos—into inbox/, run first time. It'll likely have a few landing in needs_human, that's exactly what this graph should look like.
<!-- PART_13_END_MARKER -->