Compose hanks without losing their boundaries

A single hank handles a single bounded job well. Real work rarely stays that small. Sooner or later you will want one hank to launch another as a separate process, a loop that rebuilds an implementation against a fixed test suite, a skill written for interactive agents turned into declarative codons, or shared scaffolding reused across several hanks. Each of these moves crosses a boundary, and each boundary has rules: what passes through it, what stays sealed, and what fails loudly when you get it wrong.

This page works through those four composition patterns. Each section gives the mechanics, a checked fixture or capture to read against, and a short check you can run to confirm the pattern behaved as expected in your own execution.

When one hank should call another#

When we already have a hank for a bounded task, we can call it from another hank rather than fold its steps into the parent. A command operation in the parent's rigSetup runs the child as a separate hankweave process, and the child remains a black box to the parent: it keeps its own state, journal, and checkpoints. The operation requires run, a shell command string; its workingDirectory is project by default (the agent workspace, agentRoot) or lastCopied. A run string is an ordinary shell invocation in that directory, not sandboxed by the path validation that governs copy operations. The schema row below records that contract; see rig concepts and hank JSON for the rest of it.

Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
typestringyes= commandType of setup operation
commandobjectyes
allowFailurebooleanfalsenoIf true, failure of this operation won't fail the codon (default: false). Recommended for rig setup in loop codons where operations might fail in some iterations (e.g., running commands that might not succeed initially).

Before invoking the child, stage it inside the agent workspace. A command rig executes in the workspace, so the child hank and every file its command needs must be reachable from there: a copy operation prepares the directory, and a command operation then runs the staged file. The anchor chapter's rig shows that ordering: make a directory, copy a script into it, run it.

Valid fragment (hank.schema.json).

JSON
      "rigSetup": [
        {
          "type": "command",
          "command": {
            "run": "mkdir -p pipeline"
          }
        },
        {
          "type": "copy",
          "copy": {
            "from": "rigs/preflight.ts",
            "to": "pipeline/preflight.ts"
          }
        },
        {
          "type": "command",
          "command": {
            "run": "bun pipeline/preflight.ts"
          }
        }
      ],

The launch itself uses the binary directly; there is no run subcommand. The child invocation recipe is:

Output
bunx hankweave@0.11.0 child/hank.json child/data --headless --start-new -o ./child-out

Three flags do most of the work. --headless runs unattended ("Run without TUI (for CI/CD and scripts)"), since a child cannot answer prompts. --start-new matters when unchanged data must be processed again: execution selection is keyed on the data signature, so without it the runtime reuses the newest completed execution for the same data, prints Resumed completed execution <id> — nothing to do. All codons already completed. Use --start-new to run fresh. on stdout under --headless (as an Info: line in the interactive TUI), runs no codon, and exits 0. -o sets the output directory the parent reads back from; a configured outputDirectory sets one too, and with neither, outputs stay in the child's own execution directory.

What appears on disk: the child creates its own auto-managed execution under ~/.hankweave-executions/<id>/ with its own state, journal, and checkpoints. Only what the child exports lands back in the parent's workspace: its declared outputFiles, copied when the completing codon ended completed and an output directory is set. A child that declares no outputFiles copies nothing, so the parent's verification step finds an empty file.

FIG. 1 Process boundary and file handoff between parent and child hanks
Read the diagram as text
Output
+-- parent execution (agentRoot) ------------------------------------+
|                                                                    |
|  +-------------------------------+    +----------------------------+
|  | copy rig stages the child's   |--->| command rig runs           |
|  | files                         |    | bunx hankweave             |
|  +-------------------------------+    +-------------+--------------+
|                                                     | spawns the process
|  +-------------------------------+                  |             |
|  | parent workspace receives     |<------------+    |             |
|  | child-out                     |  copied     |    |             |
|  +-------------------------------+  via -o     |    |             |
+---------------------------------------------- | ----|-------------+
                                                |     v
+-- child execution (its own execution directory) ------------------+
|                                                                    |
|  +-------------------------------+   +----------------------------+|
|  | child process                 |---| sealed: state, journal,    ||
|  | (--headless --start-new)      |   | checkpoints                ||
|  +---------------+---------------+   +----------------------------+|
|                  v                                                 |
|  +-------------------------------+                                 |
|  | outputFiles exported via -o   |---------------------------------+
|  +-------------------------------+                                 |
+--------------------------------------------------------------------+

The checked parent-child fixture puts these pieces together. The child hank lives inside the parent hank's directory and declares outputFiles: [{copy: ["child-out.txt"]}]; the parent codon's copy rig stages child into the workspace, and its command rig launches the child, verifies the export landed, and copies the file into the parent workspace. Read the rig below with that chain in mind: the copy operation stages, and the long command operation launches, verifies, and retrieves.

