Build a plan-review-update loop

A single review pass over a plan tends to catch the same blind spots each time, because the reviewer shares the builder's context. This page builds a small loop that avoids that: one planning codon runs once and writes plan.md, then a loop alternates a fresh reviewer and a fresh updater, passing the work through ordinary files in the workspace. The loop contains only review and update, so those two codons run on each iteration.

We will work from a checked fixture, so every command below has a captured result to compare against. By the end you will have a validated hank file (the JSON file describing the codons and loops to run), a completed headless run (with no interactive UI) with seven codon completions, a set of preserved review notes, and a measured cost table you can use before adjusting budgets. The final section deliberately breaks the configuration at two known boundaries so you can see what the failure output looks like before you meet it in your own project.

Choose the plan-review-update loop#

The design separates the builder from the evaluator. That separation lets us tune the reviewer to look for omissions while the updater applies the findings, and it is what makes the review "blind": the fresh reviewer cannot fill gaps from the builder's conversation, so the work has to travel through a file in the workspace. In this fixture, the plan codon runs once, then the loop repeats review and update, with setup work between codons when a project needs it. A related implement → review → update variant uses rigs to run tests between codons so each reviewer starts with a fresh batch of results; see reliability patterns.

The checked fixture uses haiku for its plan, review, and update codons, so the commands below run the all-haiku capture. The production-derived pairing documented in reliability patterns instead uses a haiku blind reviewer and an opus updater for three iterations. A broader planning lineup (Gemini, Opus, and GPT planning views, an Opus merge, then a Sonnet review loop) is lineage context rather than this fixture's recipe. The reason to change the pairing is different failure modes, not a promise that one model is always better.

Make the loop's handoffs explicit#

With the pattern chosen, the next step is the hank file itself. A loop entry is a loop object: it names its termination rule and codons, while optional budget and archive settings scope the loop. The full field set:

Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
typestringyes= loopType discriminator - required for loops
idstringyesminLength 1Unique identifier for this loop
namestringyesminLength 1Human-readable name displayed in UI and logs
descriptionstringnoOptional description shown to users
terminateOniterationLimit | contextExceededyesTermination condition for the loop
codonsarray<object>yesminItems 1Array of codons to execute in each iteration
budgetobjectnoBudget scope for this loop.
archiveOnSuccessarray<string>noPaths to archive when the loop terminates. Paths are relative to the agent workspace.
Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
iterationLimitobjecttype = iterationLimit
↳ limitintegeryesmin 1
contextExceededobjecttype = contextExceeded

In this fixture, terminateOn is { "type": "iterationLimit", "limit": 3 }. The runtime therefore visits iterations 0, 1, and 2, checking the limit only after the last codon in each iteration. A loop budget can also cap dollars or time. When a cap trips, the runtime requests the running codon's interruption, and the loop ends early when the finishing codon's own cap or the loop's budget was exceeded; the codon's onExceeded policy then records it completed (the default) or failed, and the run continues past the loop only in the completed case or under an ignored failure. fresh is legal in this iteration-limited loop but is a hard load error inside a contextExceeded loop.

The handoff between codons is where the blind-review property is enforced. We give both loop codons continuationMode: "fresh" explicitly: the field is required, with no default. The reviewer starts a new session without the updater's conversation history, reads the files in agentRoot/, and writes review-notes.md; the updater reads that file and rewrites plan.md. Loop runtime IDs carry the iteration suffix: review#0, update#0, through review#2, update#2.

