A hank that loads cleanly can still deliver a wrong artifact. This page walks through the checks that close that gap, ordered from the cheapest to the most expensive: static validation, deterministic file checks, a real provider probe, and finally a comparison of the delivered output against a known-truth oracle. Each section shows what the layer catches, what it costs, and the evidence it leaves behind, using the shipped anchor hank and its fixture bundle as the running example. By the end you will be able to test a single codon in isolation, size its budget, turn failures into regression fixtures, and wire the whole sequence into CI.
Start with a guarded handoff#
Before the larger examples below, try this two-codon pattern: one agent extracts a decision, and a fresh agent checks it against the original notes. A script checks the handoff before the reviewer starts and again before export. There is no loop, renderer or separate orchestration layer.
Download the complete example. Its three files are shown below; the notes are fictional.
guarded-handoff/
├── hank.json
├── check.mjs
└── data/notes.txt
The configuration keeps the prompt destination, checkpoint and export names together. The reviewer sees notes.txt and summary.json, not the first agent's conversation. The copied checker has a flat destination, so it needs no parent-directory setup.
{
"$schema": "https://unpkg.com/hankweave@0.10.0/schemas/hank.schema.json",
"meta": { "name": "A guarded handoff", "version": "1.0.0" },
"overrides": { "budget": { "maxDollars": 1, "onExceeded": "fail" } },
"hank": [
{
"id": "summarize",
"name": "Extract the decision",
"model": "haiku",
"continuationMode": "fresh",
"rigSetup": [
{ "type": "copy", "copy": { "from": "check.mjs", "to": "check.mjs" } },
{ "type": "command", "command": { "run": "cp read_only_data_source/notes.txt notes.txt" } }
],
"promptText": "Read <%AGENT_ROOT%>/notes.txt. Write <%AGENT_ROOT%>/summary.json as a JSON object with exactly three nonempty string fields: decision, owner, deadline. Preserve the source wording; do not infer dates or people. Run node check.mjs from <%AGENT_ROOT%> before finishing. No user is available to answer questions.",
"checkpointedFiles": ["notes.txt", "check.mjs", "summary.json"]
},
{
"id": "review",
"name": "Review the handoff against the notes",
"model": "haiku",
"continuationMode": "fresh",
"rigSetup": [
{ "type": "command", "command": { "run": "node check.mjs" } }
],
"promptText": "Read <%AGENT_ROOT%>/notes.txt and <%AGENT_ROOT%>/summary.json. Independently check the decision, owner and deadline against the notes; correct unsupported values in summary.json. Write <%AGENT_ROOT%>/review.md explaining what you checked and any correction. Run node check.mjs from <%AGENT_ROOT%> before finishing. Do not invent missing source facts. No user is available to answer questions.",
"checkpointedFiles": ["summary.json", "review.md"],
"outputFiles": [
{
"beforeCopy": [
{ "type": "command", "command": { "run": "node check.mjs && test -s review.md" } }
],
"copy": ["notes.txt", "summary.json", "review.md", "check.mjs"]
}
]
}
]
}
Save this checker as check.mjs. It verifies the required files and JSON shape, not whether the summary is factually correct. A missing or malformed handoff exits nonzero before the reviewer can improvise a replacement from incomplete input.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const notes = readFileSync('notes.txt', 'utf8');
assert(notes.trim(), 'notes.txt must contain the input for this run');
const summary = JSON.parse(readFileSync('summary.json', 'utf8'));
assert(summary && typeof summary === 'object' && !Array.isArray(summary),
'summary.json must be an object');
const fields = ['deadline', 'decision', 'owner'];
assert.deepEqual(Object.keys(summary).sort(), fields,
'summary.json must contain exactly decision, owner and deadline');
for (const field of fields) {
assert(typeof summary[field] === 'string' && summary[field].trim(),
`${field} must be a nonempty string`);
}
console.log('Handoff files and shape checked; factual correctness needs review.');
The input is data/notes.txt:
Fictional meeting notes for an authoring example.
Decision: trial the new search page with the support team.
Owner: Mara.
Deadline: next Friday.
From the extracted guarded-handoff directory, with ANTHROPIC_API_KEY available:
bunx hankweave@0.10.0 hank.json data/ --validate
bunx hankweave@0.10.0 hank.json data/ --headless --start-new -e run -o out
(cd out && node check.mjs && test -s review.md)
The one-dollar budget is a guard for this small exercise, not a price prediction or provider invoice. onExceeded: "fail" stops the sequence on a budget failure; it does not ask a later agent to work around an unfinished handoff. The review rig runs independently of export, whereas beforeCopy runs only when -o is supplied.
Check-it: remove summary.json from a disposable copy of the output and run node check.mjs there. It must fail. Then restore it and try an empty owner; that must fail too. Keep notes.txt and the checker in the exported bundle so the same check works from its own root. Read the review against the source to judge factual correctness; a valid JSON object is not enough.
This is an authoring example with local structure and gate checks, not a captured provider run. The minimal single-provider fixture supplies the recorded first-run evidence. For changes after a failure, see what resume preserves.
How much certainty can you buy before the run?#
Before trusting a run, separate two claims: "the file can load" and "the delivered artifact is right." A hank is the workflow file whose codons are sealed agent tasks that run in sequence. Sealing a task does not establish that its artifact is correct, and neither does a successful validation. The four layers below move from free structural checks toward the paid behavioral comparison, so that each layer only runs after the cheaper ones have passed.
| Layer | Catches | Network and credentials | Spend | Filesystem effect |
|---|---|---|---|---|
| 1. Static checks | Schema shape, file existence, safe paths, model-name resolution, and continuation logic | Configuration and harness checks; provider health checks are disabled during validation | No model-generation call | Computes a data signature, prints a GOOD TO RUN! box, resolves a validation-* execution path, and writes no codon output |
| 2. Deterministic checks | Scripts and contract tests over inputs and outputs | No model calls or provider credential | No spend | Reads files; a rig – a deterministic preparation task – may write a report |
| 3. Provider and harness checks | SDK, credential, catalog, and real provider wiring | Startup checks the local harness plane; a deliberate real run uses the network and credentials | The real probe's captured spend | The real run produces execution artifacts |
| 4. Behavioral checks | Whether the delivered artifact matches its truth file and preserves exceptions | Reads the real run's result; a sentinel – an event observer – reports only what its event input contains | The probe's spend has already happened | Compare output files with an oracle or run a deterministic artifact checker |
The table is the map for the rest of this page. Read the Spend and Filesystem columns closely: Layer 1 never calls a model, Layer 2 never touches the network, and only Layers 3–4 spend money. At 0.10.0, --validate (-v) resolves configuration, checks the local SDK/credential/catalog plane, and does not run codons or a model-generation self-test. Do not turn that into a universal offline or no-write guarantee: environment-dependent credential discovery can probe instance metadata, validation may add $schema to the hank, and it can write a temporary log. Runtime startup health checks are separate and can call generateText("Hi", maxOutputTokens: 16) for available registry providers; those calls may be billable and are outside a tracked codon total. See the operator runbook for validation procedure and side effects, authentication and models for catalog diagnostics, and model resolution for model behavior.
Start with structure#
Layer 1 runs before anything costs money. At 0.10.0, promptFile and rig copy.from references must be portable relative POSIX paths inside the hank directory. Absolute paths, .. escapes, and symlinks are rejected while the configuration loads. The Pi catalog preflight also rejects registry-known models that Pi cannot serve, with suggestions. See hank JSON for the path contract.
Check-it: from the extracted fixture bundle, prepare the task-only input and validate the shipped anchor. The captured readiness box below is the observable proof that Layer 1 passed; it says nothing yet about behavior.
╭─ GOOD TO RUN! ──────────────────────────────────────────────────────╮
│ 7 codons • 7 prompts • 0 system prompts • 10 rigs • 7 checkpoints │
╰─────────────────────────────────────────────────────────────────────╯
# From the extracted fixture bundle root; first export ANTHROPIC_API_KEY
# and BASETEN_API_KEY as described in anchor-hank/README.md.
python3 verify.py prepare-data quote-template-unification ../hankweave-input
bunx hankweave@0.10.0 anchor-hank/hank.json ../hankweave-input --validate
The box confirms the hank's shape – seven codons, ten rigs, seven checkpoints – and the runnable commands reproduce it. Treat GOOD TO RUN! as permission to proceed to Layer 2, not as a result.
Check files without calling a model#
Layer 2 covers deterministic scripts and contract tests over the hank's inputs and outputs. Two examples ship with the fixtures. The corpus checker re-parses emitted files from disk against a separately transcribed specification oracle; it prints PASS or FAIL for each invariant and exits nonzero on failure. The anchor preflight rig recomputes a sha256 fingerprint for each corpus file this hank depends on and matches it against the corpus manifest's claimed hash, checks required top-level keys, and verifies the synthetic-data watermark. Its default allowFailure: false behavior blocks the first codon when the rig exits nonzero, so a corrupted corpus stops the run before any codon spends a token. The rig mechanics belong to tutorial/2-determinism.
#!/usr/bin/env bun
// Preflight rig for the quote-template-unification anchor hank.
// Zero dependencies beyond node:crypto/node:fs, run under `bun`. Verifies that
// the corpus mounted at read_only_data_source/ (a) contains the files this
// hank depends on, (b) matches corpus-manifest.json's sha256 for each, and
// (c) has the expected top-level shape and watermark for each quote document.
// Exits nonzero on any failure so rigSetup (allowFailure: false) stops the
// run before any codon spends a token on corpus that doesn't match its
// manifest.
…
function writeReportAndExit(ok: boolean, findings: Finding[]): never {
writeFileSync(
REPORT_PATH,
JSON.stringify({ ok, checkedAt: new Date().toISOString(), dataRoot: DATA_ROOT, findings }, null, 2),
);
const failed = findings.filter((f) => !f.ok);
if (!ok) {
console.error(`preflight: ${failed.length} check(s) failed -- see ${REPORT_PATH}`);
for (const f of failed) console.error(` FAIL ${f.path}: ${f.reason}`);
process.exit(1);
}
console.log(`preflight: ${findings.length}/${findings.length} corpus files verified (checksum + shape + watermark). See ${REPORT_PATH}.`);
process.exit(0);
}
Note the exit contract in the excerpt: any failed finding exits nonzero, which is what makes the rig a gate rather than a report.
Check-it: regenerate the corpus, then run the checker against that generated directory and require its captured green result.
5. `generator/check-invariants.ts` re-parses the **emitted** files from disk — not `seed.ts` —
against an independently re-transcribed `SPEC_ORACLE` (BOM qtys, per-supplier prices, stated
totals, re-typed a second time directly from corpus-spec.md §2/§3 into the checker file) so
the check is a real two-path comparison, not a tautology against the generator's own constant.
…
Two iterate-and-fix passes before green:
| # | Bug found | Root cause | Fix |
|---|---|---|---|
| 1 | `INV-4/EMB/NC-1005-A`, `INV-1`, `INV-6`, `INV-18` failing | `embar-quote.md`'s renderer never listed NC-1005-A in the page-1 qty table — only its price appeared (page 2) | added the missing page-1 row in `render-digitized.ts:embarQuoteMd` |
| 2 | `INV-10` (ENOENT) | checker inferred each JSON's parent dir from the filename instead of tracking dir alongside the walked path | `walkAll` results now carry their directory prefix explicitly |
| 2 | `F06` false failure | checker's F06 assertion re-implemented alias resolution instead of calling the shared `resolvePartId`, and its regex accepted any `NC-####-X`-shaped string as "resolved" regardless of BOM membership | F06 check now calls `resolvePartId` directly |
…
Final state: **76/76 checks pass** — INV-4 (cross-dialect consistency) is split into one
`check()` per (supplier, part) pair for granularity (8 suppliers × 5 parts = 40 checks), 17
more aggregate checks cover INV-1,2,3,5–16,18,20, and 19 checks confirm each failure fixture
(F01–F19) is actually malformed the way its spec row claims. Confirmed determinism end-to-end:
`rm -rf corpus && bun generator/gen-corpus.ts corpus/ && bun generator/check-invariants.ts corpus/`
run twice into `corpus/` and `corpus2/`, `diff -rq corpus corpus2` → no differences, `corpus2`
discarded.
…
76/76 checks passed.
cd quote-template-unification
rm -rf corpus && bun generator/gen-corpus.ts corpus && bun generator/check-invariants.ts corpus
The report excerpt shows why the green result is credible: the checker found real bugs in two iterate-and-fix passes before reaching 76/76, and the regeneration was run twice with a byte-for-byte diff to confirm determinism.
Probe the provider, then inspect behavior#
Layer 3 is where the network and credentials enter. The built-in startup self-test checks SDK import, credential presence, and model catalog/runtime locally; the smallest deliberate probe that exercises the provider is a real run. The minimal single-provider fixture records one complete haiku codon run at $0.01394590, captured on 2026-09-06 with actual service output – cheap enough to use as a wiring check before any larger run.
# Capture manifest
- Captured: 2026-09-06T05:52:24.303456+00:00 · runtime: hankweave@0.10.0
- actual_service_output: true
- Scope: one complete live execution
- Complete codons: 1/1
- Tracked codon cost: $0.01394590 (provider health checks and sentinel calls are separate)
- Input scope: notes only
The same fixture also captures the failure branch. The expected no-key output below is a useful failure regression, not a successful provider probe:
[<ts>] [ERROR] - authentication: ✗ No authentication found (set ANTHROPIC_API_KEY)
…
Error message: Self-test failed for 1 model(s):
…
• authentication: No authentication found (set ANTHROPIC_API_KEY)
Evaluate the delivered artifact#
Layer 4 answers the question the other layers cannot: is the output right? The anchor's deterministic oracle compares unified-records.csv with the held-out truth rows exactly: 40 supplier-part data rows across eight suppliers, plus the CSV header. It also compares the exception types and supplier coverage against the eight planted hazard classes. The truth directory stays outside the agent's task input, so the comparison is against information the run never saw. Run the checker after the real run; a completed codon or a sentinel message is not a substitute for the artifact comparison.
The quality-observer sentinel is deliberately narrower. It triggers only on the validate-and-repair codon.completed event and can report only status, failure reasons, and budget fields present in that event. Its input does not contain artifact contents or file.updated events, so artifact existence, row coverage, and correctness belong to verify.py and the expected output files.
"description": "Observes the validate-and-repair completion event. Reports only status and budget fields present in that event; cannot read or verify output artifacts.",
"trigger": {
"type": "event",
"on": [
"codon.completed"
],
"conditions": [
{
"operator": "equals",
"path": "codonId",
"value": "validate-and-repair"
}
]
},
…
"userPromptText": "The codon `validate-and-repair` completed. Here are the triggering completion events:\n\n<%= JSON.stringify(it.events, null, 1) %>\n\nSummarize only status, failure reasons and budget fields actually present. These completion events do not provide file contents or file.updated events. State explicitly that artifact existence, row coverage and correctness cannot be verified from this input. Never treat missing file-update events as proof of missing files. Observe and report only.",
The sentinel's own prompt states this limitation, which is the point: an event observer observes events. Anything about files needs a checker that reads files.
Check-it: from the anchor fixture directory, run the deterministic oracle against the output directory and require exit 0. Its assertion covers all 40 data rows and the eight exception types; do not use the sentinel log as that assertion.
# From the extracted fixture bundle root; reuse ../hankweave-input prepared above.
bunx hankweave@0.10.0 anchor-hank/hank.json ../hankweave-input --headless --start-new -o anchor-hank/out
python3 verify.py anchor anchor-hank/out
Apply the cheapest-first rule#
The layers earn their ordering by what they can spare you. Layers 1–2 catch missing keys, wrong aliases, bad shapes, and missing paths before we pay for the work needed by Layer 4. The shipped anchor plan has seven codons: five use haiku, and two use pi/baseten/deepseek-ai/DeepSeek-V4-Pro; the --init scaffold has four codons. See Examples for the product/provider glossary.
The re-proven anchor run assigns the three normalize codons, survey-and-extracts, and award-brief to haiku; validate-and-repair and reconcile use pi/baseten/deepseek-ai/DeepSeek-V4-Pro. Its tracked codon cost is $0.88303367; provider health checks and sentinel calls are separate. The three normalize codons have $0.50 hard caps, raised from $0.20 after haiku price drift tripped the old cap at $0.204.
Check-it: keep both receipts: the anchor validation box proves structural readiness, while python3 verify.py anchor proves behavioral coverage. They are different claims, and a hardening pass needs both.
Which layer catches this failure?#
With the layers defined, the failure inventory becomes an assignment exercise. Each row below is one single mutation of a golden input, and the Pipeline must column states the required handling. Assign the cheapest layer that can catch each failure; do not confuse a structural failure with a judgment about the delivered artifact. The full inventory is in tutorial/fixtures. JC-1 is the planted expiry-versus-visibility judgment call: route it to a human rather than resolving it silently.
| # | File | What's wrong | Pipeline must |
|---|---|---|---|
| F01 | bad-source-sha256.json | manifest hash mismatch | preflight rig exits nonzero; STOP before any codon [S§validations-2] |
| F02 | datalab-missing-block-id.json | block lacks block_id | schema-validate fail; evidence immutable → typed exception, source excluded [S§validations-1,3] |
| F03 | reducto-result-type-url.json | result.type=="url", chunks absent | adapter explicitly fetch-or-reject; never read missing chunks as empty doc [S§fixtures] |
| F04 | reducto-citation-page-out-of-range.json | citation page 3 of 2 | ref-integrity fail; exception, excluded [S§validations-4] |
| F05 | duplicate-source-line.json | same source_line_id twice | validation fail; repair dedups or line excluded — never silently kept [S§validations-6] |
| F06 | unknown-part-alias.json | part id absent from aliases | line → UNRESOLVED + exception; no price invention [S§validations-6] |
| F07 | ambiguous-ocr-part-number.json | NC-1OO5-A without disambiguating context | → UNRESOLVED + exception (deterministic; distinct from JC-1) [S§fixtures] |
| F08 | no-bid-as-zero.json | NO BID rendered $0.00 | normalize to status:NO_BID; zero valid only if source says zero with QUOTED [S§contract-enums] |
| F09 | per-pack-as-per-each.json | pack price read per-each (100× error) | TOTAL_MISMATCH blocks; repair via price_basis fields [S§validations-9] |
| F10 | unknown-uom-conversion.json | UOM pair not in conversions | fail closed: UNRESOLVED, no guessed factor [S§validations-7] |
| F11 | missing-currency.json | currency field absent | exception + excluded from ranking; never fabricate FX [S§contract-fx] |
| F12 | quote-revision-a.json | quote targets RFQ rev A | route to exceptions; excluded from rev-B comparison [S§validations-5] |
| F13 | expired-quote.json | valid_until < award date | cannot win ranking; stays visible; JC-1 banner (§4) [S§validations-10, G§4-break-it] |
| F14 | stated-total-mismatch.json | stated ≠ computed total | blocking TOTAL_MISMATCH; never "fix" source total [S§validations-9] |
| F15 | freight-exclusion-omitted.json | EXW note dropped | validation fail; freight_included:false w/ source_ref required [S§contract] |
| F16 | unsupported-substitution.json | sub without approval info | substitution.approval_required:true + exception; not counted as clean QUOTED [S§contract-enums] |
| F17 | broken-source-ref.json | ref to nonexistent block | citation-integrity fail; repair or exception [S§validations-4] |
| F18 | unlabeled-synthetic-document.json | watermark absent | final gate STOP; no publication [S§validations-14] |
| F19 | award-brief-unsupported-number.md | brief number not in comparison.json | final_validate.py fails; publication blocked [S§validations-11] |
- Assign the cheapest layer. Five examples show the reasoning:
bad-source-sha256.json: choose Layer 2. The preflight rig catches the manifest mismatch and stops before a codon spends a token.datalab-missing-block-id.json: choose Layer 1. The input has the wrong shape; this is structure, not judgment.no-bid-as-zero.json: choose Layer 4. A structurally perfect$0.00must becomestatus: NO_BID, with a null price and an exception row – only a behavioral check sees the difference.expired-quote.json: choose Layer 4. No layer decides whether expiry beats visibility; route JC-1 to the human exception queue.- An unset
ANTHROPIC_API_KEY: choose Layer 1's--validate; its local authentication self-test fails at zero spend. Runtime startup repeats the authentication check and aborts before a codon runs.
Check-it: compare those five assignments with the fixture evidence. The manifest mutation and the silent-wrongness mutation are shown directly below: the first flips a single hex digit in the claimed hash, and the second pairs a $0.00 price with a QUOTED status hint, which is exactly the shape a structural check will pass and a behavioral check must reject.
{
"data_class": "synthetic",
"purpose": "preflight-manifest-verify negative fixture",
"entries": [
{
"path": "digitized/aster.datalab-contract-fixture.json",
"sha256_claimed": "0c746a895cf089ec5720e55bffddcbca05b8c0d88f3af79710dcb8d7855d9e77",
"sha256_actual_for_checker": "dc746a895cf089ec5720e55bffddcbca05b8c0d88f3af79710dcb8d7855d9e77",
"media_type": "application/json"
}
]
}
"block_id": "doc-cedar-cw-77-b4",
…
"text": "$0.00",
…
"unit_price_minor": 0,
…
"status_hint": "QUOTED",
[<ts>] [ERROR] - authentication: ✗ No authentication found (set ANTHROPIC_API_KEY)
…
Error message: Self-test failed for 1 model(s):
…
• authentication: No authentication found (set ANTHROPIC_API_KEY)
- Promote arithmetic to Layer 2. If a Layer-4 catch is pure arithmetic over files you control, make it a Layer-2 contract test. Per-pack price consistency against
lookups/unit-conversions.jsonis the example: the anchor encodes the check in its repair prompt, while the corpus checker scripts it over the corpus.
Keep the observer and oracle boundaries distinct. The sentinel covers one completion event; the eight-supplier oracle covers 40 data rows and eight exception rows. A sentinel can tell you what the event reported, but only an artifact-reading checker can establish coverage and correctness. Assign the complete set by the Pipeline must column: F01 goes to the preflight rig, F02 to schema or shape validation, F03–F17 to typed-exception routing, and F18–F19 to the final gate or publication block.
Test one codon without the whole hank#
A whole-hank run can hide which task produced a bad artifact. The fix is to isolate the codon, select the execution directory explicitly, and compare models only when both see the same fresh state.
- Extract the codon under test. Copy the codon into a standalone
test-hank.json, stage the envelope files and survey handoff it requires, and select a named execution directory. For an A/B comparison, launch each attempt with--start-new --forceand use separate execution and output directories.
# From the extracted fixture bundle root, with test-hank.json staged here.
bunx hankweave@0.10.0 test-hank.json ../hankweave-input --validate
bunx hankweave@0.10.0 test-hank.json ../hankweave-input --headless --start-new --force -e ./exec-a -o ./out-a
-e selects, creates, or resumes an execution directory. For an explicitly selected existing execution, --start-new --force backs up prior state and wipes agentRoot/; add --no-wipe when preserving that workspace is intentional. Without -e, --start-new creates a new managed directory rather than selecting the previous workspace. Name the hank, prepared data, execution, and output paths; the flags alone are not a complete test.
Check-it: validate the extracted hank, then inspect its event journal for the extracted codon ID and run the artifact checker against its output contract.
- Inspect an execution one codon at a time. Launch with
--no-autostart, then pressnin the TUI – the interactive terminal view – to advance one codon. The key sends thecodon.nextWebSocket command. Pin the execution when you need to return to it.
bunx hankweave@0.10.0 hank.json data/ --no-autostart
bunx hankweave@0.10.0 hank.json data/ -e ./dir --no-autostart
Check-it: after each n, confirm that the next codon is the one you intended to inspect.
- Keep extraction and checking separate. Use an extraction-sandwich pattern: extraction codons write a receipted JSONL intermediate; rigs then receipt-check and coverage-check it fail-closed; a deterministic renderer emits byte-stable output. Models find scattered bits; rigs verify them.
If a verification rig flags failures, check the line receipts before blaming extraction. In a 2026-09-01 event-catalog extraction audit, the verification rig flagged 23 failures; the audit adjudicated every one as a validator defect, not an extractor error. Fix the checker against receipts instead of trusting either side. The final state had exact coverage for 36 of 36 event rows, with all receipt and coverage checks passing.
After a provider quota failure, resume with a --model override only for codons that have not run, because those codons still resolve their model. This is not an A/B replacement for --start-new on the failed codon.
Will it even complete on the cheapest model?#
Before paying for an expensive judgment run, check whether the paths, handoffs, and prompt shape work with haiku. The shortcut for --model is -m:
bunx hankweave@0.10.0 hank.json data/ --model haiku
This is a structural test, not evidence that the result is right. The anchor, for example, validated GOOD TO RUN before its first run, and all five chapter variants validate. The anchor still needed four live iterations to satisfy its behavioral checks. Record the validation result and the later artifact comparison as separate observations. The captured anchor rerun is in tutorial 5: break, resume, and inspect.
What should happen if a test run reaches its cap?#
Choose onExceeded according to whether a partial artifact is safe to pass downstream:
- Use
onExceeded: "fail"when incomplete output is worse than no output. The anchor'sreconcilecodon must produce all 40 supplier-part rows, so a budget interruption fails that codon. - Use
onExceeded: "complete"only when partial output is an acceptable, checked result. The anchor uses it forvalidate-and-repair, whose output is checked later by the 40-row, eight-exception oracle.
When Hankweave detects an exceeded budget, it requests interruption of the model process. onExceeded: "complete" then marks that interrupted codon completed; it does not give the model time to finish its reasoning. The run may advance with partial output, so pair this policy with an artifact check that can reject omissions. See budgets for the full policy and the operator runbook for cap-trip procedure.
Estimate with the full workload when possible, then leave margin. In early full-corpus tests, caps estimated from three suppliers met an eight-supplier workload: one codon reached $1.05 against a $1.00 cap, and another reached $0.51 against $0.50. Using at least twice the estimate would have covered those two overruns, but it is a starting margin, not a guarantee for a different model, price, or input size.
These are per-codon caps. The historical sonnet-policy run measured $2.41; the current anchor's tracked codon cost is $0.88303367, while its configured per-codon caps sum to $9.00. The three normalize codons now have $0.50 hard caps, raised from $0.20 after price drift tripped the old cap at $0.204.
For each iteration, record the estimate, configured cap, measured cost, whether the cap tripped, and whether the artifact check passed. This distinguishes a useful sizing measurement from a completed-artifact claim.
When is a repair finished?#
A repair is unfinished while the same failure can return without detection. Close the loop by turning the failure into a regression fixture or changing the policy that defines the expected result.
- Mutate a golden input. The 19 failure fixtures are programmatic single mutations of golden inputs. The checker carries 19 fixture-shape checks confirming that each mutation is malformed in the way its row claims. Keep the mutation, expected handling, and checker assertion together.
- Preserve expected failures. Keep the missing-key case as an expected-failure regression. Its captured diagnostic prevents the quickstart's failure branch from silently rotting.
- Reduce runtime bugs. Convert live findings into deterministic minimal reproductions in a bug report. The manifest-sha error was reproduced from a fixture, so the defect could be checked without replaying the whole incident.
At pipeline scale, capture-fixtures.sh <version> is design intent rather than a proven workflow command: it is designed to rerun real fixtures, including the oracle, at a locked version, but its fixture and extraction steps were recorded as untested beyond dry plumbing. Treat it as a design note, not as a runnable recipe.
Keep the machine ledger and the human queue separate. exception-ledger.json is the durable ledger written by validate-and-repair and appended by reconcile. Its EXPIRED_VALIDITY row carries judgment_call: true, options, a tentative resolution, and confidence. exceptions.csv is serialized by the render-exceptions.ts rig from the ledger's seven declared fields in exception-ID order. The human reviewer makes the expiry decision; no codon makes that judgment call.
Check-it: before declaring a repair done, require a fixture row and a passing fixture-shape check for that failure. The corpus checker must retain its green F01–F19 checks.
Where do these layers live in CI?#
Wire CI from cheap evidence to expensive evidence, in the same order this page used. On every push, validate first and execute the Layer-2 deterministic scripts next. Reserve Layer-3 and Layer-4 probes for deliberate points such as a merge. See the operator runbook for complete recipes, including GitHub Actions setup.
For Layer 2, pin dependencies and require a clean regeneration to reproduce the corpus byte-for-byte:
diff -rq corpus corpus2
An empty diff is the determinism proof shape to copy for the deterministic corpus. Keep the real provider probe and behavioral oracle out of every-push work unless that CI point explicitly calls for them. This preserves the ordering: structure, deterministic contracts, provider wiring, then artifact behavior.