Current parent-child fixture · hankweave@0.11.0 · the rig command pins bunx hankweave@0.11.0.

JSON
        {
          "type": "copy",
          "copy": {
            "from": "child",
            "to": "child"
          }
        },
        {
          "type": "command",
          "command": {
            "run": "cd child && bunx hankweave@0.11.0 hank.json data/ --headless --start-new --max-cost 0.3 -o child-run > child-run.log 2>&1 && printf '\\nchild exit: 0\\n' >> child-run.log && test -s child-run/child-out.txt && cp child-run/child-out.txt ../child-out.txt && tail -3 child-run.log > ../child-run-tail.txt"
          }
        }

The capture records the parent's own managed execution (parent-success, exit 0) and a clean child exit with COMPOSE exported via -o. The child runs as its own managed execution because each launched hankweave process auto-creates one. In this fixture the child inherits the parent process's ambient environment: the child runs with no env block and no requirements.env, the rig command passes no key, yet the child completed, so its provider key came from the inherited environment. Listing keys in the parent's requirements.env is still useful as an optional fail-fast check at load, but it does not decide what reaches the rig. A default child also never trips the nested-execution guard: that guard applies only to an existing directory passed to -e without --start-new, and only when the path contains both /.hankweave-executions/ and /data.

Failure crosses the boundary in a specific way. A non-zero child exit fails the rig operation, the parent codon fails in preparing before any agent tokens are spent, and the rig.setup.failed event carries failureType and, when present, exitCode. With allowFailure: true, the operation logs a warning and the codon continues instead. The event rows below show exactly what the journal records in each case.

Scroll to explore the table →
idcategoryjournaledsentinelRoutedpayloadFieldsreceipts
rig.setup.completedagentic-backbonetruetruecodonId, rigType, commandCount, durationMs, createdCheckpointschemas/event-schemas.ts:709, schemas/event-schemas.ts:1104, schemas/event-schemas.ts:1356, hankweave-runtime.ts:2604
rig.setup.failedagentic-backbonetruetruecodonId, failureType, exitCode?, commandIndex?, ignoredschemas/event-schemas.ts:714, schemas/event-schemas.ts:1105, schemas/event-schemas.ts:1357, hankweave-runtime.ts:2213

Check-it: After a successful call, confirm three things. The parent's execution directory appears under ~/.hankweave-executions/ (the capture's Created directory for new execution line names parent-success), and the child ran as its own managed execution. The rig's test -s child-run/child-out.txt passed and child-out.txt in the parent workspace holds COMPOSE; child-run-tail.txt ends with child exit: 0. The captures below show both outcomes from the fixture: the success run, and the supplied failure variant, where a child whose prompt file is missing makes the command rig fail with exit code 1 and rig.setup.failed while the parent codon fails in preparing with zero agent tokens spent.

Observed parent-child success run · hankweave@0.11.0.

Output
exit=0

Observed child failure · hankweave@0.11.0.

Output
[<ts>] [ERROR] [DEBUG] Command failed with exit code 1
[<ts>] [ERROR] [DEBUG] Full stderr:
[<ts>] [ERROR] [DEBUG] Rig setup error details - Exit code: 1
[<ts>] [ERROR] Rig setup failed at item 2 ({"type":"command","command":{"run":"cd child && bunx hankweave@0.11.0 hank.json data/ --headless --start-new --max-cost 0.3 -o child-run > child-run.log 2>&1 && printf '\\nchild exit: 0\\n' >> child-run.log && test -s child-run/child-out.txt && cp child-run/child-out.txt ../child-out.txt && tail -3 child-run.log > ../child-run-tail.txt","workingDirectory":"project"},"allowFailure":false}): Command failed with exit code 1
[<ts>] [ERROR] [fatal] Rig setup item 2: Command failed with exit code 1

exit=1

A child can also live outside the parent entirely. Pass one positional remote hank beginning with https://, http://, or git@; Hankweave clones it into a local cache and reports either ✓ Cloned to cache or Using cached version. See the CLI reference for the remote grammar.

At campaign scale, the docs forge uses this same process boundary for concurrent shard hanks (three shards and three writer models in the first wave, peaking at four executions and about 37 codons in flight, under a machine-wide per-provider-family cap learned when four shards hit rate limits within two minutes). It also uses a hank as another hank's data: one gate-packet hank included its own hank.json, system prompt, and codon prompts in its data directory. Treat those as separate executions and inputs, not a shared session.

Build to a fixed spec with a discovered implementation#

Sometimes we know exactly what the output must look like but need an agent to discover the implementation. The Clausetta pattern handles this: fix the specification and tests first, then research, run a build loop and an evaluation loop, and document the result. The spec stays fixed while repeated evaluation guides the implementation.

