# Operate a Hankweave run
A Hankweave run is easy to start and easy to 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 `running`. 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.

Run lifecycle: before-run → preflight → running → completed, with fix and recovery branches
Diagram as text
```text
// 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](#watch-a-run-that-says-nothing); those labels are not additional run states. When a run lands on the fix branch, use [troubleshooting](/0.10.0/files/operate/troubleshooting). For the recovery branches, use [resume, rollback, and retry](/0.10.0/files/operate/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:
```bash
bunx hankweave@0.10.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. At 0.10.0, the validation self-test checks SDK import, executable or credential presence, and the Pi model catalog; it does not run model generation or make a billable model call. It can still probe EC2 instance metadata for Bedrock credentials when no static AWS credential source is visible, so do not treat validation as a universal offline guarantee. A key must be present, but Hankweave does not call the model during this validation.
Validation runs before execution setup and creates no execution directory. Its printed `Execution path` is a would-be path: `validation-` 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](/0.10.0/files/reference/model-resolution) for those diagnostics. Runtime startup separately performs provider health checks; those checks can call `generateText("Hi", maxOutputTokens:16)`, 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.10.0` fixture.*
```text
╭────────────────────────────────────────────────────────────────────╮
│ Hankweave v0.10.0 │
│ darwin arm64 • node v23.8.0 │
╰────────────────────────────────────────────────────────────────────╯
Calculating data signature for validation...
> Validating configuration: /fixtures/minimal-single-provider/hank.json
Data source: /fixtures/minimal-single-provider/data
Execution path: ~/.hankweave-executions/validation-
✓ 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
```
*From the env-vars fixture at 0.10.0.*
The second capture shows the masked environment-variable report that appears when the hank or system configures variables: values are redacted to their last characters with a length, so you can confirm the right variables arrived without exposing them.
```text
╭────────────────────────────────────────────────────────────────────╮
│ Hankweave v0.10.0 │
│ darwin arm64 • node v23.8.0 │
╰────────────────────────────────────────────────────────────────────╯
Calculating data signature for validation...
> Validating configuration: /fixtures/scenarios/env-vars/hank.json
Data source: /fixtures/scenarios/env-vars/data
Execution path: ~/.hankweave-executions/validation-
✓ 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
Environment Variables:
From System (HANKWEAVE_ prefixed):
- CAPTURE_VERSION: 0.10.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.
```text
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 Use specific execution directory
Creates if doesn't exist, resumes if has state
-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 Non-interactive mode, skip confirmation prompts
Output:
-o, --output Copy outputs to this path (default: stay in execution dir)
--overwrite-output Overwrite existing output files instead of renaming
```
```text
Server:
-p, --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)
--anthropic-base-url Custom Anthropic API base URL
--idle-timeout Idle timeout for WebSocket and proxy servers (0-255, default: 0)
--shim-idle-timeout Harness idle timeout in seconds (default: 120, per-codon)
```
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`. Hankweave copies those outputs after each codon's `outputFiles` step. At the `-o` 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.10.0` fixture; execution IDs and ports are run-specific.*
```text
Created new execution directory: ~/.hankweave-executions/
New execution:
Source → data
Exec → ~/.hankweave-executions/
SDKs → Claude node_modules ✓
…
Hankweave Server Started
WebSocket: ws://localhost:
…
Running in headless mode on port
```
**Check-it:** The capture names the new execution and prints `Running in headless mode on port ` after the `Hankweave Server Started` box.
## Don't mistake a silent no-op 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 `--` directory under that managed root.
A completed execution can therefore look successful while doing no new work: the console prints `Resuming execution in: `, `all codons completed` appears in `server.log` rather than on the console, and the process exits 0 with no new `codon.*` events in the same execution directory. Pass `--start-new` (`-n`) to force a fresh run.
> **Pitfall:** A rerun that exits 0 in seconds may have run nothing. Reuse is keyed on the data signature and can silently resume a completed run; pass `--start-new` when you need fresh work.
Use `-e, --execution ` to pin the directory. Hankweave creates it when missing and resumes it when it contains `.hankweave/` state. The safety checks have three tiers. A new explicit `-e` target under the managed root is refused when it lacks `execution-meta.json`; an existing explicit path containing both `/.hankweave-executions/` and `/data` is rejected by the nested-execution guard. Choose another explicit directory or omit `-e` to let Hankweave allocate one, and keep the input source separate from the execution directory. On existing state, `--start-new` requires `--force`; that backs up state to `.hankweave.backup-` and wipes `agentRoot/` unless `--no-wipe`. Another non-empty directory warns and prompts. Without `-e`, `--start-new` creates a fresh managed directory; `--force` does not select the previous workspace.
When resuming after a hank edit, Hankweave shows a hash-mismatch warning and asks for confirmation. In non-interactive mode, that prompt cancels by default, so CI needs `-y`. A changed data source is an error with remediation choices; override it with `--force` or the deprecated `--ignore-data-mismatch`.
The three captures below are the evidence for the silent no-op. The console shows a resume 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.*
```text
Resuming execution in: ~/.hankweave-executions/
Resuming:
…
➜ Listening on: http://localhost:/ (all interfaces)
exit=0
```
*Captured from the reused execution's `server.log`.*
```text
[] [INFO] Resuming from last completed codon: write-line in run
…
[] [INFO] Shutdown: all codons completed (exit code: 0)
```
*Captured from the published silent-reuse fixture; event counts are normalized.*
```text
codon.* events in /.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: ` and `exit=0` on the console, `all codons completed` in `server.log`, and zero new `codon.*` events in the same directory; fresh selection prints `Created execution directory: `, 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 codon, 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": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "RunStarted", "runId": "", "transition": {"type": "RunStarted", "data": {"runId": "", "runFolder": "~/.hankweave-executions//.hankweave/runs/", "gitBranch": "run-", "startingConditions": {"type": "fresh"}, "serverPid": 50024}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "CodonStarted", "runId": "", "codonId": "summarize-notes", "transition": {"type": "CodonStarted", "data": {"runId": "", "codonId": "summarize-notes"}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "CodonTransitioned", "runId": "", "codonId": "summarize-notes", "transition": {"type": "CodonTransitioned", "data": {"runId": "", "codonId": "summarize-notes", "from": "preparing", "to": "starting", "metadata": {}}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "CodonTransitioned", "runId": "", "codonId": "summarize-notes", "transition": {"type": "CodonTransitioned", "data": {"runId": "", "codonId": "summarize-notes", "from": "starting", "to": "initializing", "metadata": {"claudePid": 962109, "claudeLogPath": ".hankweave/runs//summarize-notes-claude.log"}}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "CodonTransitioned", "runId": "", "codonId": "summarize-notes", "transition": {"type": "CodonTransitioned", "data": {"runId": "", "codonId": "summarize-notes", "from": "initializing", "to": "running", "metadata": {"claudeSessionId": "be7a1143-f289-4e61-9346-9c3bf0dc8ed2"}}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "CostsIncremented", "runId": "", "codonId": "summarize-notes", "transition": {"type": "CostsIncremented", "data": {"runId": "", "codonId": "summarize-notes", "costDelta":"", "tokensDelta": {"inputTokens":"", "outputTokens":"", "cacheCreationTokens":"", "cacheReadTokens":""}}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "AssistantMessageCountUpdated", "runId": "", "codonId": "summarize-notes", "transition": {"type": "AssistantMessageCountUpdated", "data": {"runId": "", "codonId": "summarize-notes", "newCount":""}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "CostsIncremented", "runId": "", "codonId": "summarize-notes", "transition": {"type": "CostsIncremented", "data": {"runId": "", "codonId": "summarize-notes", "costDelta":"", "tokensDelta": {"inputTokens":"", "outputTokens":"", "cacheCreationTokens":"", "cacheReadTokens":""}}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "AssistantMessageCountUpdated", "runId": "", "codonId": "summarize-notes", "transition": {"type": "AssistantMessageCountUpdated", "data": {"runId": "", "codonId": "summarize-notes", "newCount":""}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "CostsIncremented", "runId": "", "codonId": "summarize-notes", "transition": {"type": "CostsIncremented", "data": {"runId": "", "codonId": "summarize-notes", "costDelta":"", "tokensDelta": {"inputTokens":"", "outputTokens":"", "cacheCreationTokens":"", "cacheReadTokens":""}}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "AssistantMessageCountUpdated", "runId": "", "codonId": "summarize-notes", "transition": {"type": "AssistantMessageCountUpdated", "data": {"runId": "", "codonId": "summarize-notes", "newCount":""}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "CostsIncremented", "runId": "", "codonId": "summarize-notes", "transition": {"type": "CostsIncremented", "data": {"runId": "", "codonId": "summarize-notes", "costDelta":"", "tokensDelta": {"inputTokens":"", "outputTokens":"", "cacheCreationTokens":"", "cacheReadTokens":""}}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "AssistantMessageCountUpdated", "runId": "", "codonId": "summarize-notes", "transition": {"type": "AssistantMessageCountUpdated", "data": {"runId": "", "codonId": "summarize-notes", "newCount":""}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "CostsIncremented", "runId": "", "codonId": "summarize-notes", "transition": {"type": "CostsIncremented", "data": {"runId": "", "codonId": "summarize-notes", "costDelta":"", "tokensDelta": {"inputTokens":"", "outputTokens":"", "cacheCreationTokens":"", "cacheReadTokens":""}}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "AssistantMessageCountUpdated", "runId": "", "codonId": "summarize-notes", "transition": {"type": "AssistantMessageCountUpdated", "data": {"runId": "", "codonId": "summarize-notes", "newCount":""}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "CostsIncremented", "runId": "", "codonId": "summarize-notes", "transition": {"type": "CostsIncremented", "data": {"runId": "", "codonId": "summarize-notes", "costDelta":"", "tokensDelta": {"inputTokens":"", "outputTokens":"", "cacheCreationTokens":"", "cacheReadTokens":""}}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "AssistantMessageCountUpdated", "runId": "", "codonId": "summarize-notes", "transition": {"type": "AssistantMessageCountUpdated", "data": {"runId": "", "codonId": "summarize-notes", "newCount":""}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "CodonFinalCostSet", "runId": "", "codonId": "summarize-notes", "transition": {"type": "CodonFinalCostSet", "data": {"runId": "", "codonId": "summarize-notes", "finalCost":"", "finalTokens": {"inputTokens":"", "outputTokens":"", "cacheCreationTokens":"", "cacheReadTokens":""}}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "CheckpointCreated", "runId": "", "codonId": "summarize-notes", "transition": {"type": "CheckpointCreated", "data": {"runId": "", "codonId": "summarize-notes", "checkpointType": "completed", "sha": "", "branch": "run-"}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "CodonTransitioned", "runId": "", "codonId": "summarize-notes", "transition": {"type": "CodonTransitioned", "data": {"runId": "", "codonId": "summarize-notes", "from": "running", "to": "completed", "metadata": {"exitCode": 0, "resultMessageReceived": true, "checkpointSha": "", "contextExceeded": false, "extensionCount": 0}}}, "resultingState": {"currentRunId": "", "runCount": 1, "totalCost":"", "currentRunCost":""}}}
{"id": "", "timestamp": "", "type": "codon.completed", "data": {"codonId": "summarize-notes", "success": true, "cost":"", "duration":"", "exitStatus": {"type": "success"}}}
{"id": "", "timestamp": "", "type": "state.transition", "data": {"transitionType": "RunCompleted", "runId": "", "transition": {"type": "RunCompleted", "data": {"runId": ""}}, "resultingState": {"currentRunId": null, "runCount": 1, "totalCost":"", "currentRunCost":""}}}
```
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 are client-side guards, not server-enforced authority: the handshake requests `READANDWRITE`, the server accepts the requested mode without authentication, and even its read-only command set includes `server.shutdown` and `server.force_shutdown`. 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](/0.10.0/files/operate/resume-rollback-and-retry) for the WebSocket envelopes. PostHog `ENOTFOUND hw-telemetry.southbridge.ai` stack traces on headless error output are a known 0.10.0 telemetry defect, 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. Force shutdown sends `SIGKILL` to harness processes, the processes supporting codon execution. It performs minimal cleanup: checkpoints, telemetry, and state transitions are skipped, and it exits 1. A graceful signal-driven shutdown returns exit code 0 for the `SIGINT`, `SIGTERM`, and `client request` reasons. Since 0.7.3, a 30-second shutdown watchdog starts before awaited cleanup and force-exits with the computed exit code if graceful shutdown wedges.
If you control the server through WebSocket, `server.force_shutdown` is the deliberate-shutdown command. See [resume, rollback, and retry](/0.10.0/files/operate/resume-rollback-and-retry) before choosing a recovery action.
On SIGINT, the console prints `Shutting down server in 3s... (press Ctrl+C again to force close) Server closed.` and exits 0. `server.log` records `Shutting down server: SIGINT` and `Killing current codon runner for shutdown`; the interrupted run's `state.json` still shows the run and codon as `running`, with no terminal transition persisted. When checkpointing is enabled, a graceful interruption of a running codon still writes a checkpoint commit whose first line starts `exit: [run:…]`, while the corresponding `CheckpointCreated` state/event uses `checkpointType: "skipped"`. Those checkpoint labels are separate from the process exit code: force shutdown skips checkpoints and exits 1, while graceful shutdown exits 0. A second signal during shutdown follows the source-level force-shutdown path and exits 1, but the captured double-SIGINT run delivered its second signal after graceful shutdown completed: it also exited 0 and contains no force-shutdown lines. Treat that capture as a race, not proof that force shutdown completed.
The captures show all three artifacts of a graceful interruption: the server-log lines, the double-signal attempt that arrived too late, and the persisted state still reading `running`.
*Captured from the published signal fixture's graceful-interruption `server.log` excerpt.*
```text
[] [INFO] Shutting down server: SIGINT
[] [INFO] Killing current codon runner for shutdown
```
*Captured from the published signal fixture's double-signal attempt; graceful shutdown completed before the second signal was observed.*
```text
[] [INFO] Shutting down server: SIGINT
[] [DEBUG] [SentinelManager] Shutdown complete
[] [INFO] Sentinel manager shutdown complete
```
*Captured from the published signal fixture's final state after the graceful interruption.*
```json
{
"status": "running",
"codons": [
{
"codonId": "write-line",
"status": "running"
}
]
}
```
**Check-it:** After Ctrl+C, match the console shutdown message and the `Shutting down server: SIGINT` / `Killing current codon runner for shutdown` lines in `server.log`, then confirm `.hankweave/state.json` still shows the run and codon as `running` as in the signal capture above. If checkpointing is enabled, distinguish an `exit:` commit prefix and `checkpointType: "skipped"` from the process's exit status. Do not infer a force exit from the double-signal capture; the runtime's force-shutdown path exits 1.
## Wire it into CI
**6. Gate the run on validation, then use headless mode.** In CI, stop before launch if validation fails:
```bash
bunx hankweave@0.10.0 --validate || exit 1
bunx hankweave@0.10.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](/0.10.0/files/reference/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](#stop-a-run-without-losing-state) is a separate exit-0 case: the process can return 0 while the interrupted run record still shows the run and codon as `running`.
> **VersionNote:** Since 0.9.0, a failed `outputFiles` copy or `beforeCopy` fails the run and returns 1, so CI can use that result as its gate.
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.
When a budget cap trips, the runner is interrupted and requests SIGTERM. With `onExceeded: "complete"` (the default), the attempt resolves by completing the codon and recording a `budget limit reached` information event; with `onExceeded: "fail"`, it fails the codon non-retriably. A `codon.completed` event may carry `budgetExceeded` with its `currency`, `limit`, and `used` values. Read [budgets](/0.10.0/files/concepts/budgets) for the two-party budget rules.
`--max-cost` and `--max-time` are parsed and validated as run-level caps, but neither appears in the shipped `--help`; see [CLI reference](/0.10.0/files/reference/cli) for the parity note. `maxTimeSeconds` has an active watchdog: each `costIncremented` event calls `checkTime()`, and a timer checks independently every 1000 ms; when the cap is exceeded, the runner requests SIGTERM. A timer tick and a kill request do not prove prompt harness teardown. One older observed attempt ran for 24,923 seconds against a 2,400-second cap; treat that as a historical unresolved capture, not the intended 0.10.0 mechanism. For unattended work, add an external process deadline. A mid-codon `onExceeded: "fail"` cost abort can forfeit the whole spend and can arrive after `codon.completed`; do not treat that event alone as proof that the final spend is settled. Use per-codon `onExceeded: "complete"` soft caps and hard caps at least twice the estimate for expensive codons. At 0.10.0, codons routed to `pi/zai/*` report no cost, so `maxDollars` does not bind for them; guard those codons 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](/0.10.0/files/reference/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](/0.10.0/files/operate/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](/0.10.0/files/operate/resume-rollback-and-retry).