Observe quality and budget a hank

The pipeline from the previous chapters normalizes three supplier quotes and reconciles them into unified records, but so far nothing watches it run and nothing limits what it spends. In this chapter we add both: a sentinel that observes the validation step's completion event and reports what it can see, and a budget block on every codon that caps dollars, time, and tokens. Along the way we will validate the resolved plan before spending anything, run the chapter against prepared inputs, and check both the sentinel's report and the reconciled artifacts afterward.

By the end you will know when a sentinel is the right tool and when a review belongs in a codon, how codon-level caps and the operator's run envelope interact, and how to read the evidence a run leaves behind.

What changes after chapter 3?#

We begin in the shipped chapters/ch4/ directory. A checkpoint is a persisted run record, not the starting files for this chapter, so the chapter directory itself is the starting point.

The shipped chapters/ch4/ hank adds two independent concerns: a quality-observer sentinel (a parallel event observer) watching the validate-and-repair completion event, and a budget block on every codon. A hank is the JSON run definition, and a codon is one agent task in its sequence. We now have five codons: three normalizers, validate-and-repair, and reconcile. We still work with three suppliers – AST, BCN, and CDR – across five buyer part IDs, or 15 supplier-part data rows.

Before running anything, get the fixtures and credentials in place. Tutorial chapters are shipped directories with their own hank.json; they are not checkpoint tags to check out or roll back. Use the ch4 directory against a prepared task-input directory. The credentials ladder is also part of the setup: chapters 1–2 use ANTHROPIC_API_KEY, while chapters 3–5 and the full anchor additionally need BASETEN_API_KEY for pi/baseten/deepseek-ai/DeepSeek-V4-Pro. Ch4's sentinel uses the full registry ID anthropic/claude-haiku-4-5, not a codon shortcut or a pi/ spelling.

The fixture bundle's publication path is hankweave-fixtures-0.11.0.tar.gz; individual artifacts use /fixtures/0.11.0/files/<relative-file>. That archive already contains one top-level hankweave-fixtures-0.11.0/ directory. From its parent, run tar -xzf hankweave-fixtures-0.11.0.tar.gz, then enter hankweave-fixtures-0.11.0/chapters/ch4. Do not create another directory with that name before extracting.

For the supplied bunx/npx recipe, use Node >=22.19.0; Bun supplies the chosen bunx package launcher. An npm user can substitute npx hankweave@0.11.0 once for bunx hankweave@0.11.0. Run as a non-root user with Bun, Git, Python 3 (the python3 interpreter), ANTHROPIC_API_KEY, and BASETEN_API_KEY available. A passing preflight is not evidence that a Baseten key is valid; Baseten is absent from Hankweave's explicit Pi credential-enforcement map.

How can a sentinel watch without editing?#

A sentinel is a parallel observation agent with its own model, trigger, execution strategy, and output. It observes matching events and fires a report; it does not edit codon outputs or run tools. It does not block the main agent while the codon runs, though queued sentinel work is drained at the codon boundary before the next codon starts. Use a codon instead when the review must read the workspace or change an artifact. That read-only boundary shapes everything about how the ch4 observer is configured, so we will build it up piece by piece.

Attach the quality observer#

The ch4 codon attaches a reusable configuration with a wrapper object. The sentinels array on the codon points at a standalone sentinel file rather than inlining the whole definition:

JSON
      "sentinels": [
        {
          "sentinelConfig": "sentinels/quality-observer.json"
        }
      ],

The referenced file defines when the observer fires. The quality observer's event trigger is type: "event", listens for codon.completed, and applies an equals condition to the codonId field with value validate-and-repair. Its immediate execution strategy fires for every matching event. Other supported strategies are debounce (quiet-period milliseconds 1–300000), count (a threshold of 1–1000), and timeWindow (milliseconds 1–3600000). Conditions can use equals, notEquals, in, notIn, contains, matches, greaterThan, or lessThan on event paths. The schema strip below summarizes the trigger and execution fields:

Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
typestringyes= event
onarray<string>yesminItems 1
conditionsarray<object | object | contains | matches | object>no
immediateobjectstrategy = immediate
debounceobjectstrategy = debounce
↳ millisecondsintegeryes> 0; max 300000
countobjectstrategy = count
↳ thresholdintegeryes> 0; max 1000
timeWindowobjectstrategy = timeWindow
↳ millisecondsintegeryes> 0; max 3600000