Give the agent the documentation, the fixed spec as its system prompt, and an evaluation suite. In Clausetta, the spec is a 1,400-line definition of the exact output format, and a rig runs the evaluation suite between build iterations. Each iteration gets fresh context, with the new context told to inspect the current implementation and continue from it. The pseudocode below shows the shape of one cycle before we look at the hank that implements it.

Output
// Pseudocode, not the implementation.
build loop:
  give the iteration fresh context
  read the current implementation and fixed specification
  build or continue the implementation
  let the rig execute the fixed evaluation suite
  if the termination condition is met: terminate
  otherwise: iterate

Use continuationMode: "fresh" for the fresh-eyes choice. <%EXECUTION_DIR%> is a silent alias of canonical <%AGENT_ROOT%>; prefer <%AGENT_ROOT%> in new hanks, and see hank JSON for template variables. In the checked fixture, the evaluation command sits inside the implement codon's rigSetup and the surrounding loop supplies the termination condition, so the evaluation is part of each cycle rather than a separate evaluation codon.

The selection question for what to move out of the agent is the same one the anchor's normalization rigs answer: what is an agent doing that we don't need an agent for? Running a fixed test suite in a rig beats asking an agent to improvise the check. The pattern works when the specification remains the source of truth, each iteration sees current state with fresh eyes, the evaluation suite gives an objective pass or fail, and files preserve documentation across iterations. A reported production comparison put (Agentic Build × Automated Review) × 5 + Agentic Review ahead of (Agentic Build + Agentic Review) × 4; comics-hank v2 was reported at one-tenth the cost with better output. That is a named reported result, not a promise about a new hank.

Clausetta ships as learning/examples/clausetta in the runtime repository. The checked counterpart is examples/connector-build-test. The excerpt below shows its build loop and evaluation rig: note the iterationLimit termination, the loop budget, the fresh continuation mode, and the bun test rig command with allowFailure: true so a failing test run feeds the next iteration instead of killing the codon.

Captured 2026-09-03T07:25Z · hankweave@0.10.0 · connector-build-test build loop and evaluation rig.

