You're reading the 0.10.0 archive.

Sentinels observe event patterns

When a coding agent runs, it produces a stream of events: codons start and complete, files change, tools run, costs accumulate. Sometimes you want something watching that stream while the agent works – checking quality, tracking cost, narrating progress – without adding instructions to the agent's prompt or interrupting its run. That is what a sentinel does. A sentinel is a separate observation agent attached to a codon, the unit of work in a hank. It watches the event stream, makes its own LLM calls when a pattern it cares about appears, and writes a report for a later consumer.

The important boundary is in the name: observation is separate from action. A sentinel never edits the agent's files, never sends instructions back, and never blocks the run. This page builds up how that works: why sentinels exist instead of hooks, what one is and is not, how it decides to fire, how its lifecycle fits around a codon, and the design rules that keep observation safe. The configuration details come from the pinned 0.10.0 schema; the sentinel configuration reference carries the full field-by-field contract.

Why not hooks?#

A hook usually means a callback at a fixed lifecycle point. That model works when the question is "run this at codon start," but it is a poor fit for "when these event types appear together, under these conditions, produce a report." A sentinel watches patterns in the event stream and can combine event types with conditions, so its rule is declarative: when X, then Y, then Z. The shape of a sentinel's work, end to end, is:

Output
pseudocode:
sentinel: trigger(event-pattern) → accumulate(strategy) → fire(llm) → write(output-file)

A sentinel is also configured separately from the main workflow. A codon attaches a file reference or an inline configuration in its sentinels array, so you can add or remove an observer without changing the codon's prompt or agent logic. That separation is what makes observers cheap to experiment with: a cost watcher, a code reviewer, or a narrator can sit alongside the main agent, and changing how you monitor the work never changes the agent's task. The 0.10.0 contract described here is the version pinned by docs.lock.

The output format is deliberately narrow. In 0.10.0, a sentinel writes text or jsonl; the old json format was removed in 0.5.5.

