Operate a Hankweave run

A Hankweave run starts in one command and can be misread. Validation can pass while the launch command is wrong; a rerun can exit 0 having done nothing; a graceful stop can leave the run record showing failed. This runbook walks one run through its full arc: validate the hank, launch the intended execution, confirm what actually ran, watch progress, stop cleanly, and wire the result into CI. Each step ends with a Check-it line naming the evidence that tells you the step did what you expected.

Know where the run is#

Before the steps, one piece of vocabulary and one picture. A hank is the configuration describing the work; each codon is one sealed agent task in its sequence. A run moves through four states, and most operator actions are about moving between them or recovering from them.

FIG. 1 Run lifecycle: before-run → preflight → running → completed, with fix and recovery branches
Read the diagram as text
Output
// Pseudocode: reviewable ASCII source for the run lifecycle.
before-run -> preflight -> running -> completed
                 ^             ^
                 | fix         | resume / rollback / retry
                 +-------------+

Run lifecycle: before-run → preflight → running → completed, with fix and recovery branches.

The diagram tracks the run as a whole. Individual codons have their own finer-grained transitions, visible in the event journal described in Watch a run that says nothing; those labels are not additional run states. When a run lands on the fix branch, use troubleshooting. For the recovery branches, use resume, rollback, and retry.

Catch a broken hank before it spends money#

1. Validate before you launch. Configuration-only validation is the cheapest check Hankweave offers, so it comes first:

⌁ Terminal
bunx hankweave@0.11.0 --validate

Validation checks that the data source exists, computes its data signature, validates the hank including the strict-reference preflight, and runs a self-test for each unique model. It exits 0 when valid and 1 when invalid. The self-test is a harness, credential and catalog check, not a billable model call: it verifies the SDK imports, that an executable or credential is present, and that a Pi-routed model exists in the Pi catalog. Keys must be present, but no model generation runs, because validation initializes the provider registry with health checks off. The one network exception is a Bedrock codon: its credential check can probe EC2 instance metadata when no static AWS credential source is visible, so do not treat validation as a universal offline guarantee.

Validation runs before execution setup and creates no execution directory. Its printed Execution path is a would-be path: validation-<dataHash6> by default, the directory selected by -e when supplied, or a timestamp/random/data-hash name with --start-new. Validation is not side-effect-free: it writes a temporary validation log under the OS temp directory and can add a missing $schema key to the hank. A successful validation prints ✓ Configuration is valid!, the hank structure, the GOOD TO RUN! box, and a Run it: line. When the hank or system configures environment variables, it also prints a masked environment-variable report. If a model is known to the registry but cannot be served by Pi, preflight fails while loading the configuration; follow model-resolution for those diagnostics. Runtime startup separately performs provider health checks; those checks can make small billable generateText calls when provider credentials are set, unlike --validate.

The first capture shows a successful validation of a minimal one-codon hank; note the GOOD TO RUN! summary box and the Run it: line at the bottom, which gives the exact launch command for this fixture.

Captured from the published hankweave@0.11.0 fixture.

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

Calculating data signature for validation...

> Validating configuration: <workspace>/fixtures/minimal-single-provider/hank.json

  Data source:    <workspace>/fixtures/minimal-single-provider/data
  Execution path: ~/.hankweave-executions/validation-<id>

✓ Configuration is valid!

╭──────────────────────────────────────────────────────────────────────────────╮
│  Minimal single provider v1.0.0                                              │
│  1 codon • 0 loops                                                           │
╰──────────────────────────────────────────────────────────────────────────────╯

└─ [1] summarize-notes (Summarize the notes)
      model: haiku │ mode: fresh │ prompts: 1 (13 lines)
      checkpointedGlobs: 1

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

Run it:  hankweave hank.json <data_path>

From the published env-vars fixture.

The second capture shows the masked environment-variable report that appears when the hank or system configures variables: secret values are masked to a •••••• prefix with the last four characters and a length, while non-secret values appear in clear, so you can confirm the right variables arrived without exposing them.