JSON
      "type": "loop",
      "id": "build",
      "name": "Build the connector until the tests pass",
      "terminateOn": {
        "type": "iterationLimit",
        "limit": 3
      },
      "budget": {
        "maxDollars": 0.6,
        "maxTimeSeconds": 600,
        "onExceeded": "complete"
      },
      "codons": [
        {
          "id": "implement",
          "name": "Write the connector implementation",
          "model": "haiku",
          "continuationMode": "fresh",
          "promptFile": "prompts/build.md",
          "rigSetup": [
            {
              "type": "copy",
              "copy": {
                "from": "spec.md",
                "to": "spec.md"
              }
            },
            {
              "type": "copy",
              "copy": {
                "from": "tests",
                "to": "tests"
              }
            },
            {
              "type": "command",
              "command": {
                "run": "bun test 2>&1 | tee test-output.txt"
              },
              "allowFailure": true
            }
          ],
          "checkpointedFiles": ["connector.ts"],
          "onFailure": "retry",
          "retryConfig": {
            "maxAttempts": 2,
            "delayMs": 1000,
            "maxDelayMs": 60000
          },
          "budget": {
            "maxDollars": 0.2,
            "maxTimeSeconds": 300,
            "onExceeded": "complete"

Check-it: The connector fixture's build loop declares terminateOn: iterationLimit (limit 3) with the implement codon's eval rig running bun test between fresh build iterations. The loop terminates at its declared limit, with the evaluation-pass artifact on disk; run and cost details belong to the linked example page.

Import a skill by extracting, never by pasting#

A skill is written for interactive use: an agent decides when to invoke it and improvises. A hank is declarative: its codons and operations are predetermined. That mismatch means a skill cannot be pasted into a hank wholesale. The usable parts have to be extracted and re-expressed as a recipe.

Use this four-way map:

  1. Turn scripts into rig commands.
  2. Turn relevant instructions into prompt sections.
  3. Turn references/-style documents into pre-compiled context files.
  4. Turn a multi-step workflow into codon structure.

Discard trigger conditions, interactive instructions, UI-specific logic, and "when to use this skill" metadata; a hank has explicit codons instead of an invocation decision. Skills commonly live in Claude Code's CLAUDE.md and .claude/commands/*.md, Codex's ~/.codex/skills/{name}/SKILL.md and AGENTS.md, or Cursor's .cursor/rules/*.md and ~/.cursor/skills-cursor/{name}/SKILL.md. Read these as source material, not as Hankweave inputs.

Read the whole skill before extracting. Many skills in the wild are model-generated and never human-verified, with hallucinated endpoints, outdated instructions, or confidently wrong advice, so test every script against sample data, check dates, and question unverified endpoints. Prefer extracting three useful lines over importing three pages, and verify the scripts before wiring them into a codon; see rig verification.

The worked example, hankweave-skill.md, is maintained as a front door plus a catalogue and per-concern section files, with bun assemble.ts assembling the completed sections instead of one hand-edited blob. The excerpt below is that front door: the assembly header, the title, and the split note that explains the layout.

Assembled 2026-09-01 · illustrative source excerpt.

Output
<!-- hankweave skill · assembled 2026-09-01 · 5 sections · filter: ruled,living -->

# How to be a good agentic programmer

*Split into section files 2026-08-30 (Hrishi's design): this file is the front door — the definition and the fit test. What-is-where lives in `catalogue.md`. Everything else lives in `sections/`, one file per concern, loaded when needed. `catalogue.md` says what is where and how done it is; `bun assemble.ts` builds the skill from the done sections as one clean document; `--all` includes the staged stubs.*

For this skill, we can extract fit-test and write/run-shape material into prompt sections and map its section-file layout to per-codon promptFile entries. It ships no scripts, so most of the skill need not enter the hank, and assemble.ts is a maintenance command named in the source header, not behavior we are extracting. That honest result (mostly "discard") is the point of the pattern. Keep the capabilities you need explicit as rig code, prompt instructions, and workspace templates so you can inspect and debug them.

Once the hank is composed, validate it. --validate is configuration-only: it hashes the data source, resolves and validates the hank, and runs harness-setup, credential, and catalog checks with provider health checks disabled, so it makes no model calls. The billable model probes (generateText("Hi", maxOutputTokens: 16)) belong to runtime startup's health checks on available registry providers, not to --validate.

The acceptance marker is the GOOD TO RUN! box:

Captured validation output from the minimal single-provider fixture.

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

Check-it: Confirm the GOOD TO RUN! box appears after composing the hank. Do not confuse this configuration check with runtime-startup provider calls.

Share scaffolding without leaving the hank directory#

When several hanks need the same project skeleton, keep the deterministic boilerplate (TypeScript configs, ESLint rules, folder layouts, package.json, tsconfig.json, src skeletons) in a templates directory inside the hank, then copy-rig that template into the agent workspace before the codon that needs it. The old advice puts it directly: "Don't make the agent generate boilerplate from scratch. Pre-bake project structures like TypeScript configs, ESLint rules, and folder layouts into templates. Use the rig to copy the template, then let the agent fill in the unique parts." A working template also keeps the boilerplate deliberate: it prevents "creative" tsconfig.json settings instead of hoping the agent's choices are reproducible.

The location matters because of the path rules. A copy rig's from must be a portable relative POSIX path inside the parent hank's directory; absolute paths, .. escapes, and symlinks are rejected. Shared scaffolding therefore lives inside the hank, or arrives as a remote hank. See the hank JSON path rules.

For independent variants of a template, do the opposite of reuse: copy a fresh template in each iteration and set archiveOnSuccess to a file glob such as current-project/** (a bare directory name matches no files). A loop codon's archive destination is rigArchive/<loopId>-<iteration>/<runtime-codon-id>/<path>, where <runtime-codon-id> is the codon's runtime ID <codonId>#<iteration> with # replaced by -, and <path> is the matched file's path relative to agentRoot/.

Output
<execution>/rigArchive/<loopId>-<iteration>/<runtime-codon-id>/<path>

The checked fixture bears this out: loop revise, codon edit, iteration 0 runs as edit#0 and moves current-project/history.txt to rigArchive/revise-0/edit-0/current-project/history.txt; iteration 1 moves it to rigArchive/revise-1/edit-1/current-project/history.txt. The iteration appears once in each segment: edit-0, never edit-0-0 and never a bare edit. From a command running in agentRoot, copy-back starts at the sibling path ../rigArchive/. A codon outside a loop archives to rigArchive/<codonId>/<path>.

A loop's own archiveOnSuccess uses rigArchive/<loopId>-loop/<path>, but its timing is subtle: it fires whenever a completed or skipped loop codon's expansion leaves the plan unchanged, which is after every codon of an iteration except the last as well as at real termination. It runs once only when the loop body has a single codon; in a multi-codon body, later codons of the iteration find the archived paths gone. execution directory owns the full archive contract.

Those archive paths make sense once the ID scheme is clear. A loop's declared id is its loopId. A codon inside that loop receives a runtime ID of <codonId>#<iteration>, with the iteration suffix starting at zero–for example, review#0.

Check-it: For template variants, inspect the archive tree: both iteration suffixes appear once each, and the codon directory is the runtime ID with # replaced by -.