This page walks through a complete, checked example: a hank that turns eight supplier quotes, submitted in three different digitizer formats plus five native extracts, into a single reconciled table and a cited award recommendation. The example is the anchor fixture for the tutorial series, and it is the place to see how Hankweave's pieces – codons, rigs, checkpoints, sentinels, budgets and verifiers – fit together on a realistic workload. We will read the workflow, run it against a prepared input corpus, and verify its outputs against a held-out answer key. If you have not built a hank before, the chapter-by-chapter build in tutorial/0-tour is the gentler path; this page assumes you want the full working system and the reasoning behind its boundaries.
What this hank turns eight quote inputs into#
The task is a procurement consolidation against RFQ NC-RFQ-0042. Eight suppliers replied, but their quotes arrive in different shapes: three came through digitizer services with different output dialects, and five are native structured extracts. A hank is the JSON workflow that processes them. Here, a digitizer dialect is a supplier quote format normalized by a codon, while a native extract is parsed by the shipped rig before the agent reviews the unmatched intake. The unmatched granite-bid.xlsx intake is quarantined, the records are validated and repaired, and reconciliation produces 40 supplier-part rows. We finish with a cited award brief plus an exception queue, so a person can review ambiguous cases instead of having them silently resolved.
The hank directory contains the program and its supporting prompts, rigs, sentinel, RFQ reference, design notes, and README. The separate quote corpus contains the RFQ, source quotes, digitized inputs, native extracts, lookups, a held-out answer key, and failure specimens; its 43 files are SHA-256 hashed and marked data_class: "synthetic". See tutorial/0-tour for the chapter-by-chapter build and tutorial/fixtures for the full inventory.
The diagram below shows every stage and handoff in the run. Read it as a data-flow map: boxes are stages or artifacts, arrows are handoffs, and the two side branches are the event observer and the post-run checker.
Read the diagram as text
[N1 corpus — `read_only_data_source/`: digitized/ (3 dialect fixtures) · digitized-extracts/ (5 native + aster rev2) · source-quotes/ (raw intake) · lookups/ (aliases, UOM, FX)]
|
verify
v
[N2 preflight rig (bun, zero-dep): sha256 + shape + watermark — nonzero exit stops the run]
|
pass / fail-closed
v
[N3 normalize-aster/-beacon/-cedar (haiku ×3)] ──> [N5 envelope-aster.json (2) · envelope-beacon.json (1) · envelope-cedar.json (1)] ─┐
│
[N4 survey-and-extracts (haiku + native-inputs rig)] ─> [N6 envelope-{dover,embar,fjord,harbor,iris}.json + survey-notes.json (quarantine: GRN only)] ─┤
v
[N7 validate-and-repair (deepseek) — reads the 8 envelopes + `reference/rfq-bom-rev-b.json` + lookups/] ─> [N8 validated-records.json (40) + exception-ledger.json (8)]
^
[N9 quality-observer sentinel (`anthropic/claude-haiku-4-5`)]
observes N7's `codon.completed`
|
v
[N10 reconcile (deepseek) — coverage join: 8 suppliers × 5 BOM parts] ─> [N11 unified-records.csv (40 rows) + exception-ledger.json]
|
v
[N12 award-brief (haiku) — no new judgment calls] ─> [N13 award-brief.md (every $ cited [doc:block]) + exceptions.csv]
|
check
v
[N14 oracle: `python3 verify.py anchor out`]
N1–N14 are stages, handoffs, the event observer, and the post-run checker – not additional codons. The three digitizer codons preserve their source values; the native-input rig supplies five structured envelopes and the agent decides the unmatched intake's quarantine. The two judgment codons then validate and reconcile the complete eight-supplier set. In this fixture, quarantine means the unmatched file is reviewed and recorded in the survey/exception path, not turned into an envelope or a supplier-part row.
Read hank.json without losing the handoffs#
hank.json declares seven codons in fixed order, with no loops. Every codon starts a fresh session that reads prior handoff files from the workspace. Five codons use haiku for extraction, survey, or rendering; validate-and-repair and reconcile use pi/baseten/deepseek-ai/DeepSeek-V4-Pro – the Pi/Baseten provider path – for judgment work.
The excerpts below follow the file in execution order. Start with the schema (configuration contract), current description, and copy-rigged preflight setup. The rig creates pipeline/, copies the shipped preflight script, and runs it from the workspace. Before we spend tokens on a codon, the preflight checks hashes, required shape, and the fixture watermark; any failure stops that codon from starting.
Current configuration excerpt: schema, metadata, first codon, and preflight setup.
{
"$schema": "https://unpkg.com/hankweave@0.10.0/schemas/hank.schema.json",
"meta": {
"name": "quote-template-unification",
"version": "1.0.0",
"description": "Normalize eight suppliers across three digitizer dialects and five native extracts against RFQ NC-RFQ-0042, quarantine GRN, reconcile 40 supplier-part rows, and publish a cited award brief plus exception queue."
},
"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.",
"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"
}
}
],
"promptFile": "prompts/normalize-aster.md",
"checkpointedFiles": [
"envelope-aster.json"
],
"outputFiles": [
{
"copy": [
"envelope-aster.json"
]
}
],
"onFailure": "abort",
"budget": {
"maxTimeSeconds": 240,
"maxDollars": 0.5,
Preflight excerpt: required input shape and failure-report handling.
const REQUIRED_KEYS: Record<string, string[]> = {
"digitized/aster.datalab-contract-fixture.json": ["document_id", "quote_meta", "fields", "lines"],
"digitized/beacon.reducto-contract-fixture.json": ["document_id", "quote_meta", "result", "lines"],
"digitized/cedar.generic-ocr-contract-fixture.json": ["document_id", "quote_meta", "ocr_blocks", "lines"],
"digitized-extracts/aster-quote-qb-1047_rev2.json": ["document_id", "quote_meta", "lines"],
"lookups/part-aliases.json": ["version", "aliases", "substitutions"],
"lookups/unit-conversions.json": ["version", "conversions", "unknown_policy"],
"lookups/fx-rates.json": ["version", "base", "rates"],
};
const NEEDS_WATERMARK = new Set([
"digitized/aster.datalab-contract-fixture.json",
"digitized/beacon.reducto-contract-fixture.json",
"digitized/cedar.generic-ocr-contract-fixture.json",
"digitized-extracts/aster-quote-qb-1047_rev2.json",
]);
type Finding = { path: string; ok: boolean; reason?: string };
function sha256(bytes: Buffer): string {
return createHash("sha256").update(bytes).digest("hex");
}
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);
The next excerpt covers the native-input setup, validation handoff, event observer, and per-codon policies. survey-and-extracts retries up to three times; validation reruns the native-input rig with --check, which requires byte-identical generated envelopes and a completed survey. The observer is attached only to validate-and-repair and receives completion-event context; it does not inspect files or certify row coverage.
Sentinel excerpt: event trigger, full registry model, and managed text output.
{
"id": "quality-observer",
"name": "Quality Observer",
"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"
}
]
},
"execution": {
"strategy": "immediate"
},
"model": "anthropic/claude-haiku-4-5",
"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.",
"output": {
"format": "text",
"file": "quality-observer.log"
}
}
Current configuration excerpt: native-inputs, validation, checkpoints, and sentinel.
"id": "survey-and-extracts",
"name": "Prepare native exports and review raw intake",
"model": "haiku",
"continuationMode": "fresh",
"promptFile": "./prompts/survey-and-extracts.md",
"rigSetup": [
{
"type": "copy",
"copy": {
"from": "rigs/native-inputs.ts",
"to": "pipeline/native-inputs.ts"
}
},
{
"type": "command",
"command": {
"run": "bun pipeline/native-inputs.ts"
}
}
],
"onFailure": "retry",
"retryConfig": {
"maxAttempts": 3,
"delayMs": 20000,
"maxDelayMs": 120000
},
"budget": {
"maxDollars": 1.5,
"maxTimeSeconds": 1800,
"onExceeded": "complete"
},
"checkpointedFiles": [
"envelope-dover.json",
"envelope-embar.json",
"envelope-fjord.json",
"envelope-harbor.json",
"envelope-iris.json",
"survey-notes.json"
],
"outputFiles": [
{
"copy": [
"envelope-dover.json",
"envelope-embar.json",
"envelope-fjord.json",
"envelope-harbor.json",
"envelope-iris.json",
"survey-notes.json"
]
}
]
},
{
"id": "validate-and-repair",
"name": "Validate & Repair Envelopes",
"model": "pi/baseten/deepseek-ai/DeepSeek-V4-Pro",
"continuationMode": "fresh",
"description": "Reads all eight supplier envelopes plus survey notes, the RFQ contract and lookups. Resolves aliases, substitutions, duplicate submissions and line extensions; quarantines GRN and routes unresolved hazards to the exception ledger.",
"promptFile": "prompts/validate-and-repair.md",
"rigSetup": [
{
"type": "copy",
"copy": {
"from": "rigs/native-inputs.ts",
"to": "pipeline/native-inputs.ts"
}
},
{
"type": "command",
"command": {
"run": "bun pipeline/native-inputs.ts --check"
}
}
],
"checkpointedFiles": [
"validated-records.json",
"exception-ledger.json"
],
"outputFiles": [
{
"copy": [
"validated-records.json",
"exception-ledger.json"
]
}
],
"sentinels": [
{
"sentinelConfig": "sentinels/quality-observer.json"
}
],
"onFailure": "abort",
"budget": {
"maxDollars": 3.0,
"maxTimeSeconds": 2400,
"onExceeded": "complete"
}
},
The final excerpt closes the hank with reconciliation and the award brief. Budgets are per codon, not a shared hank-level pool: normalize ×3 cap at $0.50 and 240 seconds with fail; survey caps at $1.50 and 1,800 seconds with complete; validate at $3.00 and 2,400 seconds with complete; reconcile at $2.00 and 1,800 seconds with fail; and award at $1.00 and 1,200 seconds with complete. The configured caps sum to $9.00; that is not a measured bill. Reconcile is still the judgment and coverage boundary, but its fail policy prevents a capped run from publishing a truncated 40-row table. Checkpoints name the envelopes, survey notes, validated records, ledger, unified CSV, award brief, and exception CSV. The award setup also runs deterministic renderers before copying its outputs.
Current configuration excerpt: reconciliation and deterministic award exports.
"id": "reconcile",
"name": "Reconcile to Unified Records",
"model": "pi/baseten/deepseek-ai/DeepSeek-V4-Pro",
"continuationMode": "fresh",
"description": "Joins validated records against the RFQ's 5-part BOM across all 8 quoting suppliers, guarantees exactly 40 data rows, and keeps GRN only in the exception ledger.",
"promptFile": "prompts/reconcile.md",
"checkpointedFiles": [
"unified-records.csv",
"exception-ledger.json"
],
"outputFiles": [
{
"copy": [
"unified-records.csv",
"exception-ledger.json"
]
}
],
"onFailure": "abort",
"budget": {
"maxDollars": 2.0,
"maxTimeSeconds": 1800,
"onExceeded": "fail"
}
},
{
"id": "award-brief",
"name": "Award Brief & Exception Queue",
"model": "haiku",
"continuationMode": "fresh",
"description": "Renders cited financial recommendations deterministically from reconciled rows; the agent completes only a qualitative assessment. Source-based pre-copy checks protect the financial block and the lossless exception CSV.",
"promptFile": "prompts/award-brief.md",
"rigSetup": [
{
"type": "copy",
"copy": {
"from": "rigs/render-exceptions.ts",
"to": "pipeline/render-exceptions.ts"
}
},
{
"type": "copy",
"copy": {
"from": "rigs/render-award.ts",
"to": "pipeline/render-award.ts"
}
},
{
"type": "command",
"command": {
"run": "bun pipeline/render-exceptions.ts && bun pipeline/render-award.ts"
}
}
],
"checkpointedFiles": [
"award-brief.md",
"exceptions.csv"
],
"outputFiles": [
{
"beforeCopy": [
{
"type": "command",
"command": {
"run": "bun pipeline/render-exceptions.ts --check && bun pipeline/render-award.ts --check"
}
}
],
"copy": [
"award-brief.md",
"exceptions.csv",
"unified-records.csv"
]
}
],
"onFailure": "abort",
"budget": {
"maxDollars": 1.0,
"maxTimeSeconds": 1200,
"onExceeded": "complete"
}
}
]
}
The 0.10.0 reference contract also requires every promptFile and rig copy.from to be a relative POSIX (Unix-style) path inside the hank directory. Absolute paths, .. escapes, and symlinks (filesystem links) are rejected at load.
Keep judgment at boundaries that can explain it#
The seven codons are not an arbitrary split. When we choose a codon boundary, we can use the drill from design-notes.md: ask whether the model tier changes, whether the failure policy changes, and whether the next artifact is something a person inspects. Normalize-to-validate answers yes/yes/yes; validate-to-reconcile answers no/yes/yes; reconcile-to-award answers yes/yes/yes. That gives us separate places to inspect extraction, judgment, coverage, and publication without hiding a decision in a renderer.
Those boundaries matter because this fixture plants real conflicts. Aster normalization extracts both submissions without choosing a winner. Each carries an issued_at value; validation applies the later-issued_at rule, keeps the conflict in the ledger, and never merges the two documents. Its rule is that continuing must not mean silently omitting an unresolved line: the planted Cedar validity decision and other hazards become typed exceptions. Reconciliation requires every supplier-by-BOM-part combination exactly once; BOM means the RFQ's bill of materials. A missing combination becomes MISSING_COVERAGE plus an UNRESOLVED row.
The native rig preserves explicit source line and block identifiers, TSV currency, missing prices, OCR warnings, handwritten notes, and joined Markdown page fragments. It starts survey-notes.json with complete: false; the agent reviews only unmatched raw intake, records the Granite quarantine, and completes the survey. These are fixture-assigned locators, not evidence of a real OCR-service call. The two excerpts below show the rig's preservation rules and its survey-and-quarantine check.
Native-input excerpt: source identifiers, missing-price handling, and native annotations.
function nativeLine(documentId: string, row: Row, price: unknown, uom: string, page: number | null): Row {
require(uom === 'EA', `Unsupported native price unit ${uom}; no pack conversion was inferred`);
const amount = price === 'TBD' || price === '' || price === null ? null : integer(price, 'unit_price_minor');
return { source_line_id: text(row.source_line_id, 'source_line_id'),
buyer_part_id_raw: text(row.buyer_part_id ?? row.internal_part_code, 'raw part identifier'),
qty: integer(row.qty, 'qty'), uom, unit_price_minor: amount, price_basis: { kind: 'PER_EACH' },
status_hint: amount === null ? 'UNRESOLVED' : 'QUOTED',
source_ref: { document_id: documentId, block_id: text(row.source_block_id, 'source_block_id'), page, bbox: null },
...(row.note ? { source_note: row.note } : {}) };
}
function markdownEnvelope(body: string): Row {
const front = body.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
Native-input excerpt: unmatched intake, quarantine review, and completed-survey checks.
const surveyed = fs.readdirSync(path.join(root, 'source-quotes')).filter((name) => fs.statSync(path.join(root, 'source-quotes', name)).isFile()).sort().map((name) => `source-quotes/${name}`);
const recognizedFiles = new Set(recognized.map((item) => item.file));
const unmatched = surveyed.filter((file) => !recognizedFiles.has(file));
const surveyFile = path.join(output, 'survey-notes.json');
if (!check) {
fs.writeFileSync(surveyFile, JSON.stringify({ complete: false, surveyed, recognized, unmatched, quarantined: [] }, null, 2) + '\n');
} else {
const survey = JSON.parse(fs.readFileSync(surveyFile, 'utf8'));
require(survey.complete === true, 'Intake survey is incomplete');
require(JSON.stringify(survey.surveyed) === JSON.stringify(surveyed), 'Generated surveyed-file list changed');
require(JSON.stringify(survey.unmatched) === JSON.stringify(unmatched), 'Generated unmatched-file list changed');
require(Array.isArray(survey.recognized) && JSON.stringify(survey.recognized.map((item: Row) => [item.file, item.document_id, item.evidence])) === JSON.stringify(recognized.map((item) => [item.file, item.document_id, item.evidence])), 'Generated source-file recognition changed');
require(Array.isArray(survey.quarantined), 'Survey is missing the quarantine array');
const accounted = new Set<string>();
for (const item of survey.quarantined) {
const file = text(item.file, 'quarantined file').replace(/^read_only_data_source\//, '');
require(unmatched.includes(file) && !accounted.has(file), `${file}: not an unmatched intake file, or listed twice`);
text(item.supplier_hint, `${file}: supplier hint`); text(item.reason, `${file}: quarantine reason`);
accounted.add(file);
}
require(accounted.size === unmatched.length, 'An unmatched intake file was not reviewed');
}
console.log(`Native inputs: ${names.length} envelopes, ${names.reduce((sum, name) => sum + outputs[name].lines.length, 0)} lines; ${unmatched.length} unmatched intake file(s); ${check ? 'completed survey verified' : 'survey awaits agent review'}.`);
The RFQ reference is hand-transcribed from the corpus's XLSX/PDF RFQ and checked against its rev-A changes block, so this hank does not need a binary-parsing codon. Treat design-notes.md as a historical iteration record; the current seven-codon, eight-supplier contract is in hank.json and the current program contract. At publication, the agent writes only the qualitative assessment. Code serializes the ledger's seven fields into exceptions.csv, and code derives the financial recommendation from eligible QUOTED rows, using supplier-code order for ties and a source-document/block citation for each price. The remaining excerpts show the two renderers and the validation prompt rules they enforce.
Exception renderer excerpt: declared fields, scalar CSV cells, and ledger checks.
const columns = ['exception_id', 'type', 'supplier_code', 'buyer_part_id', 'detail', 'resolution', 'source_ref'] as const;
function cell(value: unknown): string {
if (value === null || value === undefined) return '';
if (!['string', 'number', 'boolean'].includes(typeof value)) throw new Error('Exception CSV fields must be scalar values');
const text = String(value);
return /[",\r\n]/.test(text) ? '"' + text.replace(/"/g, '""') + '"' : text;
}
try {
const ledger = JSON.parse(fs.readFileSync(path.join(workspace, 'exception-ledger.json'), 'utf8'));
if (!Array.isArray(ledger)) throw new Error('Exception ledger must be an array');
const ids = new Set<string>();
for (const row of ledger) {
if (!row || typeof row !== 'object' || columns.some(column => !(column in row))) throw new Error('Exception ledger row is missing required fields');
if (typeof row.exception_id !== 'string' || !row.exception_id || ids.has(row.exception_id)) throw new Error('Exception IDs must be present and unique');
ids.add(row.exception_id);
}
const order = new Intl.Collator('en', { numeric: true });
ledger.sort((a, b) => order.compare(a.exception_id, b.exception_id));
const rendered = [columns.join(','), ...ledger.map(row => columns.map(column => cell(row[column])).join(','))].join('\n') + '\n';
const target = path.join(workspace, 'exceptions.csv');
if (check) {
if (fs.readFileSync(target, 'utf8') !== rendered) throw new Error('Exception CSV differs from its ledger; rerun the renderer');
Award renderer excerpt: source citations, 40-row contract, and eligibility checks.
function amount(value: string, row: Row): string {
const minor = integer(value, 'Price');
const whole = (minor / 100n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return `$${whole}.${(minor % 100n).toString().padStart(2, '0')} ${citation(row)}`;
}
function records(text: string): Row[] {
const [header, ...data] = csv(text);
require(header?.join(',') === columns.join(','), 'Unified CSV header differs from the reconciled row contract');
require(data.length === suppliers.length * parts.length, 'Expected exactly 40 supplier-part rows');
const keys = new Set<string>(), quantities = new Map<string, string>();
return data.map(cells => {
require(cells.length === columns.length, 'Malformed unified CSV row');
const row = Object.fromEntries(columns.map((name, i) => [name, cells[i]]));
const key = `${row.supplier_code}/${row.buyer_part_id}`;
require(suppliers.includes(row.supplier_code) && parts.includes(row.buyer_part_id) && !keys.has(key), `Unknown or duplicate supplier-part row: ${key}`);
keys.add(key);
require(row.rfq_rev === 'B', `Unsupported RFQ revision: ${key}`);
require(integer(row.qty, `Quantity for ${key}`) > 0n, `Quantity must be positive: ${key}`);
require(!quantities.has(row.buyer_part_id) || quantities.get(row.buyer_part_id) === row.qty, `Cannot compare different ordered quantities: ${key}`);
quantities.set(row.buyer_part_id, row.qty);
require(['QUOTED', 'NO_BID', 'UNRESOLVED', 'QUARANTINED'].includes(row.status), `Unsupported status: ${key}`);
require(['true', 'false'].includes(row.ranking_eligible), `Invalid ranking eligibility: ${key}`);
if (row.status === 'QUOTED') {
require(row.currency === 'USD', `Cannot render an unsupported or missing currency as dollars: ${key}`);
require(['PER_EACH', 'PACK', 'BOX'].includes(row.price_basis), `Unsupported normalized price basis: ${key}`);
const unit = integer(row.unit_price_minor, `Unit price for ${key}`);
const extension = integer(row.line_extension_minor, `Line extension for ${key}`);
require(unit * BigInt(row.qty) === extension, `Unit price and line extension disagree: ${key}`);
citation(row);
} else {
require(row.ranking_eligible === 'false', `A non-QUOTED row cannot be eligible: ${key}`);
require(row.unit_price_minor === '' && row.line_extension_minor === '', `Non-QUOTED prices must remain blank: ${key}`);
Award renderer excerpt: deterministic financial projection and tie handling.
function financials(rows: Row[]): string {
const lines = [
'# Award brief', '', '<!-- BEGIN GENERATED FINANCIALS -->', '## Financial recommendation', '',
'Projection of unified-records.csv only. Rank QUOTED, ranking_eligible=true rows by line_extension_minor; break equal-price ties by supplier_code ascending. Unit prices are already normalized per each upstream. No eligible bid means no recommendation; unsupported data stops export rather than inventing a price or citation.', '',
'| Buyer part | Selected supplier | Unit price (USD per each) | Line extension (USD) | Tie / no-bid decision | Ineligible QUOTED suppliers |',
'| --- | --- | --- | --- | --- | --- |',
];
for (const part of parts) {
const partRows = rows.filter(row => row.buyer_part_id === part);
const eligible = partRows.filter(row => row.status === 'QUOTED' && row.ranking_eligible === 'true');
eligible.sort((a, b) => {
const left = BigInt(a.line_extension_minor), right = BigInt(b.line_extension_minor);
return left < right ? -1 : left > right ? 1 : a.supplier_code < b.supplier_code ? -1 : a.supplier_code > b.supplier_code ? 1 : 0;
});
const winner = eligible[0];
const excluded = partRows.filter(row => row.status === 'QUOTED' && row.ranking_eligible === 'false')
.sort((a, b) => a.supplier_code < b.supplier_code ? -1 : 1)
.map(row => `${row.supplier_code} ${citation(row)}`).join('; ') || 'None';
if (!winner) lines.push(`| ${part} | No eligible QUOTED bid | — | — | No recommendation | ${excluded} |`);
else {
const tied = eligible.filter(row => row.line_extension_minor === winner.line_extension_minor).map(row => row.supplier_code);
const decision = tied.length > 1 ? `Tie: ${tied.join(', ')}; supplier_code order selects ${winner.supplier_code}` : 'Unique lowest eligible bid';
lines.push(`| ${part} | ${winner.supplier_code} | ${amount(winner.unit_price_minor, winner)} | ${amount(winner.line_extension_minor, winner)} | ${decision} | ${excluded} |`);
}
}
return lines.join('\n') + '\n<!-- END GENERATED FINANCIALS -->\n';
Validation prompt excerpt: the Aster deduplication rule.
## Step 1 — Dedup Aster's two submissions
Both Aster envelopes share `dedup_key: "AST|NC-RFQ-0042|B"`. The rev2 envelope's `supersedes` field names the original document. Rule: **the later `issued_at` wins**; the earlier document is superseded. Do not merge the two — pick one winner's lines to carry forward.
Regardless of which wins, this is a real conflict and must be logged: emit a `DEDUP_CONFLICT` exception citing both documents' `stated_total_minor` values (they differ by one cent — this is deliberate, not a bug you should "fix"). Only the winning document's lines proceed to Step 2.
Validation prompt excerpt: Cedar's explicit judgment call.
## Step 3 — Quote-level judgment call (JC-1)
Compare each quote's `quote_meta.valid_until` to the RFQ contract's `award_decision_date` (`2026-02-01`). Cedar's `valid_until` is `2026-01-20` — **before** the award date. The quote is expired at the moment of award.
This is a **planted, irresolvable judgment call**, not a bug to silently fix. Whether an expired-at-award quote should be excluded entirely or kept visible-but-ineligible is a human decision. Resolve it as follows and make the judgment visible, not buried:
- Set `ranking_eligible: false` on **every** line of the expired quote, regardless of that line's own per-line status.
- Emit exactly one exception of `type: "EXPIRED_VALIDITY"` for the whole quote (one row, `buyer_part_id` covering all 5 parts pipe-joined, e.g. `NC-1001-A|NC-1002-A|NC-1003-B|NC-1004-A|NC-1005-A`), with these additional fields beyond the standard ones:
- `judgment_call: true`
- `options: ["exclude", "show-flagged"]`
- `tentative_resolution: "show-flagged"` — the quote stays visible in the unified output and cannot win ranking, but a human reviewer can override this.
- `confidence: 0.5` (deliberately uncertain — this is not a confident automatic call).
Run the hank with task-only inputs#
Download the versioned fixture bundle into a working directory. That site-relative path is a publication link, not a hostless curl URL. The archive already contains one top-level hankweave-fixtures-0.10.0/ directory; extract it in its parent, then run this anchor recipe from anchor-hank. Prepare the self-contained task-data directory once for this recipe, reuse it for repeats, and do not treat another chapter's task-data as the same physical directory:
tar -xzf hankweave-fixtures-0.10.0.tar.gz
cd hankweave-fixtures-0.10.0
cd anchor-hank
python3 ../verify.py prepare-data ../quote-template-unification task-data
Before the first launcher command, install Bun and Git, use a non-root account, and make sure the execution and output directories are writable. Bun is required even when you choose npx, because this hank's setup and output checks run bun scripts. The shown package recipe uses Bun's bunx launcher for a Node entry point; require Node >=22.19.0 for this recipe. A Node/npm user can use the equivalent npx hankweave@0.10.0 spelling. The tutorial ladder uses ANTHROPIC_API_KEY for ch1–ch2; ch3–ch5 and this full anchor also need BASETEN_API_KEY. For this paid run, the Haiku codons use ANTHROPIC_API_KEY and the two DeepSeek codons use BASETEN_API_KEY. Baseten is not covered by the explicit Pi credential-enforcement map, so a passing configuration check does not prove that its key works.
With prerequisites in place, validate the configuration without running codons, then start a new headless (noninteractive) execution and copy its outputs to out:
bunx hankweave@0.10.0 hank.json task-data --validate
bunx hankweave@0.10.0 hank.json task-data --headless --start-new \
--execution exec --max-cost 9 --shim-idle-timeout 1800 \
-o out
python3 ../verify.py anchor out
bun rigs/render-exceptions.ts out --check && bun rigs/render-award.ts out --check
The data directory is linked into the execution directory at agentRoot/read_only_data_source by default; --copy copies it instead. The prepared directory contains only digitized/, digitized-extracts/, lookups/, rfq/, source-quotes/, and a filtered manifest; truth/, generator/, and failure fixtures stay out. Prompts read the task directories immediately below that mount. --validate hashes inputs, resolves the configuration, and performs setup, credential, and catalog checks with provider health checks disabled. It runs no codons and no model-generation self-test, and it can add $schema or write a temporary log. Runtime startup has separate provider health checks, which may call a short generation on available providers and may be billable outside the tracked-codon total. A successful validation prints the configuration summary below.
Validation capture: the observed configuration summary.
╭─ GOOD TO RUN! ──────────────────────────────────────────────────────╮
│ 7 codons • 7 prompts • 0 system prompts • 10 rigs • 7 checkpoints │
╰─────────────────────────────────────────────────────────────────────╯
To test the fail-closed path without touching the good input, make a disposable copy of task-data, append one trailing space to the named JSON file, and use fresh execution/output paths. The JSON remains syntactically valid, but its bytes no longer match corpus-manifest.json:
set -e
test ! -e task-data-corrupt && cp -a task-data task-data-corrupt
printf ' ' >> task-data-corrupt/digitized/aster.datalab-contract-fixture.json
bunx hankweave@0.10.0 hank.json task-data-corrupt --headless --start-new \
--execution exec-corrupt --max-cost 9 --shim-idle-timeout 1800 \
-o out-corrupt
The preflight rig writes preflight-report.json with a sha256 mismatch finding and prints this source-derived diagnostic before the normalize-aster model launches; the attempt exits 1:
preflight: 1 check(s) failed -- see preflight-report.json
This is the expected rig diagnostic, not a captured full-Hank corruption transcript or a promise that provider startup health checks are free. The original task-data, exec, and out remain available for the good run.
Check-it: the validation capture shows the 10-rig GOOD TO RUN! box; a normal run ends with seven successful codons, the 14 copied files, and python3 ../verify.py anchor out exits 0; the disposable-copy attempt exits 1 with the checksum finding and does not launch the normalize-aster model.
Unless -o/--output is used, outputs remain in the execution directory's agentRoot; there is no default results directory. Auto-managed execution directories under ~/.hankweave-executions/ are reserved and cannot be targeted directly with -e. The copied full-run set contains eight envelopes, survey-notes.json, validated-records.json, exception-ledger.json, unified-records.csv, award-brief.md, and exceptions.csv – 14 files in all – with preflight-report.json in the workspace. A bare sentinel filename is different: this fixture's quality-observer.log is managed at <execution>/.hankweave/sentinels/outputs/<sentinelId>/quality-observer.log, not beside the ledger or in out.
Check the records before trusting the brief#
The accepted capture is one complete live execution from 2026-09-06 at published 0.10.0: all seven codons succeeded, and its tracked codon cost is $0.88303367. Provider health checks and sentinel calls are outside that tracked-codon figure.
Capture manifest: date, completion, cost, and input scope.
# Capture manifest
- Captured: 2026-09-06T13:07:23.154353+00:00 · runtime: hankweave@0.10.0
- actual_service_output: true
- Scope: one complete live execution
- Raw execution (path normalized): ~/.hankweave-executions/anchor/1788699352631-qoit-40a58f
- Complete codons: 7/7
- Tracked codon cost: $0.88303367 (provider health checks and sentinel calls are separate)
- Input scope: task inputs only; oracle/generator/failure fixtures excluded
The held-out checker requires a 40-data-row unified CSV (41 lines including its header) whose parsed rows match truth/expected.csv field-for-field, plus the same eight (type, supplier_code) hazard pairs in the exception CSV; it compares parsed values rather than raw bytes. The exception ledger and CSV each contain eight rows across eight hazard types. Cedar has four QUOTED rows excluded from ranking for expired validity and one NO_BID row for NC-1004-A; do not collapse those into five expired bids. The final brief keeps every dollar cited as [doc:block] and names Cedar for every part. The captures below show the CSV header, Cedar's five rows, the generated financial table, and the first exception rows.
Unified-record excerpt: the current CSV header.
supplier_code,buyer_part_id,rfq_rev,qty,unit_price_minor,price_basis,line_extension_minor,currency,status,ranking_eligible,source_document_id,source_quote_id,source_line_id,source_block_id
Unified-record excerpt: Cedar's five rows, including four ineligible quotes and one no-bid.
CDR,NC-1001-A,B,500,425,PER_EACH,212500,USD,QUOTED,false,doc-cedar-cw-77,q-cedar-cw-77,doc-cedar-cw-77-L1,doc-cedar-cw-77-b1
CDR,NC-1002-A,B,2000,88,PER_EACH,176000,USD,QUOTED,false,doc-cedar-cw-77,q-cedar-cw-77,doc-cedar-cw-77-L2,doc-cedar-cw-77-b2
CDR,NC-1003-B,B,1000,110,BOX,110000,USD,QUOTED,false,doc-cedar-cw-77,q-cedar-cw-77,doc-cedar-cw-77-L3,doc-cedar-cw-77-b3
CDR,NC-1004-A,B,25,,PER_EACH,,USD,NO_BID,false,doc-cedar-cw-77,q-cedar-cw-77,doc-cedar-cw-77-L4,doc-cedar-cw-77-b4
CDR,NC-1005-A,B,5000,24,PER_EACH,120000,USD,QUOTED,false,doc-cedar-cw-77,q-cedar-cw-77,doc-cedar-cw-77-L5,doc-cedar-cw-77-b5
Award-brief capture: the generated financial recommendation and its source-citation contract.
Projection of unified-records.csv only. Rank QUOTED, ranking_eligible=true rows by line_extension_minor; break equal-price ties by supplier_code ascending. Unit prices are already normalized per each upstream. No eligible bid means no recommendation; unsupported data stops export rather than inventing a price or citation.
| Buyer part | Selected supplier | Unit price (USD per each) | Line extension (USD) | Tie / no-bid decision | Ineligible QUOTED suppliers |
| --- | --- | --- | --- | --- | --- |
| NC-1001-A | BCN | $3.95 [doc-beacon-8821:doc-beacon-8821-b1] | $1,975.00 [doc-beacon-8821:doc-beacon-8821-b1] | Unique lowest eligible bid | CDR [doc-cedar-cw-77:doc-cedar-cw-77-b1] |
| NC-1002-A | AST | $0.79 [doc-aster-qb-1047-rev2:doc-aster-qb-1047-rev2-b2] | $1,580.00 [doc-aster-qb-1047-rev2:doc-aster-qb-1047-rev2-b2] | Unique lowest eligible bid | CDR [doc-cedar-cw-77:doc-cedar-cw-77-b2] |
| NC-1003-B | EMB | $1.05 [doc-embar-quote:doc-embar-quote-b3] | $1,050.00 [doc-embar-quote:doc-embar-quote-b3] | Unique lowest eligible bid | CDR [doc-cedar-cw-77:doc-cedar-cw-77-b3] |
| NC-1004-A | AST | $6.40 [doc-aster-qb-1047-rev2:doc-aster-qb-1047-rev2-b4] | $160.00 [doc-aster-qb-1047-rev2:doc-aster-qb-1047-rev2-b4] | Unique lowest eligible bid | None |
| NC-1005-A | BCN | $0.19 [doc-beacon-8821:doc-beacon-8821-b5] | $950.00 [doc-beacon-8821:doc-beacon-8821-b5] | Unique lowest eligible bid | CDR [doc-cedar-cw-77:doc-cedar-cw-77-b5] |
<!-- END GENERATED FINANCIALS -->
Exception CSV capture: header and the first structured exception rows.
exception_id,type,supplier_code,buyer_part_id,detail,resolution,source_ref
EX-01,DEDUP_CONFLICT,AST,,"Aster submitted two quotes with dedup_key AST|NC-RFQ-0042|B. Original (doc-aster-qb-1047) stated_total_minor: 604000; Rev2 (doc-aster-qb-1047-rev2) stated_total_minor: 604001. Rev2 wins by later issued_at (2026-01-14T11:30:00 > 2026-01-10T09:00:00). Totals differ by one cent — deliberate, not corrected.",Rev2 lines carried forward; original superseded.,doc-aster-qb-1047|doc-aster-qb-1047-rev2
EX-02,EXPIRED_VALIDITY,CDR,NC-1001-A|NC-1002-A|NC-1003-B|NC-1004-A|NC-1005-A,Quote valid_until 2026-01-20 < award_decision_date 2026-02-01. Quote expired at moment of award.,Quote visible but ranking_eligible false on all lines; human review required,doc-cedar-cw-77
We keep the truth material out of the agent input and read it with the verifier only after the run. The output keeps its distinct name unified-records.csv; we check the artifact rather than rely on a model's claim that it passed. compare-json similarly checks the required extraction projection across two fresh attempts and reports full-JSON and byte variation instead of rejecting it.
The quality observer is an event observer, not an artifact inspector. Its Eta prompt (a small template that substitutes values from the event context) receives event data only when it interpolates it.events; the captured report says that artifact existence, row coverage, and correctness cannot be verified from completion events. The separate sentinel-sweep fixture demonstrates this boundary: its captures show both a no-event-data response and a templated report, rather than another anchor run.
Quality-observer capture: completion metadata.
# Codon Completion Summary: `validate-and-repair`
**Status:** Success
**Cost:** $<cost>
**Duration:** 197.8 seconds
**Exit Status:** Success
Quality-observer capture: explicit inspection limits.
- **Artifact existence cannot be verified** — no file.updated events provided
- **Row coverage cannot be verified** — no file contents or update events provided
- **Correctness cannot be verified** — no file contents or update events provided
Sentinel sweep capture: a report that receives interpolated events.
---
# Sweep Report
1. Received 1 event of type `codon.completed`.
2. Codon `step-two` completed successfully with a cost of $<cost> and duration of <ms>ms.
3. Sweep: clean
Sentinel sweep capture: event and output records.
{"id":"<id>","timestamp":"<ts>","type":"codon.completed","data":{"codonId":"step-one","success":true,"cost":"<n>","duration":"<n>","exitStatus":{"type":"success"}}}
{"id":"<id>","timestamp":"<ts>","type":"codon.completed","data":{"codonId":"step-two","success":true,"cost":"<n>","duration":"<n>","exitStatus":{"type":"success"}}}
{"id":"<id>","timestamp":"<ts>","type":"sentinel.triggered","data":{"sentinelId":"sweep-observer","codonId":"step-two","triggerNumber":1,"strategy":"immediate","eventCount":1,"queueSize":0}}
{"id":"<id>","timestamp":"<ts>","type":"sentinel.output","data":{"sentinelId":"sweep-observer","codonId":"step-two","triggerNumber":1,"outputType":"text","content":"I don't see any events in your message. You've described a scenario involving codon completion events and asked me to write a sweep report, but no actual event data has been provided for me to analyze.\n\nTo write the three-line report you've requested, I would need to see the events themselves. Could you please share:\n\n- The event log or event stream from the two-codon run\n- Details about each `codon.completed` event (success/failure status, costs if applicable)\n\nOnce you provide the events, I can generate:\n1. Count of codon.completed events and their success status\n2. Total cost calculation\n3. Sweep status assessment\n\nPlease paste the events and I'll write your report.","cost":"<n>","tokens":{"input":102,"output":158},"eventCount":1}}
Check-it: run the verifier and both renderer checks; then inspect the structured CSV and ledger. Treat the sentinel log only as evidence of completion-event reporting, never as proof of file contents.
Measure the run before setting a budget#
The dated current measurement is $0.88303367 in tracked codon cost for the seven-codon execution captured on 2026-09-06. It is not a promise about provider billing, retries, startup health checks, or sentinel calls. The configured $9.00 sum is a set of independent caps, not a predicted bill. The receipt below records the runtime, scope, tracked cost, and per-codon model split for that capture.
Cost receipt: runtime, scope, tracked cost, and model split.
{
"runtime": "0.10.0",
"captured_at": "2026-09-06T13:07:23.154353+00:00",
"scope": "one complete live execution",
"tracked_codons_cost": 0.8830336700000001,
"models": {
"normalize-aster": "haiku",
"normalize-beacon": "haiku",
"normalize-cedar": "haiku",
"survey-and-extracts": "haiku",
"validate-and-repair": "pi/baseten/deepseek-ai/DeepSeek-V4-Pro",
"reconcile": "pi/baseten/deepseek-ai/DeepSeek-V4-Pro",
"award-brief": "haiku"
},
"codons": [
{
"codonId": "normalize-aster",
A cost stop on onExceeded: "fail" forfeits the spend already incurred by that codon. The anchor therefore gives expensive stages complete where a partial artifact can still be inspected, while reconcile deliberately keeps fail because a truncated 40-row table is worse than no table. Size caps from measured workload rather than treating the cap arithmetic as a price estimate.
The original golden run used sonnet at ~$2.41. That is historical context, not the current model split or current measured cost.
Extend the example without losing its checks#
As we adapt the example, keep the 19 failure fixtures as regression targets. The generic validation checks cover unknown part aliases, unsupported substitutions, per-pack-as-per-each pricing, missing currency, quote revision A, and expired quotes.
For a designed extension, onboard Granite by turning the UNKNOWN_TEMPLATE quarantine into a normalize path. Another extension could add a codon that digitizes the RFQ instead of using the hand-transcribed reference, or reuse the sentinel configuration on more codons. For the optional maturity exercise – a repeatability check for a changed model tier – run the judgment codons at a cheaper tier such as --model haiku, then run python3 ../verify.py anchor out so the resulting unified-records.csv and exception pairs are checked against truth/expected.csv and truth/exceptions.csv. Keep compare-json for two fresh whole-hank runs with distinct paths (--execution exec-a -o out-a, then --execution exec-b -o out-b); it checks only the three dialect normalizers' required projection – identity, raw parts, quantities, prices and basis, status, and citations – and reports full-JSON and byte variation rather than rejecting it.