# Build a minimal hank from Chapter 1 In this chapter we take one supplier's two quote submissions, run a single Hankweave codon over them, and end with a checked output file: `envelope-aster.json`, containing both submissions extracted side by side with nothing merged and nothing decided. Along the way we will write no code of our own. The shipped Chapter 1 directory already contains the hank and its prompt; our job is to prepare the input, read what we are about to run, validate it, run it, and verify the result. By the end you will have seen the full lifecycle of one codon – configuration, prompt contract, validation, execution, checkpointing, and verification – on the smallest hank that still does real work. ## Before we start We begin with the shipped Chapter 1 directory, not a Git tag or a checkpoint name. Download the [0.10.0 fixture bundle](/content-assets/cf45dff5691c48c0/fixtures/0.10.0/hankweave-fixtures-0.10.0.tar.gz) or follow the [fixture inventory](/0.10.0/files/tutorial/fixtures). The archive contains one top-level `hankweave-fixtures-0.10.0/` directory. Extract it from the directory that contains the archive; do not create a second directory with the same name. Then enter `hankweave-fixtures-0.10.0/chapters/ch1/`. Every command in this chapter runs from that directory, where the shipped `hank.json` and prompt already live. For the commands in this chapter, use a non-root account, Git, Node 22.19.0 or later, and an `ANTHROPIC_API_KEY`. We use Bun's `bunx` as the package launcher below; if you use npm, substitute `npx hankweave@0.10.0` for `bunx hankweave@0.10.0`. The launcher starts hankweave's Node entry point, so Bun is not a substitute for the required Node version. The RFQ scenario, supplier dialects, and target contract come from [the tour](/0.10.0/files/tutorial/0-tour); we do not repeat them here. One preparation step remains before we can run anything. The fixture bundle's `verify.py` keeps held-out truth and generator data out of the agent input, and its `prepare-data` command builds the task-only input directory the agent will actually see. Run it once from the Chapter 1 directory; the commands below reuse its `task-data/` output: ```sh # Run in the directory containing the downloaded archive. tar -xzf hankweave-fixtures-0.10.0.tar.gz cd hankweave-fixtures-0.10.0/chapters/ch1 python3 ../../verify.py prepare-data ../../quote-template-unification task-data ``` The destination `task-data/` must not already exist. The preparation copies `digitized/`, `digitized-extracts/`, `lookups/`, `rfq/`, `source-quotes/`, and a filtered manifest. It leaves `truth/`, `generator/`, and planted failure specimens outside the input passed to the agent. This is evaluation-data separation, not a security sandbox. The prompt sees those copied directories immediately below `read_only_data_source/` rather than below a `data/` prefix. **Check-it:** before continuing, confirm that you are in `hankweave-fixtures-0.10.0/chapters/ch1/` and that its `task-data/` directory was created by `prepare-data`. After the run, `python3 ../../verify.py chapter 1 out` re-asserts the Chapter 1 output contract; held-out truth is consulted only by Chapters 4–5 reconciliation and anchor checks. ## What we're building With the input prepared, we can look at the hank itself. We will run one codon, `normalize-aster`, against the Aster-only Chapter 1 input. A codon is one configured agent step in the hank. Its job is mechanical extraction: it writes one `envelope-aster.json` containing two separate envelope objects, one for each submission. It does not merge, deduplicate, or decide which submission wins; those judgments belong to a later codon. This is a minimal-valid hank, not an empty one. The `hank` array contains exactly one codon, using the `haiku` model and `continuationMode: "fresh"`; there are no rigs, sentinels, or budget settings. An empty `hank: []` fails validation because the schema requires at least one item. **Check-it:** the complete source file below is the Chapter 1 hank we will validate and run. It has one `hank` item and names `envelope-aster.json` as its checkpointed and copied output. ```json { "$schema": "https://unpkg.com/hankweave@0.10.0/schemas/hank.schema.json", "meta": { "name": "quote-template-unification", "version": "1.0.0", "description": "Aster-only extraction checkpoint; preserves both submissions." }, "hank": [ { "id": "normalize-aster", "name": "Normalize Aster (Datalab dialect)", "model": "haiku", "continuationMode": "fresh", "description": "Mechanical extraction of both Aster submissions (original + rev2) into canonical envelope objects. No judgment: dedup, alias resolution, and validity checks happen downstream.", "promptFile": "prompts/normalize-aster.md", "checkpointedFiles": [ "envelope-aster.json" ], "outputFiles": [ { "copy": [ "envelope-aster.json" ] } ], "onFailure": "abort" } ] } ``` ## The hank file, line by line The file above is short, but each block in it does a distinct job, and a few of its rules are strict enough to be worth knowing before we touch anything. The `meta` block (`name`, `version`, and `description`) is human-facing metadata. Hankweave exposes it in the startup banner and resolved plan. The `$schema` value points editor tooling at the published 0.10.0 schema; the runtime validates with its bundled copy rather than fetching that URL. The `hank` array is the immutable logic sequence. Codon order is fixed when the hank loads. Each codon needs `id`, `name`, `model`, and `continuationMode`, and it receives its prompt through exactly one of `promptFile` or `promptText`. Here, `promptFile` is `prompts/normalize-aster.md`. At 0.10.0, that prompt reference is a relative path inside the hank directory, uses `/` separators, and cannot be absolute, escape with `..`, or resolve through a symlink. A `checkpointedFiles` glob watches `envelope-aster.json`, streams changes to the TUI client, and tracks the file in the Git-based checkpoint system using Gitignore resolution. `outputFiles` runs after codon completion and copies the named file to the output destination requested by the run. The detailed destination behavior belongs to the [runbook](/0.10.0/files/operate/runbook); there is no default output directory. `onFailure: "abort"` is the default here: retriable errors leave the server active, while non-retriable errors shut it down. The registry shortcut `haiku` selects Anthropic Claude Haiku through the Claude Agent SDK harness. It is a suitable choice for this mechanical extraction. Fields such as `rigSetup`, `env`, `budget`, `sentinels`, `exhaustWithPrompt`, and `retryConfig` belong to later chapters and to the [codon concepts](/0.10.0/files/concepts/codons) and [hank JSON reference](/0.10.0/files/reference/hank-json); we keep this first hank small. **Check-it:** compare the complete hank source printed above with the description below: it has the schema URL, one codon, one relative prompt file, one checkpointed output glob, one copied output, and `onFailure` set to `abort`. ## The prompt: teaching the codon what NOT to decide The hank tells the runtime what to run; the prompt tells the model how to behave. We know why both Aster submissions matter; the model does not unless we tell it. Writing the prompt means supplying that missing context as part of the handoff contract. This is the theory-of-mind problem in practical terms: every unstated assumption can become an extraction error. We tell the codon to read both Aster documents and to extract them faithfully without deciding between them. The prompt names `validate-and-repair` as the downstream consumer, so the output must keep both submissions independently loadable. It also tells the agent to use shell commands such as `cat` and `ls` for the mounted `read_only_data_source/` because that mount may be a symlink. The extraction rules are deliberately literal. Copy `unit_price_minor`, `qty`, `price_basis`, `status_hint`, and `source_ref`; keep `description` as `null` where the revision lacks it; carry through `quantity_breaks` and informational vendor extensions; and use `null` rather than inventing or omitting a missing value. The output is `envelope-aster.json` in the current workspace, never in the input mount. The complete prompt below is what the codon receives. Notice how much of it is prohibition: the prompt spends most of its length ruling out the helpful-sounding behaviors – merging, resolving, computing – that would corrupt this stage's output: ````markdown # Normalize Aster (Datalab dialect) You are the mechanical extraction stage for supplier **Aster Metals (AST)**. Aster submitted **two** documents against RFQ NC-RFQ-0042 — extract both, faithfully, and do not decide between them. Which one wins is a judgment call for the `validate-and-repair` codon, not you. Do not read anything under `read_only_data_source/truth/` — that directory is the held-out answer key used to check this pipeline's output after the fact. Reading it here would defeat the exercise. ## Read (do not modify) Use shell commands (`cat`, `ls`) to read from `read_only_data_source/` — it may be a symlink, so avoid the native Read/LS tools on it. - `read_only_data_source/digitized/aster.datalab-contract-fixture.json` — Aster's first submission, document `doc-aster-qb-1047`, Datalab-shaped digitizer output (`fields[]` + `lines[]`). - `read_only_data_source/digitized-extracts/aster-quote-qb-1047_rev2.json` — Aster's second submission, document `doc-aster-qb-1047-rev2`, a native JSON re-export (`lines[]` only, no `fields[]`). Its `supersedes` field points back at the first document — copy that pointer through; do not act on it. ## Extract For **each** of the two documents, produce one envelope object with exactly these fields (use `null` for anything the source document doesn't have — never omit the key, never invent a value): ```json { "document_id": "doc-aster-qb-1047", "quote_id": "q-aster-qb-1047", "adapter": "datalab", "supplier_code": "AST", "dedup_key": "AST|NC-RFQ-0042|B", "issued_at": "2026-01-10T09:00:00", "supersedes": null, "watermark": "FICTIONAL TRAINING FIXTURE — NOT CUSTOMER DATA", "quote_meta": { "rfq_id": "NC-RFQ-0042", "rfq_rev_target": "B", "issued_date": "2026-01-10", "valid_until": "2026-02-15", "incoterm": "FCA", "freight_included": true, "currency": "USD", "payment_terms": "NET_30", "stated_total_minor": 604000 }, "lines": [ { "source_line_id": "doc-aster-qb-1047-L1", "buyer_part_id_raw": "NC-1001-A", "description": "mounting bracket, steel", "qty": 500, "uom": "EA", "unit_price_minor": 410, "price_basis": { "kind": "PER_EACH" }, "status_hint": "QUOTED", "vendor_printed_extension_minor": null, "source_ref": { "document_id": "doc-aster-qb-1047", "block_id": "doc-aster-qb-1047-b1", "page": 1, "bbox": [0.06, 0.1, 0.94, 0.16] } } ] } ``` Notes on the two documents' quirks: - The **first** document's lines carry `description`; the **rev2** document's lines do not (set `description: null` for rev2 lines). - The **rev2** document's lines carry `vendor_printed_extension_minor` on some lines (e.g. NC-1004-A: `16001`). Carry it through verbatim; it is informational only. Never use it as a computed value. - `quantity_breaks` on the first document's NC-1002-A line is a merged-cell quantity-break table from the original XLSX. Carry it through under a `quantity_breaks` key if present, `null` otherwise — it is not used downstream in this run, but must not be silently dropped. - Copy `unit_price_minor`, `qty`, `price_basis`, `status_hint`, `source_ref` verbatim. Do not compute, convert, or infer anything (no arithmetic, no alias resolution, no expiry checking — those belong to `validate-and-repair`). ## Write Write a single JSON array of the two envelope objects — the original document first, then rev2 — to `./envelope-aster.json` relative to the working directory returned by `pwd`. The agent workspace is already the current directory. Do not add an execution-directory prefix or write under `read_only_data_source/`. Before finishing, run `test -s ./envelope-aster.json`. This file is the contract for `validate-and-repair`: it must be able to load `envelope-aster.json` and see both Aster submissions independently, with nothing pre-merged or pre-decided. ```` **Check-it:** before running, find the prompt's explicit instruction not to choose a winning submission and its `envelope-aster.json` write target. Those are the boundaries this chapter checks after execution. ## Validate the hank before running Before spending anything on a run, we validate. Validation checks the hank without running its codons. At 0.10.0 it checks schema conformance, strict path references, the model registry, and sentinel configuration. It also performs a local self-test for each unique model: the checks verify SDK import, CLI and credential presence, and the local model catalog lookup. Keys must be present, but validation does not call a provider, perform a network health check, or make a billable provider call. From the Chapter 1 directory, validate the prepared input: ```sh bunx hankweave@0.10.0 hank.json task-data --validate validation_status=$? printf 'validation process status: %s\n' "$validation_status" test "$validation_status" -eq 0 # npm users: npx hankweave@0.10.0 hank.json task-data --validate ``` The Chapter 1 hank validates cleanly: one `normalize-aster` codon, model `haiku`, no rigs, no sentinels, and no budget warnings. The command's process status is 0; validation is not a codon execution. The labelled `validation process status: 0` line is printed by the shell recipe above. A successful validation ends with a summary box like this one: ```text ╭─ GOOD TO RUN! ─────────────────────────────────────────────────────╮ │ 1 codons • 1 prompts • 0 system prompts • 0 rigs • 1 checkpoints │ ╰────────────────────────────────────────────────────────────────────╯ ``` The `GOOD TO RUN!` excerpt is from the separate minimal-provider validation capture and illustrates the validation summary shape. It is not the Chapter 1 run transcript. **Check-it:** run the validation command and confirm the recipe prints `validation process status: 0` before starting the paid codon run. If the `hank` array were empty, validation would instead report the schema's `minItems: 1` error. ## Run the validated hank With validation green, we prepare a fresh execution and let the single codon run headlessly. We pass the same task-only input directory used for validation, and we choose both an execution name and an output directory so the result has a known location: ```sh bunx hankweave@0.10.0 hank.json task-data \ --headless --start-new --execution exec --max-cost 9 \ --shim-idle-timeout 1800 --overwrite-output -o out run_status=$? printf 'run process status: %s\n' "$run_status" test "$run_status" -eq 0 # npm users: npx hankweave@0.10.0 hank.json task-data \ # --headless --start-new --execution exec --max-cost 9 \ # --shim-idle-timeout 1800 --overwrite-output -o out ``` Because this command explicitly sets `--execution exec`, Hankweave uses `exec/` relative to the current working directory stated above, `hankweave-fixtures-0.10.0/chapters/ch1/`. Its workspace is `exec/agentRoot/`, and its mounted corpus is `exec/agentRoot/read_only_data_source/`. The managed `~/.hankweave-executions//` pool is the default only when `-e`/`--execution` is omitted. That corpus is a symlink by default; `--copy` selects a copied mount when compatibility requires it. The agent writes `envelope-aster.json` into `exec/agentRoot/`, not into `read_only_data_source/`. The `-o out` option copies the declared output to the `out/` directory created in the current Chapter 1 directory; without `-o`, outputs remain in the execution workspace. The TUI is the default interactive client. `--headless` omits it and autostarts the codon; the shell recipe immediately inspects its process status and prints the labelled `run process status: 0` line. The shipped headless transcript contains a capture-added `exit=0` annotation; that annotation is not a line promised by the reader's command. For now, we only need the paths used in the file handoff. See the [execution-directory reference](/0.10.0/files/reference/execution-directory) for the canonical execution tree. > **Pitfall:** `read_only_data_source/` is the mounted input, not a write target. Keep generated files in the workspace (`agentRoot/`) and let `-o out` select the copied output location. **Check-it:** when the command finishes, check that `out/envelope-aster.json` exists and that the recipe printed `run process status: 0`. Do not use the existence of the input mount or a listening port as the output check. ## What a completed run looks like A completed codon seals a checkpoint: Hankweave commits its tracked files into the shadow Git repository at `.hankweave/checkpoints/.hankweavecheckpoints` inside the execution directory, with `agentRoot/` as the working tree. The event journal records the codon completion and then the `RunCompleted` transition. The captured excerpt below shows those two events; it is sanitized and replaces run IDs, timestamps, costs, and paths with placeholders: ```text {"id": "", "timestamp": "", "type": "codon.completed", "data": {"codonId": "normalize-aster", "success": true, "cost":"", "duration":"", "exitStatus": {"type": "success"}}} {"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "RunCompleted", "runId": "", "transition": {"type": "RunCompleted", "data": {"runId": ""}}, "resultingState": {"currentRunId": null, "runCount": 1, "totalCost":"", "currentRunCost":""}}} ``` The shipped headless capture also contains a capture-added `exit=0` annotation after the server starts and the codon completes; it is evidence about that capture, not output the recipe promises to print: ```text exit=0 ``` The resulting artifact is `out/envelope-aster.json`: a JSON array with two envelope objects, neither merged nor deduplicated. The complete example is a dated capture, not a promise that a future model response will be byte-for-byte identical. **Check-it:** use the completion event, the capture-labelled `exit=0` annotation, and the output file together. A checkpoint event without the output artifact is not a successful handoff; the runnable command's process status is checked separately above. ## Check-it: did it work? A finished process is not yet a correct output, so we verify in two steps. Run the fixture's semantic assertion first: ```sh python3 ../../verify.py chapter 1 out ``` Then inspect the output-specific contract with an executable assertion: ```sh python3 - <<'PY' import json from pathlib import Path items = json.loads(Path("out/envelope-aster.json").read_text()) assert isinstance(items, list) and len(items) == 2 assert [item["document_id"] for item in items] == [ "doc-aster-qb-1047", "doc-aster-qb-1047-rev2", ] assert all(item["dedup_key"] == "AST|NC-RFQ-0042|B" for item in items) assert items[1]["supersedes"] == "doc-aster-qb-1047" assert items[0]["supersedes"] is None print("PASS: two independent Aster envelopes") PY ``` The assertions establish four properties: 1. The file is a JSON array of length two, with the original document first and the revision second. 2. Both objects retain `dedup_key: "AST|NC-RFQ-0042|B"`; sharing that key does not merge them. 3. The revision carries `supersedes: "doc-aster-qb-1047"`, while the original remains present independently. 4. The dated capture's receipt records `codon.completed` for `normalize-aster` with tracked cost `$0.10441000` and exit code 0, captured on 2026-09-06. That is a dated observation; costs vary from run to run. The capture set also records the model assignment as `haiku`. The dated `receipt.json` record supplies the cost; the executable check is the file contract, and the captured cost is diagnostic context rather than a fixed budget or a value to hard-code into a new run. **Check-it:** require both `python3 ../../verify.py chapter 1 out` and the JSON assertion to pass. Together they check the chapter contract, the two-document order, the shared-but-unmerged identity, and the revision pointer. ## When things go wrong The most common first failure is a missing credential. If `ANTHROPIC_API_KEY` is missing, the credential-presence self-test fails before any codon runs, including when the check is requested through validation. The sanitized failure capture below shows the startup diagnostic: `Self-test FAILED` and missing authentication; its `exit=1` line is a capture-added process-status annotation: ```text ╭────────────────────────────────────────────────────────────────────╮ │ Hankweave v0.10.0 │ │ darwin arm64 • node v23.8.0 │ ╰────────────────────────────────────────────────────────────────────╯ Created new execution directory: ~/.hankweave-executions/ New execution: Source → data Exec → ~/.hankweave-executions/ SDKs → Claude node_modules ✓ [] [ERROR] Self-test completed: FAILED [] [ERROR] Self-test FAILED: Some checks failed [] [ERROR] - authentication: ✗ No authentication found (set ANTHROPIC_API_KEY) [ERROR] Server startup failed! Error message: Self-test failed for 1 model(s): - Claude Haiku 4.5 (latest) (anthropic/claude-haiku-4-5): Some checks failed • authentication: No authentication found (set ANTHROPIC_API_KEY) Stack trace: Error: Self-test failed for 1 model(s): - Claude Haiku 4.5 (latest) (anthropic/claude-haiku-4-5): Some checks failed • authentication: No authentication found (set ANTHROPIC_API_KEY) at eU (file:///build/runtime/0.10.0/node_modules/hankweave/dist/index.js:260:5401) at async Egt (file:///build/runtime/0.10.0/node_modules/hankweave/dist/index.js:842:647) at async file:///build/runtime/0.10.0/node_modules/hankweave/dist/index.js:843:754 exit=1 ``` A misspelled model name is rejected during `--validate` by the 0.10.0 model-catalog preflight; correct the model in the hank rather than attempting a run. If the prompt writes to `read_only_data_source/`, it is targeting the mounted prepared input. Keep the output target relative to the current workspace as the prompt requires. To exercise the missing-key path with the Chapter 1 paths, run this from the Chapter 1 directory: ```sh env -u ANTHROPIC_API_KEY bunx hankweave@0.10.0 hank.json task-data \ --headless --start-new --execution fail --max-cost 9 \ --shim-idle-timeout 1800 --overwrite-output -o out-fail failure_status=$? printf 'failure process status: %s\n' "$failure_status" test "$failure_status" -eq 1 ``` The observable is `Self-test FAILED` followed by the recipe's labelled `failure process status: 1`; no codon output is expected. The sanitized transcript shown above comes from the separate minimal-provider fixture and its `exit=1` line is a capture-added annotation, not a line the Chapter 1 command promises to print. Restore the key and rerun validation before spending on execution. **Check-it:** for the intentional missing-key case, require the startup failure and `failure process status: 1`, not a codon output. ## Roll back and run it again Once a run has completed, there are two ways to come back to the work, and they use different flags. Runtime rollback is an action in the runtime client, not a Git command; select the sealed checkpoint there rather than editing the checkpoint repository. For recovery, use the same execution and data without `--start-new`; a fresh attempt is different and must use `--start-new`. For an independent fresh attempt, keep `task-data/` but choose a new execution and output directory: ```sh bunx hankweave@0.10.0 hank.json task-data \ --headless --start-new --execution exec-2 --max-cost 9 \ --shim-idle-timeout 1800 --overwrite-output -o out-2 repeat_status=$? printf 'repeat process status: %s\n' "$repeat_status" test "$repeat_status" -eq 0 python3 ../../verify.py chapter 1 out-2 ``` Control flow and input integrity are repeatable, but model-written output text and its checkpoint commit are not guaranteed to be byte-identical. For this chapter, check each output semantically with `python3 ../../verify.py chapter 1 out`; the `compare-json` two-attempt projection belongs to Chapter 2, where it reports required-field variation rather than universally rejecting full-JSON or byte variation. We do not claim an unperformed comparison result here. The later chapters document their own model assignments and credentials; continue with those chapter command blocks rather than changing Chapter 1's model here. Chapters 1–2 use `ANTHROPIC_API_KEY`; Chapters 3–5 and the full anchor additionally require `BASETEN_API_KEY` for `pi/baseten/deepseek-ai/DeepSeek-V4-Pro`. In the Chapter 4/anchor quality-observer sentinel, use the full `anthropic/claude-haiku-4-5` registry ID rather than a `haiku` shortcut or a `pi/` identifier. The anchor's dated tracked codon cost is `$0.88303367`; it is not a Chapter 1 cost or a guarantee for a future run. Continue with the next shipped chapter directory rather than checking out a chapter tag. Use the fixture progression and [fixture guide](/0.10.0/files/tutorial/fixtures) to find the files for the next step. **Check-it:** a fresh repeat has `--start-new`, a distinct execution, a distinct output path, and a passing Chapter 1 semantic check. A recovery attempt omits `--start-new` and uses the persisted execution instead. > **Version note:** At 0.10.0, `promptFile` references are strict: relative, slash-separated paths inside the hank directory only. Absolute paths, `..` escapes, and symlinked references are rejected; a `./prompts/`-style reference still normalizes inside the hank directory.