What a sentinel is (and isn't)#

A sentinel is a parallel observation agent, not a logging hook. It watches events from the main workflow, makes its own LLM calls when a pattern matches, and writes to its own output files – a full parallel execution framework in miniature. What it does not do matters just as much: it runs no tools, sends no instructions back to the main agent, and never blocks the main agent's execution. Think of it as a note-taker, not an editor.

That boundary defines the safe data flow, which is one way: the main agent edits files X and Y, the sentinel watches and writes analysis to file A, and the agent or a later codon reads file A. Do not route a sentinel's output into a file that the main agent is editing; concurrent-edit protection can report a hash mismatch when another process changes that file.

Required configuration and attachment#

A sentinel needs five fields: id, name, trigger, execution, and model. The ID is a kebab-case string. Attachment happens in hank.json, on the codon – there is no root-level sentinels setting. The minimal config below shows the required fields, and the hank fragment shows how a codon attaches a sentinel by file reference:

JSON
{
  "id": "quality-observer",
  "name": "Quality Observer",
  "trigger": { "type": "event", "on": ["codon.completed"] },
  "execution": { "strategy": "immediate" },
  "model": "anthropic/claude-haiku-4-5"
}
JSON
{
  "hank": [
    {
      "id": "step-two",
      "sentinels": [
        { "sentinelConfig": "sentinels/sweep-observer.json" }
      ]
    }
  ]
}

The wrapper accepts either a sentinel config file path, as above, or an inline config object. Its optional settings can include failCodonIfNotLoaded, outputPaths, and reportToWebsocket. failCodonIfNotLoaded defaults to false: if loading fails, the runtime skips the sentinel; when it is true, the codon fails instead. The full field list, including prompt, output, and error-handling options, is:

Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
idstringyesminLength 1; pattern ^[a-z0-9-]+$
namestringyesminLength 1
descriptionstringno
triggerevent | sequenceyes
executionimmediate | debounce | count | timeWindowyes
systemPromptFilestring | array<string>noSystem prompt file(s), relative to this config file's directory (or the hank directory for inline configs), using '/' separators. Must stay inside the hank directory; absolute paths and symlinks are rejected.
systemPromptTextstringno
userPromptFilestring | array<string>noUser prompt file(s), relative to this config file's directory (or the hank directory for inline configs), using '/' separators. Must stay inside the hank directory; absolute paths and symlinks are rejected.
userPromptTextstringno
conversationalobjectno
errorHandlingobjectno
llmParamsobjectno
modelstringyesThe full model ID to use (e.g., "anthropic/claude-3-5-sonnet-20241022", "openai/gpt-4-turbo").
structuredOutputobjectno
joinStringstringnoString to join entries in text-based log file. Only valid for text output. Supports escape sequences: \n (newline), \t (tab), \r (carriage return), \ (backslash). Defaults to '\n---\n' for visual separation.
reportToWebsocketobjectno
outputobjectno
$schemastringnoJSON Schema URL for editor support

Two load-time behaviors are worth knowing before you rely on a sentinel. First, model validation: a full model ID containing /, such as anthropic/claude-haiku-4-5, is checked against the provider registry at load, and if it is absent the sentinel is skipped with an info-level log. A model without / is not registry-checked at load; it loads but fails on its first LLM call with No LLM provider available. Use a registry <provider>/<model> ID for a sentinel that must fire. Second, path rules: at 0.10.0, file references in a sentinel config and in its prompt or schema fields are strict portable relative POSIX paths. They cannot climb above the hank root or traverse a symlink, and for a file-based config, prompt and schema references resolve from that config's directory.

Finally, a sentinel is not an agent session with filesystem tools. It sees event data only when its Eta prompt – the template engine used for sentinel prompts – interpolates the template context. The context contains events (up to 1,000), the current codon (id, name, description, startTime), and world.currentTime. A prompt without an it.events interpolation receives no event data at all.

How a sentinel decides to fire#

Two configuration objects control firing. A trigger selects events and optional conditions; an execution strategy decides when matching events become one LLM call. The captures and schema strips below show the contracts for triggers, strategies, prompts, model parameters, structured output, and conversational mode, all pinned from the schema and linked in the sentinel configuration reference. The first capture is a real sentinel log, included because it demonstrates the model-ID behavior from the previous section: haiku passes validation but fails at firing with No LLM provider available, pi/google/gemini-2.5-flash is not found in the registry, and google/gemini-2.5-flash fires, writing to the managed sentinel path shown in the log header.

Output
# ./.hankweave/sentinels/outputs/quality-observer/quality-observer.log

---
Yes, it contains exactly the line `hello from hankweave` and nothing else.
Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
eventobjecttype = event
onarray<string>yesminItems 1
conditionsarray<object | object | contains | matches | object>no
sequenceobjecttype = sequence
interestFilterobjectyes
↳ ↳ onarray<string>yesminItems 1
patternarray<object>yesminItems 1
optionsobjectno
↳ ↳ consecutivebooleanno
Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
immediateobjectstrategy = immediate
debounceobjectstrategy = debounce
millisecondsintegeryes> 0; max 300000
countobjectstrategy = count
thresholdintegeryes> 0; max 1000
timeWindowobjectstrategy = timeWindow
millisecondsintegeryes> 0; max 3600000
Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
userPromptTextstringno
stringstringminLength 1
array<string>array<string>
systemPromptTextstringno
stringstringminLength 1
array<string>array<string>
Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
temperaturenumbernomin 0; max 2Temperature for response generation (0=deterministic, 2=creative)
maxOutputTokensintegerno> 0; max 100000Maximum tokens in the response
maxRetriesintegernomin 0; max 5Number of retry attempts for failed LLM calls
Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
outputenumyesobject | array | enum
schemaStrstringnoInline Zod schema code
schemaFilestringnominLength 1Path to a Zod schema file, relative to this config file's directory (or the hank directory for inline configs), using '/' separators. Must stay inside the hank directory; absolute paths and symlinks are rejected.
enumValuesarray<string>nominItems 1
schemaNamestringno
schemaDescriptionstringno
Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
trimmingStrategymaxTurns | maxTokensyes
continueOnErrorbooleanno

With the mechanics in view, here is a complete event-observer configuration. It fires on codon.completed for one codon, uses immediate execution, and asks the model to summarize only status, failure reasons, and budget fields present in the event. Note the prompt's explicit instruction that artifact contents are not available – the config encodes the observation boundary rather than leaving the model to guess.

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 captured report from that observer shows both the value and the limit of the separation. The observer summarizes completion status and budget fields, then states plainly that artifact existence, coverage, and correctness cannot be verified from its input:

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

**Status:** Success

**Cost:** $<cost>

**Duration:** 197.8 seconds

**Exit Status:** Success

---

...

- **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

To check those properties, use a rig – a setup or checker that reads artifacts – or a codon that reads them; a sentinel completion log is not proof. The same rule applies to a prompt file: prompt text and it.events are the inputs to the model, not a filesystem workspace. For the full set of trigger operators and strategies, use the reference page rather than assuming that a lifecycle callback has access to files.

The sentinel lifecycle across a codon#

Each codon has its own set of sentinels, and loading the next codon's set unloads the previous one. Within a codon, the lifecycle is easiest to reason about as four states: Loaded → Active → Completing → Unloaded. The figure shows both the ASCII and mermaid renderings of the same state machine:

FIG. 1 The sentinel lifecycle across a codon
Read the diagram as text
Output
sentinel lifecycle:
  load config → Loaded
       ↓
  observe routed events and queue matching work → Active
       ↓
  agent exits → Completing (drain sentinel work)
       ↓
  capture final state and cost → Unloaded → next codon

During codon execution, sentinel work is fire-and-forget. Event emission does not wait for a sentinel, so the main agent continues while the sentinel evaluates its queue. The runtime routes server-state and agentic-backbone events – runtime status and agent/tool activity – to sentinels. Connection-state events such as server.ready, pong, and history.batch are excluded, and so are sentinel events themselves, which prevents a sentinel from observing its own reports. The sentinel event types, with their payloads, are:

Scroll to explore the table →
idcategoryjournaledsentinelRoutedpayloadFieldsreceipts
sentinel.errorsentineltruefalsesentinelId, codonId, errorType, message, retriable, consecutiveFailureCountschemas/event-schemas.ts:709, schemas/event-schemas.ts:1001, schemas/event-schemas.ts:1264, sentinels/sentinel.ts:576, sentinels/sentinel.ts:663, sentinels/sentinel.ts:798, sentinels/sentinel.ts:905
sentinel.loadedsentineltruefalsesentinelId, codonId, model, triggerType, executionStrategy, conversational, source, sourcePath?schemas/event-schemas.ts:699, schemas/event-schemas.ts:999, schemas/event-schemas.ts:1262, hankweave-runtime.ts:5819
sentinel.outputsentineltruefalsesentinelId, codonId, triggerNumber, outputType, content, cost, tokens, eventCountschemas/event-schemas.ts:714, schemas/event-schemas.ts:1002, schemas/event-schemas.ts:1265, sentinels/sentinel.ts:538, sentinels/sentinel.ts:635, sentinels/sentinel.ts:762, sentinels/sentinel.ts:877
sentinel.triggeredsentineltruefalsesentinelId, codonId, triggerNumber, strategy, eventCount, queueSizeschemas/event-schemas.ts:719, schemas/event-schemas.ts:1003, schemas/event-schemas.ts:1266, sentinels/sentinel.ts:392
sentinel.unloadedsentineltruefalsesentinelId, codonId, reason, errorType?, finalCost, llmCallCountschemas/event-schemas.ts:704, schemas/event-schemas.ts:1000, schemas/event-schemas.ts:1263, sentinels/sentinel-manager.ts:851

At the codon boundary, the agent's exit moves the codon to completing-sentinels. This is the blocking part of the lifecycle: debounce timers fire, count buffers flush, time-window work is processed, and queued LLM calls finish. After the runtime emits codon.completed, it performs a second drain for sentinels watching that event, so a sentinel conditioned on codon.completed completes before the next codon starts. Final sentinel states and costs are captured after that second drain, then the sentinels are unloaded.

Two consequences follow. The cost carried by the codon.completed event itself does not include sentinel work triggered by that event. And in replay mode, which reuses recorded codon output, sentinels are intentionally skipped because they make real, nondeterministic LLM calls. For reference, the routable event categories a sentinel can trigger on are:

Scroll to explore the table →
idcategoryjournaledsentinelRoutedpayloadFieldsreceipts
archive.completedserver-statetruetruecodonId, archivedPathsschemas/event-schemas.ts:668, schemas/event-schemas.ts:977, schemas/event-schemas.ts:1257, hankweave-runtime.ts:5465
archive.partialserver-statetruetruecodonId, archivedPaths, failedPathsschemas/event-schemas.ts:673, schemas/event-schemas.ts:978, schemas/event-schemas.ts:1258, hankweave-runtime.ts:5450
assistant.actionagentic-backbonetruetruecodonId, action, content, toolName?, toolInput?schemas/event-schemas.ts:572, schemas/event-schemas.ts:986, schemas/event-schemas.ts:1237, hankweave-runtime.ts:2777, hankweave-runtime.ts:2789, hankweave-runtime.ts:2824
budget.summaryserver-statetruetrueceiling, allocation, rows, totalsschemas/event-schemas.ts:724, schemas/event-schemas.ts:979, schemas/event-schemas.ts:1267, hankweave-runtime.ts:6478
checkpoint.listserver-statetruetruerunId, checkpoints, currentBranchschemas/event-schemas.ts:637, schemas/event-schemas.ts:968, schemas/event-schemas.ts:1250, hankweave-runtime.ts:4255
codon.completedserver-statetruetruecodonId, success, cost, duration, exitStatus, failureReason?, failureIgnored?, budgetExceeded?schemas/event-schemas.ts:560, schemas/event-schemas.ts:961, schemas/event-schemas.ts:1235, hankweave-runtime.ts:1886, hankweave-runtime.ts:2064, hankweave-runtime.ts:2253, hankweave-runtime.ts:3365
codon.extendedserver-statetruetruecodonId, codonName, extensionNumber, exhaustWithPrompt, cumulativeTokens, cumulativeCostschemas/event-schemas.ts:565, schemas/event-schemas.ts:962, schemas/event-schemas.ts:1236, hankweave-runtime.ts:2946
codon.startedserver-statetruetruecodonId, codonName, codonDescription?, sessionId, previousSessionId?, startTime, promptMetadata?schemas/event-schemas.ts:555, schemas/event-schemas.ts:960, schemas/event-schemas.ts:1234, hankweave-runtime.ts:316
errorserver-statetruetruemessage, codon?, fatal, severity?, context?, code?schemas/event-schemas.ts:617, schemas/event-schemas.ts:967, schemas/event-schemas.ts:1246, hankweave-runtime.ts:975, hankweave-runtime.ts:995, hankweave-runtime.ts:1019, hankweave-runtime.ts:1062, hankweave-runtime.ts:1090, hankweave-runtime.ts:1116, hankweave-runtime.ts:1814, hankweave-runtime.ts:1867, hankweave-runtime.ts:2045, hankweave-runtime.ts:2236, hankweave-runtime.ts:2609, hankweave-runtime.ts:3981, hankweave-runtime.ts:4177, hankweave-runtime.ts:4273, hankweave-runtime.ts:4349, hankweave-runtime.ts:4512, hankweave-runtime.ts:4536, hankweave-runtime.ts:4599, hankweave-runtime.ts:4769, hankweave-runtime.ts:4804, hankweave-runtime.ts:4896, hankweave-runtime.ts:4918, hankweave-runtime.ts:5016
file.updatedagentic-backbonetruetruepath, filename, content, actionschemas/event-schemas.ts:587, schemas/event-schemas.ts:988, schemas/event-schemas.ts:1240, hankweave-runtime.ts:2357, hankweave-runtime.ts:3927
filetree.updatedagentic-backbonetruetruetreeschemas/event-schemas.ts:592, schemas/event-schemas.ts:989, schemas/event-schemas.ts:1241, hankweave-runtime.ts:3954
infoserver-statetruetruemessageschemas/event-schemas.ts:627, schemas/event-schemas.ts:966, schemas/event-schemas.ts:1248, hankweave-runtime.ts:1705, hankweave-runtime.ts:1728, hankweave-runtime.ts:1902, hankweave-runtime.ts:1933, hankweave-runtime.ts:1971, hankweave-runtime.ts:2080, hankweave-runtime.ts:2196, hankweave-runtime.ts:2269, hankweave-runtime.ts:2730, hankweave-runtime.ts:3082, hankweave-runtime.ts:3100, hankweave-runtime.ts:3181, hankweave-runtime.ts:3205, hankweave-runtime.ts:3474, hankweave-runtime.ts:3723, hankweave-runtime.ts:3755, hankweave-runtime.ts:4058, hankweave-runtime.ts:4328, hankweave-runtime.ts:6427
loop.iteration.completedserver-statetruetrueloopId, iteration, durationMs, costUsd, tokensUsed, isFinal, terminationReason?schemas/event-schemas.ts:612, schemas/event-schemas.ts:976, schemas/event-schemas.ts:1245, hankweave-runtime.ts:6035
rig.outputagentic-backbonetruetruecodonId, stream, line, commandIndexschemas/event-schemas.ts:607, schemas/event-schemas.ts:992, schemas/event-schemas.ts:1244, hankweave-runtime.ts:6109
rig.setup.completedagentic-backbonetruetruecodonId, rigType, commandCount, durationMs, createdCheckpointschemas/event-schemas.ts:597, schemas/event-schemas.ts:990, schemas/event-schemas.ts:1242, hankweave-runtime.ts:2160
rig.setup.failedagentic-backbonetruetruecodonId, failureType, exitCode?, commandIndex?, ignoredschemas/event-schemas.ts:602, schemas/event-schemas.ts:991, schemas/event-schemas.ts:1243, hankweave-runtime.ts:1791
rollback.archiveRestoreserver-statetruetruecodonId, restoredPaths, failedPaths?, statusschemas/event-schemas.ts:678, schemas/event-schemas.ts:974, schemas/event-schemas.ts:1256, hankweave-runtime.ts:5557
rollback.codonCheckpointserver-statetruetruecodonId, codonName, checkpoint, checkpointType, messageschemas/event-schemas.ts:647, schemas/event-schemas.ts:971, schemas/event-schemas.ts:1252, hankweave-runtime.ts:4684, hankweave-runtime.ts:5136, hankweave-runtime.ts:5191
rollback.completedserver-statetruetruefromRun, toRun, checkpoint, codonId, codonName, checkpointType, autoRestartschemas/event-schemas.ts:662, schemas/event-schemas.ts:972, schemas/event-schemas.ts:1255, hankweave-runtime.ts:4722, hankweave-runtime.ts:5254
rollback.progressserver-statetruetruecurrentStep, totalSteps, messageschemas/event-schemas.ts:657, schemas/event-schemas.ts:970, schemas/event-schemas.ts:1254, hankweave-runtime.ts:5117, hankweave-runtime.ts:5156
rollback.rigCleanupserver-statetruetruecodonId, codonName, directories, status, successfulCleanups?, failedCleanups?, error?schemas/event-schemas.ts:652, schemas/event-schemas.ts:973, schemas/event-schemas.ts:1253, hankweave-runtime.ts:5624, hankweave-runtime.ts:5662, hankweave-runtime.ts:5678
rollback.startedserver-statetruetruefromRun, fromCodon, toCodon, toCheckpoint, checkpointType, codonsToProcessschemas/event-schemas.ts:642, schemas/event-schemas.ts:969, schemas/event-schemas.ts:1251, hankweave-runtime.ts:4645, hankweave-runtime.ts:5095
server.idleserver-statetruetruereason, messageschemas/event-schemas.ts:632, schemas/event-schemas.ts:964, schemas/event-schemas.ts:1249, hankweave-runtime.ts:941, hankweave-runtime.ts:3621, hankweave-runtime.ts:3791, hankweave-runtime.ts:3812, hankweave-runtime.ts:4072, hankweave-runtime.ts:4743, hankweave-runtime.ts:5279, hankweave-runtime.ts:5291
state.snapshotserver-statetruetruecurrentCodon?, completedCodons, fileTree, totalCost, totalTime, recentFileAccess?, isRollingBackschemas/event-schemas.ts:550, schemas/event-schemas.ts:963, schemas/event-schemas.ts:1233, hankweave-runtime.ts:1422
state.transitionserver-statetruetruetransitionType, runId?, codonId?, transition, resultingStateschemas/event-schemas.ts:693, schemas/event-schemas.ts:975, schemas/event-schemas.ts:1261, hankweave-runtime.ts:394
token.usageserver-statetruetruecodonId, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, totalCost, modelId?, modelUsage?schemas/event-schemas.ts:577, schemas/event-schemas.ts:965, schemas/event-schemas.ts:1238, hankweave-runtime.ts:2646, hankweave-runtime.ts:2660
tool.resultagentic-backbonetruetruecodonId, toolUseId, toolName, result, truncated, originalLength, executionTimeMs, isErrorschemas/event-schemas.ts:582, schemas/event-schemas.ts:987, schemas/event-schemas.ts:1239, hankweave-runtime.ts:2906

The five sentinel event types – sentinel.loaded, sentinel.unloaded, sentinel.triggered, sentinel.output, and sentinel.error – are journaled and broadcast to WebSocket clients, but, as noted above, are not routed back to sentinels.

Designing with sentinels#

Start with who will use the report. A sentinel that writes a file the main agent is also editing can cause concurrent-edit protection to reject the edit, so give the observer its own report file and ask the agent or a later codon to read it. Both contributions stay available without two processes editing the same file.

That separation also makes observers swappable. Keep the event trigger and template shape, then change the prompt for a laziness check, mock detection, data-hygiene review, or shell-error watch. The sweep observer in fixtures/sentinel-sweep/ is a working example: it is attached to the run's final codon, and when that codon's codon.completed arrives, it fires immediately with one received event and writes the single end-of-run sweep report:

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

The sweep observer does not receive the earlier codon's completion, because sentinel sets are attached per codon. The captured event sequence shows the ordering directly: the final codon.completed event is followed by sentinel.triggered and sentinel.output, and those sentinel events are journaled but not fed back into sentinels.

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

That capture carries a second lesson. The sentinel.output content shows the model reporting that it received no events – because the first version of the sweep prompt did not interpolate it.events. The template context rule from earlier is not a formality; without the interpolation, the sentinel fires on an empty picture. The fixture also confirms that sentinels belong on a codon: a root-level sentinels array is rejected. See the sentinel configuration reference for the full sweep pattern and codon settings.

For output files, the two-line rule is:

  • a bare filename such as quality-observer.log resolves to the managed directory <execution>/.hankweave/sentinels/outputs/<sentinel-id>/, where <execution> is the run's execution directory;
  • a path containing /, including ./quality-observer.log, resolves relative to agentRoot, the configured agent workspace, when present.

The configured output path is independent of an external -o directory. The full output, lastValueFile, format, and codon-level settings.outputPaths contract is in the sentinel configuration reference.

Error handling and guardrails#

The design rules above lead to a practical division of labor. Use a sentinel for a check that fits in a paragraph: "don't use any," "every function needs a docstring," or "stay on task." Use a separate codon when the review needs architecture, related files, or trade-off reasoning. If a sentinel's finding must affect execution, have it write an output file and reference that file in a later codon's prompt or rig – the codon boundary is what separates observation from action.

Sentinels are optional infrastructure, not a prerequisite for an effective hank; the documentation pilots explicitly ran without them. Model selection has its own two-plane behavior – codon self-test versus provider health-check – and sentinel key overrides belong with authentication and model selection.

The runtime manages sentinels through SentinelManager. Conversational sentinel history persists under .hankweave/sentinels/history/; configured output files follow the output-path rule above. The reportToWebsocket setting controls visibility of lifecycle, error, output, and trigger events. The output-file, error-handling, and WebSocket reporting contracts are:

Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
formatenumnotext | jsonl
filestringno
lastValueFilestringno
Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
maxConsecutiveFailuresintegernomin 1Maximum consecutive failures before unloading sentinel. Default: 3
unloadOnFatalErrorbooleannoWhether to unload on fatal errors. Default: true
Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
lifecyclebooleanno
errorsbooleanno
outputsbooleanno
triggersbooleanno

The runtime also bounds queued work: at most 100 pending triggers and 10,000 queued events. When a cap is reached, the oldest queued trigger is dropped and an info-level log records the action. For the exact failure categories, consecutive-failure behavior, scoped keys, and reporting switches, use the sentinel configuration reference.