# Runtime architecture When a run misbehaves, the question is rarely "what went wrong?" first. It is "where in the source do I look?" Hankweave's runtime spans a CLI parser, a configuration resolver, an orchestrator, two in-process model harnesses, and a set of persistence subsystems, and each of those lives in a different file. Reading the source from `index.ts` to the end is one way to find a bug; knowing the boundaries is faster. This page builds that map. It follows a single invocation from the command line to the orchestrator, through one codon's lifecycle, out to the model harnesses, and finally to the files each subsystem writes. Along the way it names the components responsible for each phase, so a code reading or a breakpoint search can start at the right boundary instead of at the top of the file. The page covers the runtime's internal structure for contributors; the public contract remains the hank schema, the CLI, the WebSocket protocol, and the exit codes. ## Finding where an invocation reaches the runtime When we trace a runtime behavior to its source, we usually have a phase in mind: parsing, validation, a codon, or a persisted record. We'll follow the invocation to the code responsible for each phase, so we can choose where to begin reading or set a breakpoint without reading the source from top to bottom. Two terms carry the whole trace. A **hank** is the work definition Hankweave **loads**; a **codon** is one sealed agent task in that definition. The runtime coordinates those tasks. The pseudocode below shows the chain at a glance; the numbered steps that follow give each link its file and its behavior. ```text // Pseudocode mental model, not the implementation. CLI args → parseCliArgs → resolveSettings → setupExecutionEnvironment → validateHank → new HankweaveRuntime → server.start() ``` 1. **Parse the invocation.** The shipped `hankweave` binary enters at `dist/index.js`, which calls `main()` at the bottom of `index.ts`. `parseCliArgs()` parses the command-line arguments once into a flat `CliArgs` object. The parser contains 31 flags, while `--help` shows 26. See the [CLI reference](/0.10.0/files/reference/cli) for the five parser-real flags omitted from help. 2. **Resolve configuration.** `resolveSettings()` merges five layers: defaults, `hankweave.json`, hank overrides, environment variables, and CLI arguments. For the precedence rules at this handoff, see [hanks](/0.10.0/files/concepts/hanks). 3. **Set up the execution directory.** `setupExecutionEnvironment()` hashes the data, finds the newest matching execution directory, or creates `--` under `~/.hankweave-executions/`. The `-e` option pins a directory instead. 4. **Validate before work begins.** On the normal run path, `validateHank()` checks the hank against the published schema, runs local model self-tests for SDK import, credential presence, and the Pi catalog, and applies the strict-reference preflight added in 0.10.0. That preflight checks that file references use portable relative paths, stay inside the hank directory, and do not traverse symlinks. The separate `--validate` command calls `runValidation()`, prints the `GOOD TO RUN!` box on success, and exits 0 or 1 without starting the runtime. The Pi harness also performs a `model_catalog` check at startup; that later, harness-specific check covers every Pi-routed codon model. 5. **Create the runtime.** The entry point constructs `new HankweaveRuntime(serverConfig)` with the merged configuration, codons list, execution-directory details, and headless flag. `HankweaveRuntime` is the orchestrator: the component that coordinates codons and the subsystems that record them. 6. **Start the server.** `server.start()` initializes checkpoints, the state manager, event journal, and sentinel manager. It then creates the WebSocket server, using `--port` or an OS-assigned port when the configured port is `0`, and starts the proxy runner after the WebSocket port is known. The server accepts connections after that setup; this page records the proxy runner's ordering, not its separate proxy contract. 7. **Choose the client path.** In headless mode, `requestAutostart()` triggers immediately after server start. In TUI mode, `BasicTUI` connects as the interactive client after a 100ms delay. A bare invocation–no hank path, flags, or data–takes a different path: `runWelcomeWizard()` runs and exits; it never executes a hank. Several of these steps are steered by command-line flags. The table below keeps only the flags that change the architecture's path–which directory is used, which client connects, whether codons start automatically–rather than the full catalogue. | Flag | Architectural effect | | ------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `-e, --execution ` | Uses a specific execution directory; creates it if it does not exist and **resumes** it when it has state. | | `--headless` | Selects the headless client path; it runs without the TUI. | | `--no-autostart` | Prevents automatic codon start after server setup. | | `-p, --port ` | Selects the WebSocket server port; the default is an automatically selected free port. | | `--shim-idle-timeout ` | Sets the default harness idle timeout; the Pi default is 120 seconds and the Claude Agent SDK default is 180 seconds. | | `--attach` | Connects the TUI to an already-running server in read-only mode. | For the complete flag catalogue, including defaults, deprecations, and `=` syntax, see the [CLI reference](/0.10.0/files/reference/cli). Here we keep the flags that help us trace the architecture. Step 3 mentioned that `-e` can pin an execution directory. Selecting a directory directly is guarded by a three-tier safety system, reproduced here from the CLI's own `--help` output: ```text Execution Safety: Hankweave implements a three-tier safety system for execution directories: - Tier 1: Cannot use ~/.hankweave-executions/ directly (reserved for auto-managed) - Tier 2: Directories with existing .hankweave/ require --force (backs up existing) - Tier 3: Non-empty directories show warning and prompt for confirmation ``` The `~/.hankweave-executions/` path in step 3 is the auto-managed parent; the safety block applies when an execution directory is selected. We now have a breakpoint boundary: input handling ends at `server.start()`, and runtime work begins at the orchestrator. From there, we can follow the calls into model work and persistence. A **sentinel** is a parallel observer of runtime events; its persistence tree is one of the storage edges below. The diagram shows the full map in two equivalent forms–an ASCII figure and a mermaid rendering of the same graph–with the orchestrator at the center and each storage subsystem as an edge labeled with the call that writes to it. ![Runtime map](/content-assets/cf45dff5691c48c0/diagrams/contribute-runtime-architecture/1.svg) Runtime map
Diagram as text ```text [CLI] CLI entry (index.ts) | | parsed CLI args + positional paths → resolveSettings() v [Config] Config resolution (config.ts) | | merged config → setupExecutionEnvironment() → execution dir v [ExecSetup] Execution setup (execution-setup.ts) | | execution setup → validateHank() → schema + strict-ref gate pass v [Validate] Validate (validateHank, config.ts) | | validateHank() passes → new HankweaveRuntime(serverConfig) → server.start() v [Orchestrator] HankweaveRuntime (hankweave-runtime.ts) | | startCodon() → runCodon() → new CodonRunner() → runner.run() v [Harness] CodonRunner + Harnesses [Orchestrator] -- handles codon completion → createCheckpoint() → git commit in checkpoints/ --> [Checkpoints] Checkpoints (.hankweavecheckpoints) [Orchestrator] -- emits typed ServerEvents → eventJournal.append() → events.jsonl --> [Events] Event Journal (events.jsonl) [Orchestrator] -- state transitions → stateManager.transition() → atomic save to state.json --> [State] State (state.json) [Orchestrator] -- during codon execution → sentinelManager manages parallel observers --> [Sentinels] Sentinel Manager (history/ for conversational JSON; outputs// for auto-generated output) ```
The labels in this map are source-level signposts, not a public API. They identify where to begin a code reading or breakpoint search; the orchestrator's private structure is versioned and non-contractual. The public contract is the hank schema, CLI, WebSocket protocol, and exit codes. ## Following the orchestrator through a codon The diagram places `HankweaveRuntime` at the center; this section walks through what it actually does with a codon. We can read it as a coordinator with named boundaries: it sequences codons, manages state transitions, drives checkpoints, routes events to WebSocket clients, and handles rollback and resume. Its private layout is versioned and non-contractual. The public contract is the hank schema, CLI, WebSocket protocol, and exit codes. A codon **runs** inside that coordination loop. The runtime holds private references to `StateManager`, `CheckpointGit`, `EventJournal`, `SentinelManager`, `RetryCoordinator`, `Budget`, `ArchiveManifestManager`, `LlmProviderRegistry`, a map of codon IDs to `CodonRunner` instances, `ProxyRunner`, and `Replay`. Treat this list as a source-reading map, not a promise about private fields. The execution plan begins with the codon configuration array. `ExecutionPlanner` passes ordinary codons through unchanged. A loop is expanded lazily: plan construction adds its first iteration, and `expandNextIteration()` adds a later iteration after the previous one **terminates**. The plan therefore follows the loop's actual progress. When a codon is ready, `startCodon()` transitions it to `starting`, resolves watched file patterns through `UnifiedFileResolver`, emits initial `file.updated` events for matching files, and delegates to `runCodon()`. `runCodon()` creates a `CodonRunner`, optionally giving it an `ExtensionConfig` when `exhaustWithPrompt` is set. It subscribes through `setupCodonRunnerEventHandlers()` and calls `runner.run(previousSessionId)`. The optional extension path is outside this architecture map; the important boundary here is the runner call. Completion is the next boundary. `handleCodonComplete()` collects the outcome, records final cost, creates the checkpoint, copies output files when configured, archives when configured, and calls `autoStartNextCodon()`. `createCheckpoint()` is the boundary to follow: a checkpoint **seals** the codon's recorded state, while `CheckpointGit` stores the checkpoint commit. `autoStartNextCodon()` rebuilds the execution thread through `StateManager.getExecutionThread()`, reads `thread.nextCodonId`, expands the next loop iteration when needed, and calls `startCodon()` again. The loop **iterates** until all codons complete or one fails. A continuation resumes a run's history, while recovery can **roll back** to a checkpoint. That sequence–plan, start, run, complete, seal, advance–is the mental model to use when placing a change. The retry boundary is in `HankweaveRuntime`, not `CodonRunner`. `handleCodonComplete()` asks `retryCoordinator.decide()` whether to retry; the runtime then accumulates the failed attempt's cost, records it with `recordAttempt()`, waits through the backoff delay, and re-invokes `startCodon()` for another attempt. A `CodonRunner` lifetime is one attempt; the runtime owns the failure policy and respawn. For the complete event catalog, including the payload fields, category, journaled flag, and sentinel-routed flag of all 36 server events, see the [events reference](/0.10.0/files/reference/events). > **DeepDive:** `RetryCoordinator` keeps retry counters in memory. A server restart during a retry loses that counter and leaves the codon failed. ## Choosing between the two in-process harnesses Each `CodonRunner` needs a model provider, and that is the harness boundary. A **harness** is an in-process adapter that presents a model provider to the codon runner. The boundary is narrower than older documentation suggests: version 0.10.0 has exactly two harnesses, the Claude Agent SDK harness for Anthropic models, and the embedded Pi harness for everything else addressed as `pi//`. Subprocess shims for gemini-cli, codex, and opencode were removed in 0.8.0. `selectHarness()` in `provider-ids.ts` is the single late-bound dispatch rule. It consumes the resolved model and, when present, the `harnessOverride` input: * When `harnessOverride === "pi"`, it selects the embedded Pi harness. * A model whose real provider is Anthropic selects the Claude Agent SDK harness. Amazon Bedrock also selects it when the model ID identifies an Anthropic model. * Every other model selects the embedded Pi harness. The Claude Agent SDK harness calls `query()` from `@anthropic-ai/claude-agent-sdk` in process. Its default stream-inactivity timeout is 180 seconds. On first use, it extracts the SDK binary from a vendored tarball into `~/.hankweave/claude-sdk//`. The embedded Pi harness uses the Pi coding-agent SDK in process. It builds a target with `toPiTarget()` in the form `/`. That step resolves `zhipuai` to `zai`, `moonshot` to `moonshotai`, and routes `moonshotai` through OpenRouter. Its default idle timeout is 120 seconds. Both managers extend `BaseProcessManager`, a shared `TypedEventEmitter`: an object that publishes named events with declared shapes. Both expose the same `stdout`, `stderr`, `error`, and `exit` event contract. `CodonRunner` wraps either manager uniformly and sends those events through the same parser pipeline. Its `createProcessManager()` method selects the harness, supplies codon-level model, prompt, system prompt, log path, idle timeout, and budget settings, and returns a `ClaudeAgentSDKManager | PiSdkManager | ReplayProcessManager` manager. When replay configuration is present, it uses `ReplayProcessManager` instead of a live provider manager; otherwise, `selectHarness()` chooses Claude Agent SDK or Pi. Replay is therefore a separate execution path, not a third provider choice. Since 0.10.0, the Pi path performs a `model_catalog` preflight at startup. It checks every Pi-routed codon model against Pi's catalog. A miss stops the run before any codon runs and reports `Pi model not found: /. Available '' models: …`. Idle timeouts are configurable per codon with `shimIdleTimeout`. The CLI's `--shim-idle-timeout` sets the default. The default is 120 seconds for Pi and 180 seconds for the Claude Agent SDK. Keep this separate from the WebSocket and proxy `--idle-timeout` and from budget wall-clock limits. Failure classification is shared: first-match-wins keyword matching examines error text and separates retriable from permanent failures. The complete taxonomy belongs to [errors and exit codes](/0.10.0/files/reference/errors-and-exit-codes), rather than this architecture map. > **VersionNote:** Before 0.8.0, the five-harness arrangement listed Claude CLI, Gemini CLI, Codex CLI, OpenCode, and an in-process Claude manager. Since 0.8.0, provider work uses two in-process SDK harnesses. ## Mapping what the runtime writes to disk Everything the orchestrator and harnesses do leaves a record, and those records converge on one place. The execution directory's `.hankweave/` subdirectory is the single persistence root created at server start, where each subsystem records part of a run. The complete directory tree belongs to [the execution-directory reference](/0.10.0/files/reference/execution-directory); the table below works in the other direction, from a file to the writer to inspect when that file matters. | `.hankweave/` entry | Owning subsystem | What it records | | ----------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `state.json` | `StateManager` | The authoritative run record: runs, codons with status, cost, and checkpoint SHAs, the execution plan, and the current run pointer. | | `state.json.bak` | `StateManager` | The backup made before each atomic write, used for recovery if a write crashes mid-operation. | | `events/events.jsonl` | `FileEventStorage` through `EventJournal` | The append-only JSONL event journal, with one JSON object per line. | | `checkpoints/.hankweavecheckpoints` | `CheckpointGit` | The shadow, non-bare Git repository containing checkpoint commits. | | `runs//` | `HankweaveRuntime` creates the directory; `CodonRunner` writes logs | Per-run codon logs. | | `archive-manifest.json` | `ArchiveManifestManager` | Archive and restore entries for files moved to `rigArchive/`, keyed by codon and loop iteration. | | `sentinels/` | `SentinelManager` | Persistence tree: conversational-sentinel JSON history in `history/`; auto-generated sentinel output under `outputs//`. | | `logs/server.log` | `Logger` | The runtime's structured log. | | `runtime.lock` | `HankweaveRuntime` | The lock that prevents concurrent access to the execution directory. | The writers named in the table each have their own durability behavior. `StateManager` persists `state.json` atomically by writing a temporary file and renaming it into place. Before each write it creates `state.json.bak`, leaving a recovery copy if the write crashes. `FileEventStorage` writes `events/events.jsonl`; the runtime serializes appends through `eventJournalAppendQueue` before they reach the journal. `CheckpointGit` initializes a **non-bare** repository with `git.init(false)`. Its Git metadata is `.hankweave/checkpoints/.hankweavecheckpoints`, and its work tree is `agentRoot` under the execution directory; the legacy `.git` name is a migration source, not the current layout. A checkpoint is a commit of selected workspace files. Fresh runs can produce different commit SHAs when model-produced files differ, so a SHA is not a repeatability promise. Because the checkpoint repository's metadata and work tree live in nonstandard locations, inspecting its history requires selecting both paths explicitly rather than relying on the current working directory or the main project repository: ```sh EXECUTION=/path/to/execution GIT_DIR="$EXECUTION/.hankweave/checkpoints/.hankweavecheckpoints" GIT_WORK_TREE="$EXECUTION/agentRoot" git --git-dir="$GIT_DIR" --work-tree="$GIT_WORK_TREE" log --oneline ``` `HankweaveRuntime` ensures `runs//` at run start, and `CodonRunner` writes each codon's log there. Log naming belongs to [observe and debug](/0.10.0/files/operate/observe-and-debug), so this page does not reproduce that rule. `ArchiveManifestManager` records files moved to `rigArchive/`. When persistence is enabled, `SentinelManager` creates/verifies `.hankweave/sentinels/history` during initialization. Conversational sentinels lazily persist per-instance conversation history there as `-codon-.json`; auto-generated sentinel output files use `.hankweave/sentinels/outputs//`. A sentinel **observes** runtime events in parallel; its output and state rules belong to [sentinel configuration](/0.10.0/files/reference/sentinel-config). `logs/server.log` is the runtime's own structured log. `runtime.lock` is written at server start and removed at shutdown. Its presence prevents concurrent access to the same execution directory, and `--cleanup` refuses to remove a directory while that lock exists. The server log also closes the loop with the first section of this page: the startup path traced there appears in it as a readable sequence. In a complete one-codon haiku run we can follow `Starting Hankweave Runtime v0.10.0`, shadow-repository initialization, a new run, the WebSocket listener, the autostart trigger, `Starting codon: Write one line`, and `Shutdown: all codons completed (exit code: 0)`. The capture below shows those landmarks as they appear in a real run's log, followed by an annotated rendering of the same lines. *Selected lines from the published execution-directory fixture's `server.log`.* ```text [] [INFO] Starting Hankweave Runtime v0.10.0 in ~/.hankweave-executions/ [] [INFO] Shadow git repository initialized with initial commit: [] [INFO] Started new run: [] [INFO] WebSocket server listening on port [] [INFO] [requestAutostart] Triggering initial autostart [] [INFO] Starting codon: Write one line … [] [INFO] Shutdown: all codons completed (exit code: 0) ``` ## Reconstructing runs and observing state changes The persistence map assumes one tidy run, but a run is not always a single uninterrupted history. The execution thread is the unified codon history across continuation-chained runs. `analyzeExecutionThread()` reconstructs it by walking backward from the latest run through `startingConditions.source.runId` until it reaches a fresh start. When you inspect a continuation, follow those run links rather than reading only the newest run. The state machine in `state-manager.ts` enforces an eight-state codon lifecycle through the typed `CodonTransitions` table, with metadata checks supplied by `state-transition-guards.ts`. See the [state machine page](/0.10.0/files/contribute/state-machine) for the state names and transition details. When observing a change, we need to account for the queue: transitions are processed sequentially, and `transition()` is fire-and-forget. It places the request on the internal queue instead of waiting for the applied state. If we call `getState()` immediately after `transition()`, we might not see the change yet. The state machine is eventually consistent: the returned state can lag behind a requested transition. To react to the applied transition, listen for the state manager's `on('stateChanged', …)` event rather than reading immediately after `transition()`. At state-manager load, `detectCrashedRuns()` checks whether a run marked `running` still has a living operating-system process. If it does not, the loader marks the run `crashed` and emits a `RunCrashed` event. This detection occurs while state is loaded, before later contributor tooling interprets the run. These execution-thread and state-manager modules are source-level documentation for contributors. Their repository-relative files are `server/execution-thread.ts` and `server/state-manager.ts`; `hankweave/server/...` specifiers are not public package imports. The published package exports only `.`, `./schemas`, and `./types`; use [client and exported types](/0.10.0/files/integrate/client-and-exported-types) for that public surface.