rigSetup is setup work that runs while its codon is preparing, before the agent session starts. The updater's rig copies the current review into notes/ before the updater starts, which is how the per-iteration notes survive. By default a failed rig operation fails the codon with a retriable reason, and the codon's onFailure policy decides what happens next; allowFailure: true (the fixture's choice) or --ignore-rig-failures on the run lets one operation fail without stopping the codon. The fixture picks that one guard rather than adding || true to the shell command, which would mask the failing exit status. If you replace the shell copy with a schema copy operation, keep copy.from as a relative POSIX path inside the hank directory.

archiveOnSuccess entries are file globs resolved against the agent workspace. This fixture's archiveOnSuccess: ["notes/"] is a bare directory entry and resolves zero files, so the note series is preserved by the updater's rig copy instead. When a loop codon's archive glob matches files, the runtime moves them to rigArchive/<loopId>-<iteration>/<runtime-codon-id>/<path>, where <runtime-codon-id> is the codon's runtime ID <codonId>#<iteration> with # replaced by - (so an edit#0 codon in a revise loop lands at rigArchive/revise-0/edit-0/…), and <path> is the matched file's path relative to agentRoot/. A non-loop codon uses rigArchive/<codonId>/<path>. A loop's own archiveOnSuccess moves matches to rigArchive/<loopId>-loop/<path>, and for a multi-codon body it also runs after each codon of an iteration except its last, so later codons in the iteration find the archived paths gone; it runs once only for a single-codon body. These destinations are under the execution directory, alongside agentRoot/; a rig running in agentRoot/ reaches the archive at ../rigArchive/. Use a file glob such as current-project/** for a tree: the loop-archive fixture records rigArchive/revise-0/edit-0/current-project/history.txt and rigArchive/revise-1/edit-1/current-project/history.txt. Archive destination shapes belong in the execution-directory reference. Model names here are the registry-checked haiku and opus shortcuts.

Keep the handoff file-based even when you change the model pairing.

[changed 0.10.0] Rig copy.from is strict: it cannot be an absolute path, escape the hank directory, or use a symlink.

The excerpt below isolates those handoff settings (continuationMode, the rig, and the archive entry) so you can see them without the surrounding prompt and retry configuration:

JSON
{
  "type": "loop",
  "id": "blind-reviews",
  "name": "Repeatedly improve the plan through blind iteration",
  "terminateOn": {
    "type": "iterationLimit",
    "limit": 3
  },
  "archiveOnSuccess": ["notes/"],
  "codons": [
    {
      "id": "review",
      "model": "haiku",
      "continuationMode": "fresh",
      "checkpointedFiles": ["review-notes.md"]
    },
    {
      "id": "update",
      "model": "haiku",
      "continuationMode": "fresh",
      "rigSetup": [
        { "type": "command", "allowFailure": true }
      ]
    }
  ]
}

For prompts, retries, budgets, and checkpoint details (the sealed file state recorded after a codon completes), use the fixture's complete hank.json, reproduced here:

JSON
    {
      "type": "loop",
      "id": "blind-reviews",
      "name": "Repeatedly improve the plan through blind iteration",
      "terminateOn": {
        "type": "iterationLimit",
        "limit": 3
      },
      "budget": {
        "maxDollars": 0.9,
        "maxTimeSeconds": 900,
        "onExceeded": "complete"
      },
      "archiveOnSuccess": ["notes/"],
      "codons": [
        {
          "id": "review",
          "name": "Blind review of the plan",
          "model": "haiku",
          "continuationMode": "fresh",
          "promptFile": "prompts/review.md",
          "checkpointedFiles": ["review-notes.md"],
          "onFailure": "retry",
          …
        },
        {
          "id": "update",
          "name": "Apply the review to the plan",
          "model": "haiku",
          "continuationMode": "fresh",
          "promptFile": "prompts/update.md",
          "rigSetup": [
            {
              "type": "command",
              "command": {
                "run": "mkdir -p notes && cp review-notes.md \"notes/review-notes-$(ls notes/ | wc -l | tr -d ' ').md\""
              },
              "allowFailure": true
            }
          ],
          "checkpointedFiles": ["plan.md", "plan-changelog.md"],
          …
        }
      ]
    }

checkpointedFiles names the files tracked in that checkpoint; this fixture tracks the review notes for review and the plan plus changelog for update.

Run from the fixture directory#

Download hankweave-fixtures-0.11.0.tar.gz (its manifest is at /fixtures/0.11.0/manifest.json), or use an individual artifact at /fixtures/0.11.0/files/<relative-file>. The archive already contains one top-level hankweave-fixtures-0.11.0/ directory. Extract it from the archive's parent, then enter the fixture directory; do not pre-create the same root and extract into it.

The shown bunx/npx recipe runs on Node ≥22.19.0: the package executable has a Node shebang and its engines field requires that version; the minimal fixture capture ran under Node v22.23.2. bunx hankweave@0.11.0 … is the chosen Bun launcher spelling; the equivalent Node/npm spelling for validation is npx hankweave@0.11.0 hank.json data/ --validate, with the same substitution for the run command.

Before starting a run, set ANTHROPIC_API_KEY in the environment. This fixture's plan, review, and update codons all use the haiku shortcut for claude-haiku; without the key, the per-model harness self-test's credential-presence check fails with No authentication found (set ANTHROPIC_API_KEY) at startup and under --validate alike.

⌁ Terminal
# Run these commands from the directory containing the downloaded archive.
tar -xzf hankweave-fixtures-0.11.0.tar.gz
cd hankweave-fixtures-0.11.0/plan-review-update

The entry point is plan-review-update/hank.json with its data/ directory. Validate before spending on codons:

⌁ Terminal
bunx hankweave@0.11.0 hank.json data/ --validate

Validation hashes the data source, resolves and validates the hank, and runs the same per-model harness self-test as startup: SDK availability, credential presence, and for Pi models the model catalog. A missing key fails it with exit 1. What validation does not prove is that the key is valid or the provider healthy: the credential check tests presence only, and provider health checks are off during validation. At runtime startup, before codons execute, those health checks call generateText("Hi", maxOutputTokens: 16) on available registry providers, not only the models selected by this hank; those calls may be billable and sit outside the tracked-codon capture total.

A successful validation prints the GOOD TO RUN! box. The captured full run is headless (without an interactive UI) and starts a new execution:

⌁ Terminal
bunx hankweave@0.11.0 hank.json data/ --headless --start-new -o out

--start-new gives the attempt an independent execution. This fixture declares no outputFiles, so -o out copies nothing: the captured out/ tree is empty, and plan.md, plan-changelog.md, review-notes.md, and notes/review-notes-{0,1,2}.md remain under ~/.hankweave-executions/<id>/agentRoot/, the Exec → path printed at startup. Managed execution directories live under ~/.hankweave-executions/<id>/, with run logs under .hankweave/runs/<runId>/; see execution-directory for the complete layout. Flag behavior belongs to CLI reference and resume, rollback, and retry.

[since 0.7.3] --validate warns about abort failure policies inside loops.

The startup stream confirms the new execution directory, the source-to-exec mapping, and headless mode:

Output
Created new execution directory: ~/.hankweave-executions/<exec-id>
New execution: <exec-id>
  Source → data
  Exec   → ~/.hankweave-executions/<exec-id>
  SDKs   → Claude node_modules ✓
…
══════════════════════════════════════════════════
  Hankweave Server Started
  WebSocket: ws://localhost:<port>
══════════════════════════════════════════════════

Running in headless mode on port <port>
➜ Listening on: http://localhost:<port>/ (all interfaces)

Check the run's completion evidence#

Three checks confirm the run did what the hank file describes: the validation summary, the event stream, and the files left in the workspace.

Check-it 1 – validation: confirm the GOOD TO RUN! box reports three codons, three prompts, three system prompts, one rig, and three checkpoints.

Output
╭─ GOOD TO RUN! ─────────────────────────────────────────────────────╮
│  3 codons • 3 prompts • 3 system prompts • 1 rigs • 3 checkpoints  │
╰────────────────────────────────────────────────────────────────────╯

Check-it 2 – completion (fixture capture): the fixture's recorded event stream holds seven codon.completed events, three loop.iteration.completed events (the last with isFinal: true and terminationReason: "iteration_limit"), then the final RunCompleted event. The runtime emits one codon.completed per terminal codon attempt; here that is one per codon (plan, three reviewer iterations, and three updater iterations) because nothing was retried. The headless console prints no per-codon events; the capture below is the fixture's recorded event stream.

Output
{"id":"<id>","timestamp":"<ts>","type":"codon.completed","data":{"codonId":"plan","success":true,"cost":"<n>","duration":"<n>","exitStatus":{"type":"success"}}}
…
{"id":"<id>","timestamp":"<ts>","type":"codon.completed","data":{"codonId":"review#0","success":true,"cost":"<n>","duration":"<n>","exitStatus":{"type":"success"}}}
…
{"id":"<id>","timestamp":"<ts>","type":"codon.completed","data":{"codonId":"update#0","success":true,"cost":"<n>","duration":"<n>","exitStatus":{"type":"success"}}}
…
{"id":"<id>","timestamp":"<ts>","type":"codon.completed","data":{"codonId":"review#1","success":true,"cost":"<n>","duration":"<n>","exitStatus":{"type":"success"}}}
…
{"id":"<id>","timestamp":"<ts>","type":"codon.completed","data":{"codonId":"update#1","success":true,"cost":"<n>","duration":"<n>","exitStatus":{"type":"success"}}}
…
{"id":"<id>","timestamp":"<ts>","type":"codon.completed","data":{"codonId":"review#2","success":true,"cost":"<n>","duration":"<n>","exitStatus":{"type":"success"}}}
…
{"id":"<id>","timestamp":"<ts>","type":"codon.completed","data":{"codonId":"update#2","success":true,"cost":"<n>","duration":"<n>","exitStatus":{"type":"success"}}}
{"id":"<id>","timestamp":"<ts>","type":"loop.iteration.completed","data":{"loopId":"blind-reviews","iteration":2,"durationMs":81500,"costUsd":0.0972383,"tokensUsed":6642,"isFinal":true,"terminationReason":"iteration_limit"}}
{"id":"<id>","timestamp":"<ts>","type":"state.transition","data":{"transitionType":"RunCompleted","runId":"<id>","transition":{"type":"RunCompleted","data":{"runId":"<id>"}},"resultingState":{"currentRunId":null,"runCount":1,"totalCost":"<n>","currentRunCost":"<n>"}}}

Check-it 3 – files: inspect agentRoot/ for the final plan and changelog, the latest review-notes.md, and the three preserved review notes. The note filenames are zero-indexed, matching the loop iteration IDs.

Output
notes/review-notes-0.md
notes/review-notes-1.md
notes/review-notes-2.md
plan-changelog.md
plan.md
review-notes.md

Measure costs before changing caps#

Use the captured run's measured costs rather than a forecast. The capture was measured on 2026-09-03 with haiku for all three codons:

Scroll to explore the table →
codonmeasured cost (USD)
plan$0.02284565
review#0$0.02012185
update#0$0.04359475
review#1$0.03351075
update#1$0.05477635
review#2$0.04528230
update#2$0.05195600
total$0.27208765

The verified model manifest lists haiku at $1/$5 and opus at $5/$25 per million input/output tokens. Those are manifest rates for comparing the cheap-reviewer/expensive-updater split, not a replacement for the dated capture above.

Replace agent review with a rig#

When a fixed evaluation suite can answer the review question, we can run it from a rig and reserve the agentic reviewer for work that needs judgment. Read the documented comparison as five rounds of (Agentic Build × Automated Review) plus one Agentic Review, compared with four rounds of (Agentic Build + Agentic Review); it is a comparison of review designs, not a measured cost for this fixture. For the method, use reliability patterns; use the patterns gallery to choose another runnable shape.

Break model handoffs on purpose#

The fixture ships two named break hanks, each with its own capture, so you can see both failure boundaries before hitting them in your own configuration. The first targets the model-handoff rule from the pitfall above:

⌁ Terminal
bunx hankweave@0.11.0 hank.break-continue-previous.json data/ --validate

This changes update to continue-previous and sonnet after a haiku review. Validation fails at load because different models cannot share the same session ID:

Output

╭────────────────────────────────────────────────────────────────────╮
│  Hankweave v0.10.0                                                 │
│  darwin arm64 • node v23.8.0                                       │
╰────────────────────────────────────────────────────────────────────╯

Calculating data signature for validation...

> Validating configuration: ~/.hankweave-executions/<exec-id>/agentRoot/fixtures/plan-review-update/hank.break-continue-previous.json

  Data source:    ~/.hankweave-executions/<exec-id>/agentRoot/fixtures/plan-review-update/data
  Execution path: ~/.hankweave-executions/validation-<id>

Validation failed: Loop 2 (blind-reviews) > Codon 2 (update): Cannot use continuationMode "continue-previous" when model differs from previous codon in loop. Different models cannot share the same session ID. Change to "fresh" to start a new conversation with a different model.

Captured: 2026-09-03T07:20Z · runtime: hankweave@0.10.0

To exercise the rig boundary, run the second named variant with the captured headless invocation:

⌁ Terminal
bunx hankweave@0.11.0 hank.break-allowfailure.json data/ --headless --start-new -y

Run this variant with --start-new after the main run: without it, execution lookup uses the newest managed execution for the data/ hash, not the hank filename, and would resume the completed main execution. The capture omitted the flag only because no prior execution existed. Its update rig exits before the update codon starts. Without allowFailure, that rig failure marks the codon failed with a retriable reason; under the fixture's onFailure: "retry" the codon and its rig run again twice, and then the run fails. The variant's capture (2026-09-03, hankweave@0.10.0) records the run as failed after plan and review#0 complete.