A sentinel also needs a prompt and an output destination. At least one of userPromptText (inline) or userPromptFile (a relative path with / separators) is required; when both are present, Hankweave loads the files first and combines them with the inline text. A system prompt can likewise be inline or a relative file, and joinString joins multiple events for a text output. We will use the fixture's output settings here. To choose another output format or path, configure lastValueFile, or change attachment settings, see sentinel configuration.

Here is the complete shipped observer configuration. Note the userPromptText: it interpolates event data with Eta, a template language, and it instructs the model to report only what the events contain. The template does not grant the sentinel filesystem tools:

JSON
{
  "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"
  }
}

The template context contains up to 1,000 events, the codon identity and metadata, and the current time. Event data reaches the model only where the prompt interpolates it. A separate two-codon sentinel-sweep fixture demonstrates both sides of that boundary, and it also demonstrates why the prompt must carry the events expression. Its two captures come from two different runs of the fixture, both recorded on 2026-09-04 (the caption below names the runtime): the first run's prompt had no events expression, and the model answered that it saw no events; the later run's Eta-templated prompt received one matching codon.completed event and produced a report. They are not one run, and neither is current-release output.

Captured: 2026-09-04 · runtime: hankweave@0.10.0

Output
# 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
Output
{"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}}

One spelling detail matters here. Use the current ch4 spelling, anthropic/claude-haiku-4-5. A historical bare haiku spelling in a sentinel configuration produced No LLM provider available. The haiku entries in the captured plan refer to normalizer codons, a separate model-resolution path. For provider setup, see authentication and models.

The same sentinel design fired for real in the anchor build's re-proven run: its report records status Success, a duration of 117,863 ms, the cost rendered as an unresolved $<cost> placeholder, and the same three verification limitations. The observer summarizes its completion-event input; it does not certify artifacts.

The bare output filename has a managed location. With output.file: "quality-observer.log", the report is written to:

<execution>/.hankweave/sentinels/outputs/quality-observer/quality-observer.log

A path containing a slash is a different case: it resolves relative to the run's working directory (agentRoot) when present, otherwise the execution directory. In particular, the bare filename is not placed in the external -o out directory.

Check-it: after validate-and-repair, inspect the managed path and confirm that the captured report contains completion status, duration, and the output-token alert while explicitly declining to verify artifacts.

Why do budgets have two owners?#

The sentinel watches spending; the budget blocks limit it. A budget has two owners. The author uses codon-level caps to express what each step may spend or how long it may run; the operator supplies the run envelope with --max-cost <dollars> and --max-time <seconds>. At codon level, budget supports maxDollars, maxTimeSeconds, maxOutputTokens, maxContextTokens (the input-plus-output high-water mark per turn), and onExceeded: "complete" or "fail".

At hank level, overrides.budget adds allocation and shares; its onExceeded is the default for codons. The four currencies are dollars, wall-clock time, model output tokens, and per-turn context tokens. The pool that allocation and shares distribute is the dollar pool. Output-token and per-turn context-token caps exist only at codon level; wall-clock time is a codon cap in ch4, and a hank-level budget or --max-time can also carry a run-level time ceiling that codons draw against (below).

The precedence is important. First, runtime configuration and hank-level ceilings combine as their tightest shared ceiling. If a CLI --max-cost or --max-time is present, that CLI value is the highest-priority override: it can lower or raise the hank-level ceiling. A codon's own dollar cap is still applied separately as a ceiling against its resolved allocation. Thus overrides.budget of $12 with --max-cost $5 resolves to a $5 global ceiling, while --max-cost $20 can raise that run-level pool without loosening a codon's own cap. Both flags are listed in the shipped --help, so the resolved plan table at --validate is not the only confirmation they are active. The schema strip below covers the hank-level allocation fields:

Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
allocationenumsharednoshared | proportional | proportional-strictHow the dollar budget is distributed among children. 'shared': first-past-the-post (default). 'proportional': pre-allocate shares, unspent flows back. 'proportional-strict': pre-allocate shares, unspent evaporates.
sharesobject<number>noMap of child codon/loop ID to fraction (0-1) of the budget.
onExceededenumnocomplete | failWhat happens when budget is exceeded. 'complete' = graceful completion (default). 'fail' = codon failure, triggers onFailure policy.

Check-it: find shared, proportional, and proportional-strict in the generated budget strip; use the CLI reference for the flags and Hank JSON reference for the codon budget shape.

How does Hankweave resolve competing caps?#

With two owners setting limits, the runtime needs a deterministic way to combine them. The default allocation: "shared" is first-past-the-post: codons draw from one pool in execution order, so early thrift leaves more for later codons. proportional pre-allocates shares and returns unspent money to the pool. proportional-strict pre-allocates shares and lets unspent money evaporate.

For each codon, the dollar resolution chain is:

  1. Set the global ceiling to the CLI --max-cost value when present; otherwise use the tightest runtime/hank ceiling.
  2. Subtract spend to find the remaining pool.
  3. Allocate from that pool according to shared, proportional, or proportional-strict.
  4. Apply the codon's own cap. A codon with a shares entry gets min(allocated, codon.maxDollars). A capped codon with no share ignores the mode allocation and gets min(maxDollars, remaining pool), or its maxDollars alone when no global ceiling exists.
  5. A codon is uncapped only when it has neither a maxDollars cap nor a global ceiling; otherwise it gets the allocation mode's limit (the whole remaining pool under shared).

Time and token caps are not shares of the dollar pool: ch4 sets them at codon level, while a hank-level budget or --max-time can also impose a run-level time ceiling. onExceeded: "complete" marks the codon completed and allows downstream codons to run. onExceeded: "fail" marks it failed and invokes that codon's onFailure policy; see codon failure handling.

The interrupt is separate from onExceeded. Metered assistant messages update the budget tracker; when accumulated cost or output tokens reach a cap, the tracker emits exceeded and the codon runner requests SIGTERM. A cap can therefore interrupt after an already-billable message; it is not a prepaid-spending guarantee. A completion event can carry a budgetExceeded object such as { "currency": "outputTokens", "limit": 8000, "used": 11261 }.

Loop budgets are enforced differently from codon budgets. The one-second watchdog exists only when the codon or its loop sets maxTimeSeconds, and it raises a loop breach only for a loop's own time limit, always reported as currency: "duration". A loop that sets only maxDollars is checked at the codon boundary, while the running codon is stopped by its own derived cost cap. A loop's budget exhaustion never appears as a terminationReason in loop.iteration.completed: that field is optional, the runtime sets it only on a loop's final iteration and only to context_exceeded or iteration_limit, and sentinel_skip and failure are schema-allowed values that nothing emits.

Check-it: compare a plan's onExceeded values with the resolution rule: complete continues downstream work, while fail invokes the codon's failure policy.

Why do our codons need different caps?#

The resolution rules are uniform, but the ch4 caps are not, because the five codons do different kinds of work. Every ch4 codon carries a budget block. Each normalize-* codon has maxDollars: 0.50, maxTimeSeconds: 240, and onExceeded: "fail". validate-and-repair has a $1.00 cap, a 600-second cap, maxOutputTokens: 8000, and onExceeded: "complete". reconcile has a $0.50 cap, a 420-second cap, and onExceeded: "fail".

The normalize steps are mechanical extraction and fail loudly if they cannot complete. Validation is judgment work: a soft completion cap can preserve useful partial output. Reconciliation is an all-or-nothing join, so a partial result is worse than no result. The five per-codon dollar caps sum to $3.00. The two blocks below show the contrast between the soft validation policy and the hard reconciliation policy:

JSON
      "budget": {
        "maxTimeSeconds": 600,
        "maxDollars": 1,
        "maxOutputTokens": 8000,
        "onExceeded": "complete"
      }
JSON
      "budget": {
        "maxTimeSeconds": 420,
        "maxDollars": 0.5,
        "onExceeded": "fail"
      }

The normalize caps were raised from $0.20 after haiku price drift reached $0.204 and forfeited a hard-cap attempt in the re-proven anchor run. The lesson is not that these captured amounts are universal; they are the shipped ch4 arrangement for this workload.

Check-it: compare the two budget blocks: validate-and-repair uses the soft complete policy, while reconcile uses the hard fail policy.

What did the anchor build teach us?#

The shipped caps reflect experience from building the full anchor, a larger workload than ch4. These are captured lessons, not predictions for every ch4 run.

  • Leave room above an estimate. Three-supplier estimates tripped on an eight-supplier workload: validate-and-repair reached $1.05 against $1.00, and reconcile reached $0.51 against $0.50. When workload is uncertain, the cap-2x rule says to leave at least twice the estimate.
  • Hard cost aborts forfeit spend. When a cap trips, the runtime logs Budget exceeded for <codon> and requests the codon's interruption (SIGTERM); both onExceeded policies interrupt the codon and differ only in the recorded status. A failed codon's outputFiles are not copied, so its spend buys no delivered output: two hard-cap aborts in the anchor build burned $5.10 with nothing delivered, while a later review completed under a $7 soft cap at $1.61. The budget outcome is settled before the attempt's codon.completed event, so the event itself is a reliable record of whether the codon ended completed or failed.
  • Time enforcement is active, not a teardown guarantee. Elapsed time is checked on cost updates and by a 1,000 ms watchdog; on exceed, the codon runner requests SIGTERM. That request does not prove prompt harness (the process managing the run) teardown. An older captured run observed a codon surviving a 2,400-second cap for roughly 24,923 seconds; its source/runtime cause remains unresolved. For unattended work, add an external process deadline rather than claiming the watchdog recovers a checkpoint.
  • Price your zai route per model. Zai pricing is per model, not per provider. The registry prices glm-4.7, glm-5-turbo, glm-5.2, glm-5.3, and glm-5.3-flash, so a codon on one of them reports real cost and maxDollars can stop it. glm-5.2-highspeed and glm-5.3-highspeed resolve to coding-plan entries with a present zero cost, so every message adds 0 and no dollar cap can trip for them; guard those two with maxTimeSeconds, maxOutputTokens, or maxContextTokens.
  • Read budget events carefully. A budget-exceeded abort was logged as aborted by user in the event stream. That diagnosis is misleading; it is still a budget breach.

How do we read the resolved plan?#

Before spending tokens on a run, validate it:

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

It validates the configuration and displays the resolved per-codon caps, their source, allocation mode, onExceeded policy, and sentinel count; it does not run codons or provider health checks. The captured ch4 plan has five codons: the three normalizers use haiku, while validate-and-repair and reconcile use pi/baseten/deepseek-ai/DeepSeek-V4-Pro. It shows one sentinel on validate-and-repair, onExceeded: complete there, and onExceeded: fail on reconcile. The full capture appears below; read the budget table to confirm each codon's cap and policy before trusting the GOOD TO RUN! verdict.

Output

╭────────────────────────────────────────────────────────────────────╮
│  Hankweave v0.11.0                                                 │
│  linux x64 • node v22.23.2                                         │
╰────────────────────────────────────────────────────────────────────╯

Calculating data signature for validation...

> Validating configuration: <workspace>/fixtures/chapters/ch4/hank.json

  Data source:    <workspace>/fixtures/quote-template-unification
  Execution path: ~/.hankweave-executions/validation-<id>

✓ Configuration is valid!

╭──────────────────────────────────────────────────────────────────────────────╮
│  quote-template-unification v1.0.0                                           │
│  5 codons • 0 loops                                                          │
╰──────────────────────────────────────────────────────────────────────────────╯

├─ [1] normalize-aster (Normalize Aster (Datalab dialect))
│     model: haiku │ mode: fresh │ prompts: 1 (72 lines)
│     checkpointedGlobs: 1
│     rigs: cmd: mkdir -p pipeline
│     ↓
├─ [2] normalize-beacon (Normalize Beacon (Reducto dialect))
│     model: haiku │ mode: fresh │ prompts: 1 (71 lines)
│     checkpointedGlobs: 1
│     ↓
├─ [3] normalize-cedar (Normalize Cedar (generic-OCR dialect))
│     model: haiku │ mode: fresh │ prompts: 1 (71 lines)
│     checkpointedGlobs: 1
│     ↓
├─ [4] validate-and-repair (Validate & Repair Envelopes)
│     model: Pro │ mode: fresh │ prompts: 1 (67 lines) │ checkpointedGlobs: 2
│     sentinels: 1
│     ↓
└─ [5] reconcile (Reconcile to Unified Records)
      model: Pro │ mode: fresh │ prompts: 1 (41 lines) │ checkpointedGlobs: 2


Budget
─────────────────────────────────────────────────────────────────
  No global budget. Per-codon limits only.

  Codon                Model       Max Dollars                       Max Time          Max Tokens          On exceeded
  ─────                ─────       ───────────                       ────────          ────────            ───────────
  normalize-aster      Claude Ha…  $0.50 (codon cap)                 240s (cap)        —                   ⚠ fails run
  normalize-beacon     Claude Ha…  $0.50 (codon cap)                 240s (cap)        —                   ⚠ fails run
  normalize-cedar      Claude Ha…  $0.50 (codon cap)                 240s (cap)        —                   ⚠ fails run
  validate-and-repair  DeepSeek …  $1.00 (codon cap)                 600s (cap)        8000 (output cap)   completes
  reconcile            DeepSeek …  $0.50 (codon cap)                 420s (cap)        —                   ⚠ fails run

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

Run it:  hankweave hank.json <data_path>

Environment Variables:

  From System (HANKWEAVE_ prefixed):
    - CAPTURE_VERSION: 0.11.0
exit=0

The same preflight warns about a codon cap larger than its allocation, shares below 1.0 with no absorber, missing model pricing, and onExceeded: "fail" paired with onFailure: "retry". These are preflight warnings, not runtime surprises. The validation capture is a plan snapshot; use the current ch4 hank as the source of model and cap values if the two differ.

Check-it: bunx hankweave@0.11.0 hank.json task-data --validate prints the budget table, and the captured plan ends with GOOD TO RUN!, shows one sentinel on validate-and-repair, complete for that codon, and fail for reconcile.

How do we check ch4's output?#

With the plan validated, we can run the chapter and examine what it produces. From hankweave-fixtures-0.11.0/chapters/ch4, prepare the task-only input once, validate, run, and let the held-out checker verify the artifacts. The destination must not already exist; preparation refuses to overwrite it. It copies only the task inputs, while truth, generator data, and planted failure specimens stay outside the agent's input.

⌁ Terminal
python3 ../../verify.py prepare-data ../../quote-template-unification task-data
bunx hankweave@0.11.0 hank.json task-data --validate
bunx hankweave@0.11.0 hank.json task-data --headless --start-new --execution exec --max-cost 9 --shim-idle-timeout 1800 --overwrite-output -o out
python3 ../../verify.py chapter 4 out

Use both keys before the paid run. --validate does not run codons or provider health checks. Reuse task-data for repeats, but use --start-new and distinct --execution and -o paths for an independent attempt. A fresh directory is the next chapter handoff; there is no ch4 checkpoint tag to roll back to.

The output contract includes the three supplier envelopes, validated-records.json, exception-ledger.json, and unified-records.csv. The sentinel observes completion-event fields only; the deterministic (repeatable for the same inputs) Python checker verifies artifact existence, row coverage, and correctness.

The captured per-codon receipt is the source for the dated cost and model record:

JSON
{
  "runtime": "0.11.0",
  "captured_at": "2026-09-27T08:14:35.768788+00:00",
  "scope": "one complete live execution",
  "tracked_codons_cost": 0.26905572,
  "models": {
    "normalize-aster": "haiku",
    "normalize-beacon": "haiku",
    "normalize-cedar": "haiku",
    "validate-and-repair": "pi/baseten/deepseek-ai/DeepSeek-V4-Pro",
    "reconcile": "pi/baseten/deepseek-ai/DeepSeek-V4-Pro"
  },
  "codons": [
    {
      "codonId": "normalize-aster",
      "success": true,
      "cost": 0.0550862,
      "duration": 50748,
      "exitStatus": {
        "type": "success"
      }
    },
    {
      "codonId": "normalize-beacon",
      "success": true,
      "cost": 0.03556245,
      "duration": 26718,
      "exitStatus": {
        "type": "success"
      }
    },
    {
      "codonId": "normalize-cedar",
      "success": true,
      "cost": 0.0444871,
      "duration": 32363,
      "exitStatus": {
        "type": "success"
      }
    },
    {
      "codonId": "validate-and-repair",
      "success": true,
      "cost": 0.07089717,
      "duration": 65361,
      "exitStatus": {
        "type": "success"
      },
      "budgetExceeded": {
        "currency": "outputTokens",
        "limit": 8000,
        "used": 11261
      }
    },
    {
      "codonId": "reconcile",
      "success": true,
      "cost": 0.0630228,
      "duration": 31507,
      "exitStatus": {
        "type": "success"
      }
    }
  ],
  "task_data_scope": "task inputs only; oracle/generator/failure fixtures excluded"
}

That capture records 5/5 codons completed at tracked codon cost $0.26905572. validate-and-repair exceeded its output-token cap, with budgetExceeded at 8,000/11,261, but completed under its soft onExceeded: "complete" policy. These are values from one captured execution, not a guarantee for another workload.

The reconciled CSV has exactly 15 data rows plus its header. All five CDR rows have ranking_eligible: false: EX-03 marks the whole expired-at-award Cedar quote regardless of per-line status, and NC-1004-A is also NO_BID under EX-02. The CSV and the exception ledger appear together below so you can trace each ledger entry to the rows it affects.

CSV
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
AST,NC-1001-A,B,500,410,PER_EACH,205000,USD,QUOTED,true,doc-aster-qb-1047-rev2,q-aster-qb-1047-rev2,doc-aster-qb-1047-rev2-L1,doc-aster-qb-1047-rev2-b1
AST,NC-1002-A,B,2000,79,PER_EACH,158000,USD,QUOTED,true,doc-aster-qb-1047-rev2,q-aster-qb-1047-rev2,doc-aster-qb-1047-rev2-L2,doc-aster-qb-1047-rev2-b2
AST,NC-1003-B,B,1000,115,PER_EACH,115000,USD,QUOTED,true,doc-aster-qb-1047-rev2,q-aster-qb-1047-rev2,doc-aster-qb-1047-rev2-L3,doc-aster-qb-1047-rev2-b3
AST,NC-1004-A,B,25,640,PER_EACH,16000,USD,QUOTED,true,doc-aster-qb-1047-rev2,q-aster-qb-1047-rev2,doc-aster-qb-1047-rev2-L4,doc-aster-qb-1047-rev2-b4
AST,NC-1005-A,B,5000,22,PER_EACH,110000,USD,QUOTED,true,doc-aster-qb-1047-rev2,q-aster-qb-1047-rev2,doc-aster-qb-1047-rev2-L5,doc-aster-qb-1047-rev2-b5
BCN,NC-1001-A,B,500,395,PER_EACH,197500,USD,QUOTED,true,doc-beacon-8821,q-beacon-8821,doc-beacon-8821-L1,doc-beacon-8821-b1
BCN,NC-1002-A,B,2000,81,PER_EACH,162000,USD,QUOTED,true,doc-beacon-8821,q-beacon-8821,doc-beacon-8821-L2,doc-beacon-8821-b2
BCN,NC-1003-B,B,1000,115,PACK,115000,USD,QUOTED,true,doc-beacon-8821,q-beacon-8821,doc-beacon-8821-L3,doc-beacon-8821-b3
BCN,NC-1004-A,B,25,675,PER_EACH,16875,USD,QUOTED,true,doc-beacon-8821,q-beacon-8821,doc-beacon-8821-L4,doc-beacon-8821-b4
BCN,NC-1005-A,B,5000,19,PER_EACH,95000,USD,QUOTED,true,doc-beacon-8821,q-beacon-8821,doc-beacon-8821-L5,doc-beacon-8821-b5
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
JSON
[
  {
    "exception_id": "EX-01",
    "type": "DEDUP_CONFLICT",
    "supplier_code": "AST",
    "buyer_part_id": "NC-1001-A|NC-1002-A|NC-1003-B|NC-1004-A|NC-1005-A",
    "detail": "Two submissions share dedup_key AST|NC-RFQ-0042|B with differing stated_total_minor: doc-aster-qb-1047=604000, doc-aster-qb-1047-rev2=604001. rev2 (issued 2026-01-14T11:30:00) supersedes original (issued 2026-01-10T09:00:00) by later issued_at.",
    "resolution": "rev2_wins_by_later_issued_at",
    "source_ref": "doc-aster-qb-1047|doc-aster-qb-1047-rev2"
  },
  {
    "exception_id": "EX-02",
    "type": "NO_BID",
    "supplier_code": "CDR",
    "buyer_part_id": "NC-1004-A",
    "detail": "Supplier declined to quote this part (status_hint: NO_BID). Line excluded from ranking.",
    "resolution": "excluded_from_ranking",
    "source_ref": "doc-cedar-cw-77-b4"
  },
  {
    "exception_id": "EX-03",
    "type": "EXPIRED_VALIDITY",
    "supplier_code": "CDR",
    "buyer_part_id": "NC-1001-A|NC-1002-A|NC-1003-B|NC-1004-A|NC-1005-A",
    "detail": "Quote valid_until (2026-01-20) is before RFQ award_decision_date (2026-02-01). The quote is expired at the moment of award.",
    "resolution": "show-flagged",
    "source_ref": "doc-cedar-cw-77",
    "judgment_call": true,
    "options": ["exclude", "show-flagged"],
    "tentative_resolution": "show-flagged",
    "confidence": 0.5
  }
]

The ledger carries three typed entries: EX-01 DEDUP_CONFLICT, EX-02 NO_BID, and EX-03 EXPIRED_VALIDITY. The checker, not the observer, establishes that contract.

Check-it: python3 ../../verify.py chapter 4 out is the executable assertion for the 15-row CSV, the three exception types, and the rest of the output contract.

Finally, inspect the managed observer output, not the external out directory:

exec/.hankweave/sentinels/outputs/quality-observer/quality-observer.log

The fired report contains the completion status, the 65,361 ms duration, the output-token alert at 8,000/11,261, and validate-and-repair's numeric cost of 0.07089717. It explicitly says that artifact existence, row coverage, and correctness cannot be verified from completion-event input. The receipt above carries the full per-codon dollar costs.

Output

---
# Codon Completion Summary: `validate-and-repair`

**Status:** Success ✓

**Duration:** 65,361 ms (~65 seconds)

**Cost:** 0.07089717

**Budget Exceeded:**
- Currency: outputTokens
- Limit: 8,000
- Used: 11,261 (140.8% of limit)

---

**Limitations of this report:**
- No file contents or file.updated events provided
- **Artifact existence cannot be verified**
- **Row coverage cannot be verified**
- **Correctness cannot be verified**

Only the completion status and resource metrics are observable from this input.

When should we use a sentinel or codon?#

The ch4 run illustrates the division of labor. Use a sentinel for a read-only observation of named events and a sentinel-owned report. It sees only the event data that the template passes to its LLM; it cannot inspect files, run tools, edit outputs, or block the main agent. Use a separate codon for a review that needs architecture, broad workspace context, artifact inspection, or a change.

The observing model can differ from the observed codon's model. Ch4 uses the registry-spelled anthropic/claude-haiku-4-5 for the quality observer and pi/baseten/deepseek-ai/DeepSeek-V4-Pro for the two judgment/reconciliation codons. The anchor's captured plan likewise uses haiku for normalization and DeepSeek for validation and reconciliation. A cheaper one-shot observer does not become an artifact verifier merely because its prompt names output files.

Check-it: the observer report is evidence of completion-event observation; use the Python checker and the files it checks for artifact correctness.

Where can we look up details?#

  • For the complete two-party contract, currencies, allocation modes, preflight checks, and resume semantics, see Budgets.
  • For the sentinel concept, see Sentinels. Look up triggers, execution, output, template context, conversational mode, and structured output in Sentinel configuration.
  • Look up --max-cost and --max-time in the CLI reference, and --validate in its validation entry.
  • For the hank and codon budget schema, use the Hank JSON reference.
  • To choose onFailure policies (abort, retry, and ignore) and retryConfig, see Codons, including the warning for onExceeded: "fail" paired with onFailure: "retry".

For a fresh repetition, run the same ch4 hank with --start-new, a distinct --execution/-o pair, and the same prepared task-data. This compares independent executions under a chosen envelope without pretending that a resumed execution re-plans the model or proving an unrecorded model-tier experiment.