# Deploying Hankweave as a process Most integrations with an LLM tool start from a library import or a long-running service. Hankweave offers neither. You spawn it, it runs a batch of work, it writes its state to disk, and it exits. That shape is deliberate, and it changes what "deploying" means: there is no daemon to supervise, no registry to join, and no client SDK to link. The integration surface is the process boundary plus the filesystem. This page walks through that model in the order an integrator meets it: how a run starts and ends, what it leaves on disk, how to find the server it spawns, how to talk to it, how to tell how the run ended, and why shelling out beats embedding. Each section builds on the previous one, so by the end you can assemble the full spawn-discover-drive-collect loop yourself. ## Why a process, not a service? When we integrate Hankweave, we coordinate a process rather than deploy a daemon or embed a library. At 0.10.0, Hankweave is a batch process whose integration surface is the process boundary plus the filesystem. Anything that can spawn a process and read files – a scheduler, a CI job, a Kubernetes Job, a script – can use that boundary. The `hankweave` bin has no subcommands: running is the default operation. Give it one positional path: a value ending in `.json` is the hank path, and any other value is the data path. Or give it two: `hank.json data/` supplies the hank path and then the data path. A **hank** is the file that defines the work; the runtime **loads** it from that explicit path. Do not let the optional positional argument become an automation trap. A bare `hankweave` invocation opens the welcome wizard and exits; it never runs your hank. Pass explicit hank and data arguments instead. A **codon** is one sealed unit of work in a hank. Each codon **runs** as part of the batch, and the run self-terminates when all codons complete. The runtime shuts the server down with reason `all codons completed`; there is no daemon to stop and no persistent control plane. The process exit code is the coarse verdict: `0` means completed or graceful, and `1` means failure. The full classification belongs to [errors and exit codes](/0.10.0/files/reference/errors-and-exit-codes). We coordinate through processes and files instead of service discovery. For example, in the runner that builds these docs, we **validate** each hank and drive concurrent Hankweave instances as shell-out jobs rather than importing a runtime library. The rest of this page unpacks each step of that pattern. ## What a run puts on disk Because the filesystem is half of the integration surface, the first thing to understand is where a run leaves its evidence. When a new execution is needed and you have not passed `-e` or `--execution`, Hankweave creates an execution directory under `~/.hankweave-executions/`. Since 0.8.0, `HANKWEAVE_RUNTIME_EXECUTION_BASE_DIR` can move that managed root. The `executionBaseDir` field in `hankweave.json` is informational only; nothing consumes it. See [the execution-directory reference](/0.10.0/files/reference/execution-directory) for the full directory tree. The optional `hankweave.json` is the runtime settings file. It can contain settings such as `port` and `autostart`, among others. The integrator-relevant paths live inside the execution directory: * `.hankweave/runtime.lock` – liveness and port. * `.hankweave/state.json` – run state. * `.hankweave/events/events.jsonl` – the event journal. * `.hankweave/logs/server.log` – the server log. * `agentRoot/` – the agent workspace. Outputs stay here unless `-o` is given; there is no default output directory. The copy stage is separate from the workspace. The hank's `outputFiles[].copy` pattern selects files from `agentRoot/` for the CLI `-o` output directory, resolved as an absolute path from the current working directory. `beforeCopy` commands run in the agent workspace – the `agentRoot/` directory the copy globs read from – before each copy group. This stage runs only when an output directory is configured; without `-o`, outputs remain in `agentRoot/`. Run from the directory that holds `hank.json` and `data`. Here, `hank.json` is the hank path; the optional `hankweave.json` is a separate runtime settings file. The startup block prints the source-to-data mapping and the execution path, so automation can capture the directory Hankweave created. The capture below shows what that block looks like for a real run. The absolute path in it belongs to that capture; use the `Exec` line from your own startup output: *Captured with the minimal single-provider fixture at 0.10.0.* ```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 ➜ Listening on: http://localhost:/ (all interfaces) ``` Two lines matter for automation: `New execution: ` and the `Exec →` mapping, which together tell you where this run's files will appear. The operator-facing run sequence is in [the runbook](/0.10.0/files/operate/runbook). ## Finding the server you spawned A batch process still needs a control channel while it runs, and Hankweave's is a WebSocket server inside the process. A service registry is not part of this model, so the question becomes: which port did it bind? When no port is configured, the WebSocket (a two-way connection) server defaults to port `0`; the operating system assigns a free port when the server binds. That avoids collisions when several instances share a host. Help describes this as selecting a free port automatically. The JSON setting has a stricter shape: `"port": 0` is not valid in `hankweave.json`, because runtime configuration requires a positive integer. Leave `port` unset there to keep automatic selection, or pin a port with `-p` or `--port `, or with `HANKWEAVE_RUNTIME_PORT`. Otherwise, discover the assigned port through the lock file. The runtime binds first. Bun assigns the free port, the runtime adopts `server.port` as the actual port, and then the lock file is rewritten with that port. When `--proxy` is enabled, the rewrite also carries `proxyPort`. With dynamic ports, we reread the lock until its recorded port is no longer `0`, then connect using that port. The lock is JSON with `pid`, `runId`, `startTime`, `lastHeartbeat`, and an optional `port`. The port may be `0` initially when dynamic allocation is in use. The runtime refreshes `lastHeartbeat` every 30 seconds. The interface definition and the diagram below show the lock's shape and how the spawn-bind-discover cycle fits together: ```text interface LockFile { pid: number; runId: string; startTime: string; lastHeartbeat: string; port?: number; // Optional for backward compatibility with old lock files ``` ![integrator view: your process → hankweave headless → lock file/port → WS](/content-assets/cf45dff5691c48c0/diagrams/integrate-deployment-model/1.svg) integrator view: your process → hankweave headless → lock file/port → WS
Diagram as text ```text integrator view: your process → hankweave headless → lock file/port → WS +-----------------------------------+ | your process | | CI job · forge.py · your service | +----------------+------------------+ | | spawn: hankweave hank.json data/ --headless | exit code returns: 0 done · 1 failed v +-----------------------------------+ | hankweave server process | | one per execution dir | | self-terminates when codons end | +----------+------------------------+ | writes /.hankweave/runtime.lock | {pid, runId, port, lastHeartbeat↻30s} | + events.jsonl · state.json · agentRoot/ v +-----------------------------------+ | lock file and execution files | +-----------------------------------+ your process → reread port until it is nonzero your process → ws://localhost: WebSocket only; plain HTTP gets 400 ```
*Port 0 default: the operating system picks the port; the lock file is how you learn it.* To connect, read `/.hankweave/runtime.lock`, take its `port`, and dial the WebSocket. `--attach --execution ` uses this discovery order: an explicit `--port`, then the lock file, then fallback port `7777`. If the lock file cannot be read, attach exits `1` and tells you to use `--port` directly. The complete flag contract is in [the CLI reference](/0.10.0/files/reference/cli). The lock file also answers a second question: is the run still alive? Evaluate two signals: whether the recorded process ID is alive and whether the heartbeat is older than 120 seconds. On a later start, the runtime uses those signals to remove the lock and mark the run crashed when the process is dead or the heartbeat is stale. The container health-check recipe belongs to [deployment operations](/0.10.0/files/operate/deployment). ## Driving the run over WebSocket-or-nothing Once you have the port, there is exactly one way to talk to the server. The server speaks WebSocket only. The `http://` address in the startup banner names where the server listens; it is not an HTTP API. A plain HTTP request from a browser, `curl`, or a health probe that does not upgrade receives status `400` with a JSON body saying `This server only accepts WebSocket connections`. The response includes CORS `*` and points to the WebSocket form; it does not disrupt the execution. `--headless` (unattended mode) runs without the TUI (terminal interface) and autostarts by default. To start the server and leave the run waiting for WebSocket commands, use `--headless --no-autostart`; it prints `Autostart disabled, waiting for WebSocket commands...`. That parked mode gives an integration control over when work begins. Handshakes, access modes, and command envelopes belong to [the protocol reference](/0.10.0/files/integrate/protocol). The first-client sequence belongs to [the WebSocket quickstart](/0.10.0/files/integrate/websocket-quickstart); this page does not duplicate either contract. The protocol reference fixes the endpoint path and the quickstart connects to it; this page deliberately does not choose between path forms. The bind scope deserves attention before you put this on a shared network. The startup banner says that the server listens on all interfaces. The control channel is therefore network-reachable unless the integrator constrains it. The channel is unauthenticated: the runtime checks no credential when a client connects, and the handshake does not restrict the requested mode. Treat that bind scope as part of the deployment boundary. If you publish the port, keep it on a trusted local or private network; remote access needs an external authenticated access control. Do not treat a read-only mode as a security boundary either: the server's `READ_ONLY_COMMANDS` set includes `server.shutdown` and `server.force_shutdown`, so a read-only client can still shut the server down. ## Knowing how it ended A self-terminating process reports its outcome through channels you already have: the exit code for the coarse verdict, and the filesystem or WebSocket for detail. Exit `0` means the run completed or shut down gracefully after `SIGINT` or `SIGTERM`. Exit `1` covers codon failure, a crash, startup failure, or force shutdown. A graceful shutdown has a 30-second watchdog; if a shutdown step wedges, the watchdog force-exits with the code the shutdown would have produced – `0` for a graceful `SIGINT`/`SIGTERM` shutdown, `1` when the run was already failing or the shutdown was forced. Since 0.9.0, an `outputFiles` copy or `beforeCopy` failure also produces exit `1`. See [errors and exit codes](/0.10.0/files/reference/errors-and-exit-codes) for the complete contract. > **VersionNote:** Since 0.7.3, a retriable codon failure in headless mode under `onFailure: "abort"` fails fast with `RunFailed` and exit `1`; no client is present to wait for a retry prompt. Some failures happen before any codon runs, and those are visible at startup. The capture below shows the self-test diagnostic and the final startup error when no API key is set; the run exits `1`: *Captured with the minimal single-provider fixture at 0.10.0.* ```text [] [ERROR] Self-test completed: FAILED [] [ERROR] Self-test FAILED: Some checks failed [] [ERROR] - authentication: ✗ No authentication found (set ANTHROPIC_API_KEY) [ERROR] Server startup failed! Error message: Self-test failed for 1 model(s): - Claude Haiku 4.5 (latest) (anthropic/claude-haiku-4-5): Some checks failed • authentication: No authentication found (set ANTHROPIC_API_KEY) … ``` The `authentication` line names the failing check and the missing variable, which is usually enough to fix the environment and retry. > **Pitfall:** Rerunning unchanged data when its newest execution already completed can log `Shutting down server: all codons completed` and return `0` without doing work. When automation must rerun, pass `--start-new` (`-n`). That pitfall follows from how execution directories resume. An explicit `-e` or `--execution ` selects an execution directory. Hankweave creates it when it does not exist; without `--start-new`, an existing directory with `.hankweave/execution-meta.json` is resumed, subject to the data-hash check. A completed execution can therefore resume silently when you omit `--start-new`. If that explicit path already contains `.hankweave/`, `--start-new` alone exits `1`; use `--start-new --force` (`-n -f`) for a fresh run in the same directory. That backs up `.hankweave/` as `.hankweave.backup-` and wipes `agentRoot/` unless `--no-wipe`; alternatively, use a new `-e` directory. Without an explicit `-e`, `--start-new` creates a fresh managed execution directory. During codons, the headless console is quiet by design. Capture the execution path from startup output first, then observe completion through the exit code, `.hankweave/events/events.jsonl`, `.hankweave/state.json`, files under `agentRoot/`, or a WebSocket client. The watch cadence and diagnostic procedure belong to [runbook observation](/0.10.0/files/operate/runbook) and [observe and debug](/0.10.0/files/operate/observe-and-debug). ## Shell out, don't embed The objection to a process boundary is practical: a library call looks like it would offer tighter control. At 0.10.0 that option is not on the table. The published package is unscoped `hankweave` and publishes exactly three subpaths: `.` (the CLI entry, with no verified library surface), `./schemas`, and `./types`. The public import forms are `hankweave/schemas` and `hankweave/types`, for parsing `state.json` and `events.jsonl` and for building clients–not for driving runs. See [client and exported types](/0.10.0/files/integrate/client-and-exported-types) for the exports contract. `HankweaveRuntime` exists as a class in the source, but it is not exported from any of those package paths. Deep `hankweave/server/*` imports are not public API at 0.10.0, so in-process embedding is unsupported. Contributor snippets that use those imports are source examples, not an integration contract. The flags below are the actual control surface an integrator gets: ```text 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 ``` ```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) ``` ```text --attach Connect TUI to an already-running server (read-only mode) ``` For one unattended run, pass explicit hank and data arguments with `--headless`; add `--start-new` when unchanged data must run again. Use `--no-autostart` only when a WebSocket client will start the run. For an integration, we bring the pieces of this page together into one loop: 1. Spawn the `hankweave` bin with explicit hank and data arguments; do not use the bare invocation. 2. Discover the assigned port by reading `runtime.lock`. 3. Drive the process over WebSocket, or park it with `--headless --no-autostart` for a client-controlled start. 4. Read the process exit code as the run's verdict. 5. Collect state, journal, logs, and workspace files from the execution directory. In the docs-building runner, we use this shell-out pattern: validate each hank, then spawn `bunx hankweave@0.10.0` shards with explicit hank and data arguments, `--start-new`, and `-o` results directories. The runner polls and collects artifacts; it does not import a runtime library. Port `0` makes that pattern safe for concurrent instances. The forge ran three and then four executions concurrently on one host and one data snapshot without a port collision.