FIG. 2 Masked environment-variable report
Read the diagram as text
Output
╭────────────────────────────────────────────────────────────────────╮
│  Hankweave v0.11.0                                                 │
│  linux x64 • node v22.23.2                                         │
╰────────────────────────────────────────────────────────────────────╯

Calculating data signature for validation...

> Validating configuration: <workspace>/fixtures/scenarios/env-vars/hank.json

  Data source:    <workspace>/fixtures/scenarios/env-vars/data
  Execution path: ~/.hankweave-executions/validation-<id>

✓ Configuration is valid!

╭──────────────────────────────────────────────────────────────────────────────╮
│  Environment variables v1.0.0                                                │
│  1 codon • 0 loops                                                           │
╰──────────────────────────────────────────────────────────────────────────────╯

└─ [1] write-line (Write one line)
      model: haiku │ mode: fresh │ prompts: 1 (2 lines)
      checkpointedGlobs: 1

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

Run it:  hankweave hank.json <data_path>

Environment Variables:

  From System (HANKWEAVE_ prefixed):
    - CAPTURE_VERSION: 0.11.0
    - FIXTURE_SECRET: ••••••ABCD (24 chars)
    - FIXTURE_SHORT: abc

  From Codon Configurations:
    Codon "Write one line" (write-line):
      - FIXTURE_MODE: docs
      - FIXTURE_TOKEN: ••••••cdef (20 chars)
exit=0

Check-it: The capture contains ✓ Configuration is valid!, the GOOD TO RUN! box, and the Run it: line; the validation process exits 0.

Launch the run you meant to launch#

2. Launch the intended path. Hankweave has no run subcommand; the option grammar below is the whole interface. With one positional path, a path ending in .json is the hank path; otherwise it is the data path. The Run it: line in the validation capture shows a hank path and a data path; the rule above applies when only one positional path is supplied. Add --headless, the mode without the text interface (TUI), when a CI job or script should run without it. The full flag catalog, defaults and deprecations live in the CLI reference; the excerpt below covers the flags this runbook uses.

Output
Usage: hankweave [options] [config-or-data-path]

Arguments:
  config-or-data-path       Path to hank.json or project directory
                            When only one argument provided:
                            - If ends with .json: treated as hank-path
                            - Otherwise: treated as data-path

Execution Control:
  -e, --execution <path>    Use specific execution directory
                            Creates if doesn't exist, resumes if has state
  --replay <path>           Replay an execution from recorded logs (no LLM calls)
                            Mutually exclusive with --execution
  -n, --new, --start-new    Start new execution, never resume
                            Use -n -f to overwrite existing state
  -f, --force               Override safety checks (hash mismatch, existing state)
  --no-wipe                 With --start-new --force, preserve the existing
                            agentRoot/ workspace instead of wiping it
  -y, --yes                 Skip confirmation prompts
  --max-cost <dollars>      Set a run-wide cost ceiling in USD
  --max-time <seconds>      Set a run-wide wall-clock limit in seconds

Output:
  -o, --output <path>       Copy outputs to this path (default: stay in execution dir)
  --overwrite-output        Overwrite existing output files instead of renaming
Output
Server:
  -p, --port <port>         WebSocket server port (default: auto-select free port)
  --headless                Run without TUI (for CI/CD and scripts)
  --no-autostart            Don't automatically start codons
  --proxy                   Enable the LLM proxy server (disabled by default)
  --without-proxy           Disable the LLM proxy (this is the default)
  --anthropic-base-url <url>  Custom Anthropic API base URL
  --idle-timeout <seconds>  Idle timeout for WebSocket and proxy servers (0-255, 0 disables, default: 0)
  --shim-idle-timeout <seconds>   Harness idle timeout in seconds (default: 120, per-codon)

Two help lines in that excerpt need qualification rather than trust. --idle-timeout is parsed as an integer 0–255 but its merged value is never passed to the WebSocket serve call or the proxy runners, so it changes no idle limit; --shim-idle-timeout is the wired harness setting, a positive integer up to 1800 seconds with no configured default, and its printed 120 is only the Pi SDK fallback (the Claude Agent SDK fallback is 180 seconds). The --replay line's "(no LLM calls)" describes codon replay only: replay re-emits recorded codon logs and skips the per-model self-tests, rig setup and sentinels, but the runtime still runs startup provider health checks, which can make small billable calls when provider credentials are set, and it copies the execution directory into the system temp dir and can send telemetry.

At startup, the process changes into the execution directory. By default, the data source appears inside it at agentRoot/read_only_data_source as a symlink; pass --copy to use a copy instead. There is no default output directory: outputs stay in the execution directory's agentRoot/ workspace unless you pass -o or set outputDirectory in hankweave.json (or HANKWEAVE_RUNTIME_OUTPUT_DIRECTORY). When an output directory is set, Hankweave copies a completed codon's outputFiles groups there; a beforeCopy command runs only inside that configured external-copy path, and a failed later group stops the remaining groups and fails the run without undoing earlier copies. At the destination, existing names are renamed unless you pass --overwrite-output; the rename form is _N_timestamp.

Startup prints a New execution or Resuming block with the source, execution directory, and SDKs, then the Hankweave Server Started box with its WebSocket – the control channel – URL. The capture below shows the new-execution form; the automatic fresh-selection and --start-new messages are distinguished in the execution-directory section below. Pin the WebSocket port with -p, --port. Without it, Hankweave asks the operating system for a free port, which allows concurrent instances on one machine.

Captured from the published hankweave@0.11.0 fixture; execution IDs and ports are run-specific.

Output
Created new execution directory: ~/.hankweave-executions/<exec-id>
New execution: <exec-id>
  Source → data
  Exec   → ~/.hankweave-executions/<exec-id>
  SDKs   → Claude node_modules ✓
…
  Hankweave Server Started
  WebSocket: ws://localhost:<port>
…
Running in headless mode on port <port>

Check-it: The capture names the new execution and prints Running in headless mode on port <port> after the Hankweave Server Started box.

Don't mistake a no-op rerun for a fresh run#

3. Choose the execution directory deliberately. This is the step where reruns go wrong, so check which execution you'll be using before launching again. Without -e, Hankweave hashes the data source, then resumes the newest matching execution directory under ~/.hankweave-executions/ whose execution-meta.json carries the same data hash. If no execution matches, Hankweave creates a fresh <timestamp>-<random>-<dataHash6> directory under that managed root.

The reuse is announced, not silent: the console prints Resuming execution in: <dir>, and when the resumed execution has already completed, Hankweave prints Resumed completed execution <id> — nothing to do. All codons already completed. Use --start-new to run fresh., runs no codon, and exits 0. The hazard is that exit 0 does not mean new work ran. Pass --start-new (-n) to force a fresh run.

Use -e, --execution <path> to pin the directory. Hankweave creates it when missing and resumes it when it contains .hankweave/ state. The safety checks are scoped: an -e path under the managed root is refused only when it would create a new execution there (an existing managed execution can be resumed). The back-up-or-refuse and warn-and-prompt checks run only with --start-new: a directory with .hankweave/ throws Directory already contains execution state unless --start-new --force, which backs the state up to .hankweave.backup-<timestamp> and wipes agentRoot/ unless --no-wipe; another non-empty directory warns and prompts Continue? (skipped by -y). A plain -e on a non-empty directory without .hankweave/ adopts it silently. Without -e, --start-new allocates a fresh managed directory, and --force never selects the prior workspace.

The changed-hank check applies only when -e names a directory holding .hankweave/execution-meta.json and --start-new is absent: a non-interactive run refuses with a hash-mismatch error and exits 1 unless -y or --force is given, while an interactive run warns and asks Continue with modified config?. A managed resume (no -e) never reads the hank hash, so an edited hank needs --start-new. A changed data source is refused only on the same explicit -e path, with remediation choices (-e <dir> --force, -e <dir> --start-new --force, or omitting -e); a managed rerun with changed data finds no match and starts a new execution from the first codon.

The three captures below are the evidence for the reuse. The console shows a resume, the nothing-to-do notice, and a clean exit; the server log shows the run skipping straight to shutdown; the event count proves no codon ran.

Captured from the published silent-reuse fixture; execution IDs and ports are run-specific.

Output
Resuming execution in: ~/.hankweave-executions/<exec-id>
Resuming: <exec-id>
…
➜ Listening on: http://localhost:<port>/ (all interfaces)
exit=0

Captured from the reused execution's server.log.

Output
[<ts>] [INFO] Resuming from last completed codon: write-line in run <id>
…
[<ts>] [INFO] Shutdown: all codons completed (exit code: 0)

Captured from the published silent-reuse fixture; event counts are normalized.

Output
codon.* events in <exec-id>/.hankweave/events/events.jsonl: after first run 2, after reuse 2 (delta 0); execution resumed by the second run: the same directory

Check-it: The captures show Resuming execution in: <dir>, the Resumed completed execution <id> — nothing to do notice, and exit=0 on the console, all codons completed in server.log, and zero new codon.* events in the same directory; managed fresh selection prints Created execution directory: <dir>, while --start-new prints Created new execution directory:.

Watch a run that says nothing#

4. Observe the files, not the quiet console. In headless mode, the console is quiet while codons run. Watch the event journal at .hankweave/events/events.jsonl, the human-readable server log at .hankweave/logs/server.log, and .hankweave/state.json. Also inspect files codons write under agentRoot/. These paths are inside the execution directory; for a resumed run, use the directory named by Resuming execution in: as their root, and for a new run start from the execution path printed in the startup block.

Completion has a specific shape: one codon.completed journal event per terminal codon attempt (one per codon when nothing was retried or resumed), followed in server.log by Shutting down server: all codons completed, State transition: RunCompleted, and Shutdown: all codons completed (exit code: 0). The journal excerpt below shows that full sequence for the minimal fixture: run start, the codon's state transitions, cost and message increments, the checkpoint, codon.completed, and finally RunCompleted.

JSONL
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "RunStarted", "runId": "<id>", "transition": {"type": "RunStarted", "data": {"runId": "<id>", "runFolder": "~/.hankweave-executions/<exec-id>/.hankweave/runs/<id>", "gitBranch": "run-<id>", "startingConditions": {"type": "fresh"}, "serverPid": 2862}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "CodonStarted", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "CodonStarted", "data": {"runId": "<id>", "codonId": "summarize-notes"}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "CodonTransitioned", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "CodonTransitioned", "data": {"runId": "<id>", "codonId": "summarize-notes", "from": "preparing", "to": "starting", "metadata": {}}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "CodonTransitioned", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "CodonTransitioned", "data": {"runId": "<id>", "codonId": "summarize-notes", "from": "starting", "to": "initializing", "metadata": {"claudePid": 957441, "claudeLogPath": ".hankweave/runs/<id>/summarize-notes-claude.log"}}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "CodonTransitioned", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "CodonTransitioned", "data": {"runId": "<id>", "codonId": "summarize-notes", "from": "initializing", "to": "running", "metadata": {"claudeSessionId": "cc67c965-b0af-4cef-8242-550f56ef618e"}}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "CostsIncremented", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "CostsIncremented", "data": {"runId": "<id>", "codonId": "summarize-notes", "costDelta":"<n>", "tokensDelta": {"inputTokens":"<n>", "outputTokens":"<n>", "cacheCreationTokens":"<n>", "cacheReadTokens":"<n>"}}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "AssistantMessageCountUpdated", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "AssistantMessageCountUpdated", "data": {"runId": "<id>", "codonId": "summarize-notes", "newCount":"<n>"}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "CostsIncremented", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "CostsIncremented", "data": {"runId": "<id>", "codonId": "summarize-notes", "costDelta":"<n>", "tokensDelta": {"inputTokens":"<n>", "outputTokens":"<n>", "cacheCreationTokens":"<n>", "cacheReadTokens":"<n>"}}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "AssistantMessageCountUpdated", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "AssistantMessageCountUpdated", "data": {"runId": "<id>", "codonId": "summarize-notes", "newCount":"<n>"}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "CostsIncremented", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "CostsIncremented", "data": {"runId": "<id>", "codonId": "summarize-notes", "costDelta":"<n>", "tokensDelta": {"inputTokens":"<n>", "outputTokens":"<n>", "cacheCreationTokens":"<n>", "cacheReadTokens":"<n>"}}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "AssistantMessageCountUpdated", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "AssistantMessageCountUpdated", "data": {"runId": "<id>", "codonId": "summarize-notes", "newCount":"<n>"}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "CostsIncremented", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "CostsIncremented", "data": {"runId": "<id>", "codonId": "summarize-notes", "costDelta":"<n>", "tokensDelta": {"inputTokens":"<n>", "outputTokens":"<n>", "cacheCreationTokens":"<n>", "cacheReadTokens":"<n>"}}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "AssistantMessageCountUpdated", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "AssistantMessageCountUpdated", "data": {"runId": "<id>", "codonId": "summarize-notes", "newCount":"<n>"}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "CostsIncremented", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "CostsIncremented", "data": {"runId": "<id>", "codonId": "summarize-notes", "costDelta":"<n>", "tokensDelta": {"inputTokens":"<n>", "outputTokens":"<n>", "cacheCreationTokens":"<n>", "cacheReadTokens":"<n>"}}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "AssistantMessageCountUpdated", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "AssistantMessageCountUpdated", "data": {"runId": "<id>", "codonId": "summarize-notes", "newCount":"<n>"}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "CostsIncremented", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "CostsIncremented", "data": {"runId": "<id>", "codonId": "summarize-notes", "costDelta":"<n>", "tokensDelta": {"inputTokens":"<n>", "outputTokens":"<n>", "cacheCreationTokens":"<n>", "cacheReadTokens":"<n>"}}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "AssistantMessageCountUpdated", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "AssistantMessageCountUpdated", "data": {"runId": "<id>", "codonId": "summarize-notes", "newCount":"<n>"}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "CodonFinalCostSet", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "CodonFinalCostSet", "data": {"runId": "<id>", "codonId": "summarize-notes", "finalCost":"<n>", "finalTokens": {"inputTokens":"<n>", "outputTokens":"<n>", "cacheCreationTokens":"<n>", "cacheReadTokens":"<n>"}}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "CheckpointCreated", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "CheckpointCreated", "data": {"runId": "<id>", "codonId": "summarize-notes", "checkpointType": "completed", "sha": "<sha>", "branch": "run-<id>"}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "CodonTransitioned", "runId": "<id>", "codonId": "summarize-notes", "transition": {"type": "CodonTransitioned", "data": {"runId": "<id>", "codonId": "summarize-notes", "from": "running", "to": "completed", "metadata": {"exitCode": 0, "resultMessageReceived": true, "checkpointSha": "<sha>", "contextExceeded": false, "extensionCount": 0}}}, "resultingState": {"currentRunId": "<id>", "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "codon.completed", "data": {"codonId": "summarize-notes", "success": true, "cost":"<n>", "duration":"<n>", "exitStatus": {"type": "success"}}}
{"id": "<id>", "timestamp": "<ts>", "type": "state.transition", "data": {"transitionType": "RunCompleted", "runId": "<id>", "transition": {"type": "RunCompleted", "data": {"runId": "<id>"}}, "resultingState": {"currentRunId": null, "runCount": 1, "totalCost":"<n>", "currentRunCost":"<n>"}}}

Use --attach to connect a TUI to an already-running server. It is a boolean flag: the target comes from --port, the execution's runtime.lock when --execution is supplied, or the default port 7777; it does not take a URL argument. The attached UI displays READ-ONLY: Commands are disabled and guards its mutating n, s, f, and r keys, while q disconnects. Those hotkey guards are client-side, and the attached TUI connects read-write; a client that instead completes the handshake with mode: "readonly" gets a server-enforced INSUFFICIENT_PERMISSIONS error for any command outside the read-only allowlist (checkpoint.list, server.shutdown, server.force_shutdown, ping, history.sync). Even that allowlist includes shutdown commands, so the control channel is not a security boundary: restrict access to the WebSocket and use authenticated controls for remote exposure. See resume, rollback, and retry for the WebSocket envelopes. PostHog ENOTFOUND hw-telemetry.southbridge.ai stack traces on headless error output are known telemetry noise, not a run failure.

Check-it: Find a codon.completed event, then match the three shutdown lines in server.log and the final RunCompleted transition.

Stop a run without losing state#

5. Stop gracefully first. Send SIGINT with Ctrl+C or send SIGTERM to begin graceful shutdown. A second signal during shutdown escalates to force shutdown, which aborts the running harness session rather than sending SIGKILL: for a Claude Agent SDK codon it aborts the query through its AbortController (the SDK ends its agent subprocess with SIGTERM), and for a Pi codon it aborts the in-process session. Force shutdown skips checkpoints, telemetry, and state transitions, and exits 1. Graceful shutdown's exit code follows its reason: 0 for SIGINT, SIGTERM, client request, and for all codons completed when the run did not fail; any other reason – including a server.shutdown carrying its own data.reason, or the TUI quit user request – exits 1 and marks the run RunFailed. The full contract is on errors and exit codes. A 30-second shutdown watchdog starts before awaited cleanup and force-exits with the computed exit code if graceful shutdown wedges, so a stuck shutdown cannot hang CI.

If you control the server through WebSocket, server.force_shutdown is the deliberate-shutdown command. See resume, rollback, and retry before choosing a recovery action.

Checkpoints are unconditional: the CLI asserts git is available before touching the execution directory, and a running server always has a checkpoint repository. A graceful shutdown with a codon still in progress and a reason other than all codons completed writes a checkpoint commit whose first line starts exit:<codonId> [run:…] <codonName>, while the corresponding CheckpointCreated transition records checkpointType: "skipped"; a force shutdown, a second signal, a crash or a kill writes none. Those checkpoint labels are separate from the process exit code.

The captures show the two signal paths. The single-SIGINT capture shows the graceful path: server.log records Shutting down server: SIGINT and Killing current codon runner for shutdown, the process exits 0, and the interrupted run's state.json records the run as failed with the codon in initializing. The double-SIGINT capture shows the force path: Force shutting down server: second SIGINT and Force shutdown complete, with exit 1.

Captured from the published signal fixture's graceful-interruption server.log excerpt.

Output
[<ts>] [INFO] Shutting down server: SIGINT
[<ts>] [INFO] Killing current codon runner for shutdown

Captured from the published signal fixture's double-signal attempt, which escalated to force shutdown and exited 1.

Output
[<ts>] [INFO] Shutting down server: SIGINT
[<ts>] [INFO] Killing current codon runner for shutdown
[<ts>] [INFO] Force shutting down server: second SIGINT
[<ts>] [INFO] Force shutdown complete

Captured from the published signal fixture's final state after the graceful interruption.

JSON
{
  "status": "failed",
  "codons": [
    {
      "codonId": "write-line",
      "status": "initializing"
    }
  ]
}

Check-it: After a first Ctrl+C, match the Shutting down server: SIGINT / Killing current codon runner for shutdown lines in server.log, confirm exit 0, and confirm .hankweave/state.json records the run failed with the codon initializing as in the signal capture above. After a second Ctrl+C during shutdown, the capture shows Force shutting down server: second SIGINT and Force shutdown complete with exit 1. Distinguish the exit: commit prefix and its stored checkpointType: "skipped" from the process exit status.

Wire it into CI#

6. Gate the run on validation, then use headless mode. In CI, stop before launch if validation fails:

⌁ Terminal
bunx hankweave@0.11.0 --validate || exit 1
bunx hankweave@0.11.0 --headless

For a normal CI run, the contract is exit 0 for success and exit 1 for failure. The complete exit-code catalog belongs to errors and exit codes. all codons completed returns 0 unless the run is failed or crashed; codon failure returns 1. A failed outputFiles copy or beforeCopy step also fails the run and returns 1. The graceful signal path in Stop a run without losing state is a separate exit-0 case: the process can return 0 while the interrupted run record shows the run failed. Remember that an exit code is not a full verdict: it reports the runtime's own outcome, so a CI gate should also check the produced artifacts rather than treating exit 0 as proof the output is correct.

In headless mode, a retriable codon failure under onFailure: "abort" fails fast because there is no interactive client to park for a retry. Headless mode autostarts by default; there is no --autostart flag. Use --no-autostart when the server should wait for WebSocket commands. If headless autostart fails, Hankweave prints [FATAL] Headless autostart failed and returns 1.

--max-cost and --max-time set run-level caps; both are parsed, validated as positive numbers, and appear in the shipped --help. A codon's resolved limit is composed from the remaining global budget and its allocation mode with any codon cap as a hard ceiling: cost is min(allocation, codon maxDollars) for a capped codon that has a share, while an unshared capped codon gets min(codon maxDollars, remaining global budget); time is min(codon maxTimeSeconds, hank remaining). The declared ceiling is logged at startup, and a graceful shutdown of a run with a budget configured emits a budget.summary reporting each codon's resolved limit against its actual dollars, time and tokens before that shutdown's RunCompleted/RunFailed; a run ended by a codon or output-copy failure emits no budget.summary.

When a budget cap trips, the runtime logs Budget exceeded for <codon> and requests the codon's interruption (SIGTERM); onExceeded then decides the outcome. With onExceeded: "complete" (the default), the attempt resolves as completed with a "budget limit reached" info event; with onExceeded: "fail", the codon is failed non-retriably. Either way the trip is settled before that attempt's codon.completed, which carries budgetExceeded with its currency, limit, and used values; under "fail" the event carries success: false and a Budget exceeded: … failure reason, and an error: checkpoint is committed first. The run-level failure policy runs after the event. A failed codon's spend buys no delivered output, so the working pattern is per-codon onExceeded: "complete" soft caps and hard caps at least twice the estimate on expensive codons. Read budgets for the two-party budget rules.

maxTimeSeconds is enforced actively: each costIncremented event calls checkTime(), and a 1000 ms timer checks independently; when the cap is exceeded, the runner requests SIGTERM. A SIGTERM request is not proven harness teardown, so do not promise deadline-precise stops: one older observed attempt ran for 24,923 seconds against a 2,400-second cap, an unresolved historical capture rather than the intended mechanism. For unattended work, add an external process deadline. Zai pricing is per model: five zai models are priced, so maxDollars can bind for them, while glm-5.2-highspeed and glm-5.3-highspeed report cost: 0, so no dollar cap can trip for those two; guard them with maxTimeSeconds.

Check-it: The normal CI process returns $? = 0 for success and $? = 1 for failure, including output-copy failure. The completion capture records the successful RunCompleted path.

Find what the run left behind#

When the run is over, the execution directory is the record of what happened, including work you may want to recover. See Execution directory for its full tree, including ~/.hankweave/, replay copies, and archive shape.

Use --cleanup only when you intend to remove the selected execution directory itself. Cleanup recursively deletes that directory, including other files it contains, after Proceed with cleanup? This cannot be undone! (y/N); -y skips the confirmation. With -e, the named directory is the deletion target; without it, the ordinary newest-match selection supplies the target. If the target contains .hankweave/runtime.lock, cleanup refuses with Server is running; that file-presence check does not prove a live process or silently remove a stale lock.

Continue with Observe and debug to diagnose failures by phase and find the agent log, including the claudeLogPath value and its #-to-- naming change. For recovery procedures and WebSocket envelopes, use Resume, rollback, and retry.